]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Internal restructuring of the OPFS sqlite3_vfs in order to facilitate certain experim...
authorstephan <stephan@noemail.net>
Tue, 29 Nov 2022 05:25:08 +0000 (05:25 +0000)
committerstephan <stephan@noemail.net>
Tue, 29 Nov 2022 05:25:08 +0000 (05:25 +0000)
FossilOrigin-Name: 0c5c51f4fb04a4b90c50ec9704cfea9a3fb7d7d0ee55c1b0d4476129188217a6

ext/wasm/api/sqlite3-api-opfs.js
ext/wasm/api/sqlite3-opfs-async-proxy.js
ext/wasm/tester1.c-pp.js
ext/wasm/tests/opfs/concurrency/index.html
manifest
manifest.uuid

index 458448175b549b26fd39cb7d3efeb6a97ee46338..cef15305d445be8c0e410e55af690c76958ac8c6 100644 (file)
@@ -76,15 +76,25 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
   `opfs` property, containing several OPFS-specific utilities.
 */
 const installOpfsVfs = function callee(options){
-  if(!self.SharedArrayBuffer ||
-     !self.Atomics ||
-     !self.FileSystemHandle ||
-     !self.FileSystemDirectoryHandle ||
-     !self.FileSystemFileHandle ||
-     !self.FileSystemFileHandle.prototype.createSyncAccessHandle ||
-     !navigator.storage.getDirectory){
+  if(!self.SharedArrayBuffer
+    || !self.Atomics){
     return Promise.reject(
-      new Error("This environment does not have OPFS support.")
+      new Error("Cannot install OPFS: Missing SharedArrayBuffer and/or Atomics. "+
+                "The server must emit the COOP/COEP response headers to enable those. "+
+                "See https://sqlite.org/wasm/doc/trunk/persistence.md#coop-coep")
+    );
+  }else if(self.window===self && self.document){
+    return Promise.reject(
+      new Error("The OPFS sqlite3_vfs cannot run in the main thread "+
+                "because it requires Atomics.wait().")
+    );
+  }else if(!self.FileSystemHandle ||
+           !self.FileSystemDirectoryHandle ||
+           !self.FileSystemFileHandle ||
+           !self.FileSystemFileHandle.prototype.createSyncAccessHandle ||
+           !navigator.storage.getDirectory){
+    return Promise.reject(
+      new Error("Missing required OPFS APIs.")
     );
   }
   if(!options || 'object'!==typeof options){
@@ -134,6 +144,18 @@ const installOpfsVfs = function callee(options){
        OPFS-specific sqlite3_vfs evolves.
     */
     const opfsUtil = Object.create(null);
+
+    /**
+       Returns true if _this_ thread has access to the OPFS APIs.
+    */
+    const thisThreadHasOPFS = ()=>{
+      return self.FileSystemHandle &&
+        self.FileSystemDirectoryHandle &&
+        self.FileSystemFileHandle &&
+        self.FileSystemFileHandle.prototype.createSyncAccessHandle &&
+        navigator.storage.getDirectory;
+    };
+
     /**
        Not part of the public API. Solely for internal/development
        use.
@@ -1179,12 +1201,16 @@ const installOpfsVfs = function callee(options){
     //consideration.
 
     if(sqlite3.oo1){
-      opfsUtil.OpfsDb = function(...args){
+      const OpfsDb = function(...args){
         const opt = sqlite3.oo1.DB.dbCtorHelper.normalizeArgs(...args);
         opt.vfs = opfsVfs.$zName;
         sqlite3.oo1.DB.dbCtorHelper.call(this, opt);
       };
-      opfsUtil.OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype);
+      sqlite3.oo1.OpfsDb =
+        opfsUtil.OpfsDb /* sqlite3.opfs.OpfsDb => deprecated name -
+                           will be phased out Real Soon */ =
+        OpfsDb;
+      OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype);
       sqlite3.oo1.DB.dbCtorHelper.setVfsPostOpenSql(
         opfsVfs.pointer,
         [
@@ -1206,13 +1232,6 @@ const installOpfsVfs = function callee(options){
       );
     }
 
-    /**
-       Potential TODOs:
-
-       - Expose one or both of the Worker objects via opfsUtil and
-         publish an interface for proxying the higher-level OPFS
-         features like getting a directory listing.
-    */
     const sanityCheck = function(){
       const scope = wasm.scopedAllocPush();
       const sq3File = new sqlite3_file();
@@ -1282,6 +1301,11 @@ const installOpfsVfs = function callee(options){
     W.onmessage = function({data}){
       //log("Worker.onmessage:",data);
       switch(data.type){
+          case 'opfs-unavailable':
+            /* Async proxy has determined that OPFS is unavailable. There's
+               nothing more for us to do here. */
+            promiseReject(new Error(data.payload.join(' ')));
+            break;
           case 'opfs-async-loaded':
             /*Arrives as soon as the asyc proxy finishes loading.
               Pass our config and shared state on to the async worker.*/
@@ -1308,14 +1332,18 @@ const installOpfsVfs = function callee(options){
                 warn("Running sanity checks because of opfs-sanity-check URL arg...");
                 sanityCheck();
               }
-              navigator.storage.getDirectory().then((d)=>{
-                W.onerror = W._originalOnError;
-                delete W._originalOnError;
-                sqlite3.opfs = opfsUtil;
-                opfsUtil.rootDirectory = d;
-                log("End of OPFS sqlite3_vfs setup.", opfsVfs);
+              if(thisThreadHasOPFS()){
+                navigator.storage.getDirectory().then((d)=>{
+                  W.onerror = W._originalOnError;
+                  delete W._originalOnError;
+                  sqlite3.opfs = opfsUtil;
+                  opfsUtil.rootDirectory = d;
+                  log("End of OPFS sqlite3_vfs setup.", opfsVfs);
+                  promiseResolve(sqlite3);
+                });
+              }else{
                 promiseResolve(sqlite3);
-              });
+              }                
             }catch(e){
               error(e);
               promiseReject(e);
@@ -1334,9 +1362,6 @@ const installOpfsVfs = function callee(options){
 installOpfsVfs.defaultProxyUri =
   "sqlite3-opfs-async-proxy.js";
 self.sqlite3ApiBootstrap.initializersAsync.push(async (sqlite3)=>{
-  if(sqlite3.scriptInfo && !sqlite3.scriptInfo.isWorker){
-    return;
-  }
   try{
     let proxyJs = installOpfsVfs.defaultProxyUri;
     if(sqlite3.scriptInfo.sqlite3Dir){
index 8e60969bc61b14c8b1357076a4af400e5ba3279c..1ba6e9bdbb7005d76153130f304070f0e28eeb1b 100644 (file)
   usage of those methods to remove the "await".
 */
 "use strict";
-const toss = function(...args){throw new Error(args.join(' '))};
-if(self.window === self){
-  toss("This code cannot run from the main thread.",
-       "Load it as a Worker from a separate Worker.");
-}else if(!navigator.storage.getDirectory){
-  toss("This API requires navigator.storage.getDirectory.");
-}
+const wPost = (type,...args)=>postMessage({type, payload:args});
+const installAsyncProxy = function(self){
+  const toss = function(...args){throw new Error(args.join(' '))};
+  if(self.window === self){
+    toss("This code cannot run from the main thread.",
+         "Load it as a Worker from a separate Worker.");
+  }else if(!navigator.storage.getDirectory){
+    toss("This API requires navigator.storage.getDirectory.");
+  }
 
-/**
-   Will hold state copied to this object from the syncronous side of
-   this API.
-*/
-const state = Object.create(null);
+  /**
+     Will hold state copied to this object from the syncronous side of
+     this API.
+  */
+  const state = Object.create(null);
 
-/**
-   verbose:
+  /**
+     verbose:
 
-   0 = no logging output
-   1 = only errors
-   2 = warnings and errors
-   3 = debug, warnings, and errors
-*/
-state.verbose = 1;
-
-const loggers = {
-  0:console.error.bind(console),
-  1:console.warn.bind(console),
-  2:console.log.bind(console)
-};
-const logImpl = (level,...args)=>{
-  if(state.verbose>level) loggers[level]("OPFS asyncer:",...args);
-};
-const log =    (...args)=>logImpl(2, ...args);
-const warn =   (...args)=>logImpl(1, ...args);
-const error =  (...args)=>logImpl(0, ...args);
-const metrics = Object.create(null);
-metrics.reset = ()=>{
-  let k;
-  const r = (m)=>(m.count = m.time = m.wait = 0);
-  for(k in state.opIds){
-    r(metrics[k] = Object.create(null));
-  }
-  let s = metrics.s11n = Object.create(null);
-  s = s.serialize = Object.create(null);
-  s.count = s.time = 0;
-  s = metrics.s11n.deserialize = Object.create(null);
-  s.count = s.time = 0;
-};
-metrics.dump = ()=>{
-  let k, n = 0, t = 0, w = 0;
-  for(k in state.opIds){
-    const m = metrics[k];
-    n += m.count;
-    t += m.time;
-    w += m.wait;
-    m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
-  }
-  console.log(self.location.href,
-              "metrics for",self.location.href,":\n",
-              metrics,
-              "\nTotal of",n,"op(s) for",t,"ms",
-              "approx",w,"ms spent waiting on OPFS APIs.");
-  console.log("Serialization metrics:",metrics.s11n);
-};
-
-/**
-   __openFiles is a map of sqlite3_file pointers (integers) to
-   metadata related to a given OPFS file handles. The pointers are, in
-   this side of the interface, opaque file handle IDs provided by the
-   synchronous part of this constellation. Each value is an object
-   with a structure demonstrated in the xOpen() impl.
-*/
-const __openFiles = Object.create(null);
-/**
-   __implicitLocks is a Set of sqlite3_file pointers (integers) which were
-   "auto-locked".  i.e. those for which we obtained a sync access
-   handle without an explicit xLock() call. Such locks will be
-   released during db connection idle time, whereas a sync access
-   handle obtained via xLock(), or subsequently xLock()'d after
-   auto-acquisition, will not be released until xUnlock() is called.
-
-   Maintenance reminder: if we relinquish auto-locks at the end of the
-   operation which acquires them, we pay a massive performance
-   penalty: speedtest1 benchmarks take up to 4x as long. By delaying
-   the lock release until idle time, the hit is negligible.
-*/
-const __implicitLocks = new Set();
+     0 = no logging output
+     1 = only errors
+     2 = warnings and errors
+     3 = debug, warnings, and errors
+  */
+  state.verbose = 1;
 
-/**
-   Expects an OPFS file path. It gets resolved, such that ".."
-   components are properly expanded, and returned. If the 2nd arg is
-   true, the result is returned as an array of path elements, else an
-   absolute path string is returned.
-*/
-const getResolvedPath = function(filename,splitIt){
-  const p = new URL(
-    filename, 'file://irrelevant'
-  ).pathname;
-  return splitIt ? p.split('/').filter((v)=>!!v) : p;
-};
-
-/**
-   Takes the absolute path to a filesystem element. Returns an array
-   of [handleOfContainingDir, filename]. If the 2nd argument is truthy
-   then each directory element leading to the file is created along
-   the way. Throws if any creation or resolution fails.
-*/
-const getDirForFilename = async function f(absFilename, createDirs = false){
-  const path = getResolvedPath(absFilename, true);
-  const filename = path.pop();
-  let dh = state.rootDir;
-  for(const dirName of path){
-    if(dirName){
-      dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs});
+  const loggers = {
+    0:console.error.bind(console),
+    1:console.warn.bind(console),
+    2:console.log.bind(console)
+  };
+  const logImpl = (level,...args)=>{
+    if(state.verbose>level) loggers[level]("OPFS asyncer:",...args);
+  };
+  const log =    (...args)=>logImpl(2, ...args);
+  const warn =   (...args)=>logImpl(1, ...args);
+  const error =  (...args)=>logImpl(0, ...args);
+  const metrics = Object.create(null);
+  metrics.reset = ()=>{
+    let k;
+    const r = (m)=>(m.count = m.time = m.wait = 0);
+    for(k in state.opIds){
+      r(metrics[k] = Object.create(null));
     }
-  }
-  return [dh, filename];
-};
-
-/**
-   If the given file-holding object has a sync handle attached to it,
-   that handle is remove and asynchronously closed. Though it may
-   sound sensible to continue work as soon as the close() returns
-   (noting that it's asynchronous), doing so can cause operations
-   performed soon afterwards, e.g. a call to getSyncHandle() to fail
-   because they may happen out of order from the close(). OPFS does
-   not guaranty that the actual order of operations is retained in
-   such cases. i.e.  always "await" on the result of this function.
-*/
-const closeSyncHandle = async (fh)=>{
-  if(fh.syncHandle){
-    log("Closing sync handle for",fh.filenameAbs);
-    const h = fh.syncHandle;
-    delete fh.syncHandle;
-    delete fh.xLock;
-    __implicitLocks.delete(fh.fid);
-    return h.close();
-  }
-};
-
-/**
-   A proxy for closeSyncHandle() which is guaranteed to not throw.
-
-   This function is part of a lock/unlock step in functions which
-   require a sync access handle but may be called without xLock()
-   having been called first. Such calls need to release that
-   handle to avoid locking the file for all of time. This is an
-   _attempt_ at reducing cross-tab contention but it may prove
-   to be more of a problem than a solution and may need to be
-   removed.
-*/
-const closeSyncHandleNoThrow = async (fh)=>{
-  try{await closeSyncHandle(fh)}
-  catch(e){
-    warn("closeSyncHandleNoThrow() ignoring:",e,fh);
-  }
-};
-
-/* Release all auto-locks. */
-const releaseImplicitLocks = async ()=>{
-  if(__implicitLocks.size){
-    /* Release all auto-locks. */
-    for(const fid of __implicitLocks){
-      const fh = __openFiles[fid];
-      await closeSyncHandleNoThrow(fh);
-      log("Auto-unlocked",fid,fh.filenameAbs);
+    let s = metrics.s11n = Object.create(null);
+    s = s.serialize = Object.create(null);
+    s.count = s.time = 0;
+    s = metrics.s11n.deserialize = Object.create(null);
+    s.count = s.time = 0;
+  };
+  metrics.dump = ()=>{
+    let k, n = 0, t = 0, w = 0;
+    for(k in state.opIds){
+      const m = metrics[k];
+      n += m.count;
+      t += m.time;
+      w += m.wait;
+      m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
     }
-  }
-};
+    console.log(self.location.href,
+                "metrics for",self.location.href,":\n",
+                metrics,
+                "\nTotal of",n,"op(s) for",t,"ms",
+                "approx",w,"ms spent waiting on OPFS APIs.");
+    console.log("Serialization metrics:",metrics.s11n);
+  };
 
-/**
-   An experiment in improving concurrency by freeing up implicit locks
-   sooner. This is known to impact performance dramatically but it has
-   also shown to improve concurrency considerably.
+  /**
+     __openFiles is a map of sqlite3_file pointers (integers) to
+     metadata related to a given OPFS file handles. The pointers are, in
+     this side of the interface, opaque file handle IDs provided by the
+     synchronous part of this constellation. Each value is an object
+     with a structure demonstrated in the xOpen() impl.
+  */
+  const __openFiles = Object.create(null);
+  /**
+     __implicitLocks is a Set of sqlite3_file pointers (integers) which were
+     "auto-locked".  i.e. those for which we obtained a sync access
+     handle without an explicit xLock() call. Such locks will be
+     released during db connection idle time, whereas a sync access
+     handle obtained via xLock(), or subsequently xLock()'d after
+     auto-acquisition, will not be released until xUnlock() is called.
+
+     Maintenance reminder: if we relinquish auto-locks at the end of the
+     operation which acquires them, we pay a massive performance
+     penalty: speedtest1 benchmarks take up to 4x as long. By delaying
+     the lock release until idle time, the hit is negligible.
+  */
+  const __implicitLocks = new Set();
 
-   If fh.releaseImplicitLocks is truthy and fh is in __implicitLocks,
-   this routine returns closeSyncHandleNoThrow(), else it is a no-op.
-*/
-const releaseImplicitLock = async (fh)=>{
-  if(fh.releaseImplicitLocks && __implicitLocks.has(fh.fid)){
-    return closeSyncHandleNoThrow(fh);
-  }
-};
-
-/**
-   An error class specifically for use with getSyncHandle(), the goal
-   of which is to eventually be able to distinguish unambiguously
-   between locking-related failures and other types, noting that we
-   cannot currently do so because createSyncAccessHandle() does not
-   define its exceptions in the required level of detail.
-*/
-class GetSyncHandleError extends Error {
-  constructor(errorObject, ...msg){
-    super();
-    this.error = errorObject;
-    this.message = [
-      ...msg, ': Original exception ['+errorObject.name+']:',
-      errorObject.message
-    ].join(' ');
-    this.name = 'GetSyncHandleError';
-  }
-};
-GetSyncHandleError.convertRc = (e,rc)=>{
-  if(0){
-    /* This approach makes the very wild assumption that such a
-       failure _is_ a locking error. In practice that appears to be
-       the most common error, by far, but we cannot unambiguously
-       distinguish that from other errors.
-
-       This approach is highly questionable.
-    */
-    return (e instanceof GetSyncHandleError)
-      ? state.sq3Codes.SQLITE_IOERR_LOCK
-      : rc;
-  }else{
-    return rc;
-  }
-}
-/**
-   Returns the sync access handle associated with the given file
-   handle object (which must be a valid handle object, as created by
-   xOpen()), lazily opening it if needed.
-
-   In order to help alleviate cross-tab contention for a dabase,
-   if an exception is thrown while acquiring the handle, this routine
-   will wait briefly and try again, up to 3 times. If acquisition
-   still fails at that point it will give up and propagate the
-   exception.
-*/
-const getSyncHandle = async (fh,opName)=>{
-  if(!fh.syncHandle){
-    const t = performance.now();
-    log("Acquiring sync handle for",fh.filenameAbs);
-    const maxTries = 6, msBase = 300;
-    let i = 1, ms = msBase;
-    for(; true; ms = msBase * ++i){
-      try {
-        //if(i<3) toss("Just testing getSyncHandle() wait-and-retry.");
-        //TODO? A config option which tells it to throw here
-        //randomly every now and then, for testing purposes.
-        fh.syncHandle = await fh.fileHandle.createSyncAccessHandle();
-        break;
-      }catch(e){
-        if(i === maxTries){
-          throw new GetSyncHandleError(
-            e, "Error getting sync handle for",opName+"().",maxTries,
-            "attempts failed.",fh.filenameAbs
-          );
-        }
-        warn("Error getting sync handle for",opName+"(). Waiting",ms,
-             "ms and trying again.",fh.filenameAbs,e);
-        //await releaseImplicitLocks();
-        Atomics.wait(state.sabOPView, state.opIds.retry, 0, ms);
+  /**
+     Expects an OPFS file path. It gets resolved, such that ".."
+     components are properly expanded, and returned. If the 2nd arg is
+     true, the result is returned as an array of path elements, else an
+     absolute path string is returned.
+  */
+  const getResolvedPath = function(filename,splitIt){
+    const p = new URL(
+      filename, 'file://irrelevant'
+    ).pathname;
+    return splitIt ? p.split('/').filter((v)=>!!v) : p;
+  };
+
+  /**
+     Takes the absolute path to a filesystem element. Returns an array
+     of [handleOfContainingDir, filename]. If the 2nd argument is truthy
+     then each directory element leading to the file is created along
+     the way. Throws if any creation or resolution fails.
+  */
+  const getDirForFilename = async function f(absFilename, createDirs = false){
+    const path = getResolvedPath(absFilename, true);
+    const filename = path.pop();
+    let dh = state.rootDir;
+    for(const dirName of path){
+      if(dirName){
+        dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs});
       }
     }
-    log("Got",opName+"() sync handle for",fh.filenameAbs,
-        'in',performance.now() - t,'ms');
-    if(!fh.xLock){
-      __implicitLocks.add(fh.fid);
-      log("Auto-locked for",opName+"()",fh.fid,fh.filenameAbs);
+    return [dh, filename];
+  };
+
+  /**
+     If the given file-holding object has a sync handle attached to it,
+     that handle is remove and asynchronously closed. Though it may
+     sound sensible to continue work as soon as the close() returns
+     (noting that it's asynchronous), doing so can cause operations
+     performed soon afterwards, e.g. a call to getSyncHandle() to fail
+     because they may happen out of order from the close(). OPFS does
+     not guaranty that the actual order of operations is retained in
+     such cases. i.e.  always "await" on the result of this function.
+  */
+  const closeSyncHandle = async (fh)=>{
+    if(fh.syncHandle){
+      log("Closing sync handle for",fh.filenameAbs);
+      const h = fh.syncHandle;
+      delete fh.syncHandle;
+      delete fh.xLock;
+      __implicitLocks.delete(fh.fid);
+      return h.close();
     }
-  }
-  return fh.syncHandle;
-};
+  };
 
-/**
-   Stores the given value at state.sabOPView[state.opIds.rc] and then
-   Atomics.notify()'s it.
-*/
-const storeAndNotify = (opName, value)=>{
-  log(opName+"() => notify(",value,")");
-  Atomics.store(state.sabOPView, state.opIds.rc, value);
-  Atomics.notify(state.sabOPView, state.opIds.rc);
-};
-
-/**
-   Throws if fh is a file-holding object which is flagged as read-only.
-*/
-const affirmNotRO = function(opName,fh){
-  if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs);
-};
-const affirmLocked = function(opName,fh){
-  //if(!fh.syncHandle) toss(opName+"(): File does not have a lock: "+fh.filenameAbs);
   /**
-     Currently a no-op, as speedtest1 triggers xRead() without a
-     lock (that seems like a bug but it's currently uninvestigated).
-     This means, however, that some OPFS VFS routines may trigger
-     acquisition of a lock but never let it go until xUnlock() is
-     called (which it likely won't be if xLock() was not called).
+     A proxy for closeSyncHandle() which is guaranteed to not throw.
+
+     This function is part of a lock/unlock step in functions which
+     require a sync access handle but may be called without xLock()
+     having been called first. Such calls need to release that
+     handle to avoid locking the file for all of time. This is an
+     _attempt_ at reducing cross-tab contention but it may prove
+     to be more of a problem than a solution and may need to be
+     removed.
   */
-};
-
-/**
-   We track 2 different timers: the "metrics" timer records how much
-   time we spend performing work. The "wait" timer records how much
-   time we spend waiting on the underlying OPFS timer. See the calls
-   to mTimeStart(), mTimeEnd(), wTimeStart(), and wTimeEnd()
-   throughout this file to see how they're used.
-*/
-const __mTimer = Object.create(null);
-__mTimer.op = undefined;
-__mTimer.start = undefined;
-const mTimeStart = (op)=>{
-  __mTimer.start = performance.now();
-  __mTimer.op = op;
-  //metrics[op] || toss("Maintenance required: missing metrics for",op);
-  ++metrics[op].count;
-};
-const mTimeEnd = ()=>(
-  metrics[__mTimer.op].time += performance.now() - __mTimer.start
-);
-const __wTimer = Object.create(null);
-__wTimer.op = undefined;
-__wTimer.start = undefined;
-const wTimeStart = (op)=>{
-  __wTimer.start = performance.now();
-  __wTimer.op = op;
-  //metrics[op] || toss("Maintenance required: missing metrics for",op);
-};
-const wTimeEnd = ()=>(
-  metrics[__wTimer.op].wait += performance.now() - __wTimer.start
-);
-
-/**
-   Gets set to true by the 'opfs-async-shutdown' command to quit the
-   wait loop. This is only intended for debugging purposes: we cannot
-   inspect this file's state while the tight waitLoop() is running and
-   need a way to stop that loop for introspection purposes.
-*/
-let flagAsyncShutdown = false;
+  const closeSyncHandleNoThrow = async (fh)=>{
+    try{await closeSyncHandle(fh)}
+    catch(e){
+      warn("closeSyncHandleNoThrow() ignoring:",e,fh);
+    }
+  };
 
+  /* Release all auto-locks. */
+  const releaseImplicitLocks = async ()=>{
+    if(__implicitLocks.size){
+      /* Release all auto-locks. */
+      for(const fid of __implicitLocks){
+        const fh = __openFiles[fid];
+        await closeSyncHandleNoThrow(fh);
+        log("Auto-unlocked",fid,fh.filenameAbs);
+      }
+    }
+  };
 
-/**
-   Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods
-   methods, as well as helpers like mkdir(). Maintenance reminder:
-   members are in alphabetical order to simplify finding them.
-*/
-const vfsAsyncImpls = {
-  'opfs-async-metrics': async ()=>{
-    mTimeStart('opfs-async-metrics');
-    metrics.dump();
-    storeAndNotify('opfs-async-metrics', 0);
-    mTimeEnd();
-  },
-  'opfs-async-shutdown': async ()=>{
-    flagAsyncShutdown = true;
-    storeAndNotify('opfs-async-shutdown', 0);
-  },
-  mkdir: async (dirname)=>{
-    mTimeStart('mkdir');
-    let rc = 0;
-    wTimeStart('mkdir');
-    try {
-        await getDirForFilename(dirname+"/filepart", true);
-    }catch(e){
-      state.s11n.storeException(2,e);
-      rc = state.sq3Codes.SQLITE_IOERR;
-    }finally{
-      wTimeEnd();
+  /**
+     An experiment in improving concurrency by freeing up implicit locks
+     sooner. This is known to impact performance dramatically but it has
+     also shown to improve concurrency considerably.
+
+     If fh.releaseImplicitLocks is truthy and fh is in __implicitLocks,
+     this routine returns closeSyncHandleNoThrow(), else it is a no-op.
+  */
+  const releaseImplicitLock = async (fh)=>{
+    if(fh.releaseImplicitLocks && __implicitLocks.has(fh.fid)){
+      return closeSyncHandleNoThrow(fh);
     }
-    storeAndNotify('mkdir', rc);
-    mTimeEnd();
-  },
-  xAccess: async (filename)=>{
-    mTimeStart('xAccess');
-    /* OPFS cannot support the full range of xAccess() queries sqlite3
-       calls for. We can essentially just tell if the file is
-       accessible, but if it is it's automatically writable (unless
-       it's locked, which we cannot(?) know without trying to open
-       it). OPFS does not have the notion of read-only.
-
-       The return semantics of this function differ from sqlite3's
-       xAccess semantics because we are limited in what we can
-       communicate back to our synchronous communication partner: 0 =
-       accessible, non-0 means not accessible.
-    */
-    let rc = 0;
-    wTimeStart('xAccess');
-    try{
-      const [dh, fn] = await getDirForFilename(filename);
-      await dh.getFileHandle(fn);
-    }catch(e){
-      state.s11n.storeException(2,e);
-      rc = state.sq3Codes.SQLITE_IOERR;
-    }finally{
-      wTimeEnd();
+  };
+
+  /**
+     An error class specifically for use with getSyncHandle(), the goal
+     of which is to eventually be able to distinguish unambiguously
+     between locking-related failures and other types, noting that we
+     cannot currently do so because createSyncAccessHandle() does not
+     define its exceptions in the required level of detail.
+  */
+  class GetSyncHandleError extends Error {
+    constructor(errorObject, ...msg){
+      super();
+      this.error = errorObject;
+      this.message = [
+        ...msg, ': Original exception ['+errorObject.name+']:',
+        errorObject.message
+      ].join(' ');
+      this.name = 'GetSyncHandleError';
     }
-    storeAndNotify('xAccess', rc);
-    mTimeEnd();
-  },
-  xClose: async function(fid/*sqlite3_file pointer*/){
-    const opName = 'xClose';
-    mTimeStart(opName);
-    __implicitLocks.delete(fid);
-    const fh = __openFiles[fid];
-    let rc = 0;
-    wTimeStart(opName);
-    if(fh){
-      delete __openFiles[fid];
-      await closeSyncHandle(fh);
-      if(fh.deleteOnClose){
-        try{ await fh.dirHandle.removeEntry(fh.filenamePart) }
-        catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) }
-      }
+  };
+  GetSyncHandleError.convertRc = (e,rc)=>{
+    if(0){
+      /* This approach makes the very wild assumption that such a
+         failure _is_ a locking error. In practice that appears to be
+         the most common error, by far, but we cannot unambiguously
+         distinguish that from other errors.
+
+         This approach is highly questionable.
+      */
+      return (e instanceof GetSyncHandleError)
+        ? state.sq3Codes.SQLITE_IOERR_LOCK
+        : rc;
     }else{
-      state.s11n.serialize();
-      rc = state.sq3Codes.SQLITE_NOTFOUND;
+      return rc;
     }
-    wTimeEnd();
-    storeAndNotify(opName, rc);
-    mTimeEnd();
-  },
-  xDelete: async function(...args){
-    mTimeStart('xDelete');
-    const rc = await vfsAsyncImpls.xDeleteNoWait(...args);
-    storeAndNotify('xDelete', rc);
-    mTimeEnd();
-  },
-  xDeleteNoWait: async function(filename, syncDir = 0, recursive = false){
-    /* The syncDir flag is, for purposes of the VFS API's semantics,
-       ignored here. However, if it has the value 0x1234 then: after
-       deleting the given file, recursively try to delete any empty
-       directories left behind in its wake (ignoring any errors and
-       stopping at the first failure).
-
-       That said: we don't know for sure that removeEntry() fails if
-       the dir is not empty because the API is not documented. It has,
-       however, a "recursive" flag which defaults to false, so
-       presumably it will fail if the dir is not empty and that flag
-       is false.
-    */
-    let rc = 0;
-    wTimeStart('xDelete');
-    try {
-      while(filename){
-        const [hDir, filenamePart] = await getDirForFilename(filename, false);
-        if(!filenamePart) break;
-        await hDir.removeEntry(filenamePart, {recursive});
-        if(0x1234 !== syncDir) break;
-        recursive = false;
-        filename = getResolvedPath(filename, true);
-        filename.pop();
-        filename = filename.join('/');
+  }
+  /**
+     Returns the sync access handle associated with the given file
+     handle object (which must be a valid handle object, as created by
+     xOpen()), lazily opening it if needed.
+
+     In order to help alleviate cross-tab contention for a dabase,
+     if an exception is thrown while acquiring the handle, this routine
+     will wait briefly and try again, up to 3 times. If acquisition
+     still fails at that point it will give up and propagate the
+     exception.
+  */
+  const getSyncHandle = async (fh,opName)=>{
+    if(!fh.syncHandle){
+      const t = performance.now();
+      log("Acquiring sync handle for",fh.filenameAbs);
+      const maxTries = 6, msBase = 300;
+      let i = 1, ms = msBase;
+      for(; true; ms = msBase * ++i){
+        try {
+          //if(i<3) toss("Just testing getSyncHandle() wait-and-retry.");
+          //TODO? A config option which tells it to throw here
+          //randomly every now and then, for testing purposes.
+          fh.syncHandle = await fh.fileHandle.createSyncAccessHandle();
+          break;
+        }catch(e){
+          if(i === maxTries){
+            throw new GetSyncHandleError(
+              e, "Error getting sync handle for",opName+"().",maxTries,
+              "attempts failed.",fh.filenameAbs
+            );
+          }
+          warn("Error getting sync handle for",opName+"(). Waiting",ms,
+               "ms and trying again.",fh.filenameAbs,e);
+          //await releaseImplicitLocks();
+          Atomics.wait(state.sabOPView, state.opIds.retry, 0, ms);
+        }
+      }
+      log("Got",opName+"() sync handle for",fh.filenameAbs,
+          'in',performance.now() - t,'ms');
+      if(!fh.xLock){
+        __implicitLocks.add(fh.fid);
+        log("Auto-locked for",opName+"()",fh.fid,fh.filenameAbs);
       }
-    }catch(e){
-      state.s11n.storeException(2,e);
-      rc = state.sq3Codes.SQLITE_IOERR_DELETE;
-    }
-    wTimeEnd();
-    return rc;
-  },
-  xFileSize: async function(fid/*sqlite3_file pointer*/){
-    mTimeStart('xFileSize');
-    const fh = __openFiles[fid];
-    let rc;
-    wTimeStart('xFileSize');
-    try{
-      affirmLocked('xFileSize',fh);
-      const sz = await (await getSyncHandle(fh,'xFileSize')).getSize();
-      state.s11n.serialize(Number(sz));
-      rc = 0;
-    }catch(e){
-      state.s11n.storeException(2,e);
-      rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR);
     }
-    await releaseImplicitLock(fh);
-    wTimeEnd();
-    storeAndNotify('xFileSize', rc);
-    mTimeEnd();
-  },
-  xLock: async function(fid/*sqlite3_file pointer*/,
-                        lockType/*SQLITE_LOCK_...*/){
-    mTimeStart('xLock');
-    const fh = __openFiles[fid];
-    let rc = 0;
-    const oldLockType = fh.xLock;
-    fh.xLock = lockType;
-    if( !fh.syncHandle ){
-      wTimeStart('xLock');
+    return fh.syncHandle;
+  };
+
+  /**
+     Stores the given value at state.sabOPView[state.opIds.rc] and then
+     Atomics.notify()'s it.
+  */
+  const storeAndNotify = (opName, value)=>{
+    log(opName+"() => notify(",value,")");
+    Atomics.store(state.sabOPView, state.opIds.rc, value);
+    Atomics.notify(state.sabOPView, state.opIds.rc);
+  };
+
+  /**
+     Throws if fh is a file-holding object which is flagged as read-only.
+  */
+  const affirmNotRO = function(opName,fh){
+    if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs);
+  };
+  const affirmLocked = function(opName,fh){
+    //if(!fh.syncHandle) toss(opName+"(): File does not have a lock: "+fh.filenameAbs);
+    /**
+       Currently a no-op, as speedtest1 triggers xRead() without a
+       lock (that seems like a bug but it's currently uninvestigated).
+       This means, however, that some OPFS VFS routines may trigger
+       acquisition of a lock but never let it go until xUnlock() is
+       called (which it likely won't be if xLock() was not called).
+    */
+  };
+
+  /**
+     We track 2 different timers: the "metrics" timer records how much
+     time we spend performing work. The "wait" timer records how much
+     time we spend waiting on the underlying OPFS timer. See the calls
+     to mTimeStart(), mTimeEnd(), wTimeStart(), and wTimeEnd()
+     throughout this file to see how they're used.
+  */
+  const __mTimer = Object.create(null);
+  __mTimer.op = undefined;
+  __mTimer.start = undefined;
+  const mTimeStart = (op)=>{
+    __mTimer.start = performance.now();
+    __mTimer.op = op;
+    //metrics[op] || toss("Maintenance required: missing metrics for",op);
+    ++metrics[op].count;
+  };
+  const mTimeEnd = ()=>(
+    metrics[__mTimer.op].time += performance.now() - __mTimer.start
+  );
+  const __wTimer = Object.create(null);
+  __wTimer.op = undefined;
+  __wTimer.start = undefined;
+  const wTimeStart = (op)=>{
+    __wTimer.start = performance.now();
+    __wTimer.op = op;
+    //metrics[op] || toss("Maintenance required: missing metrics for",op);
+  };
+  const wTimeEnd = ()=>(
+    metrics[__wTimer.op].wait += performance.now() - __wTimer.start
+  );
+
+  /**
+     Gets set to true by the 'opfs-async-shutdown' command to quit the
+     wait loop. This is only intended for debugging purposes: we cannot
+     inspect this file's state while the tight waitLoop() is running and
+     need a way to stop that loop for introspection purposes.
+  */
+  let flagAsyncShutdown = false;
+
+
+  /**
+     Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods
+     methods, as well as helpers like mkdir(). Maintenance reminder:
+     members are in alphabetical order to simplify finding them.
+  */
+  const vfsAsyncImpls = {
+    'opfs-async-metrics': async ()=>{
+      mTimeStart('opfs-async-metrics');
+      metrics.dump();
+      storeAndNotify('opfs-async-metrics', 0);
+      mTimeEnd();
+    },
+    'opfs-async-shutdown': async ()=>{
+      flagAsyncShutdown = true;
+      storeAndNotify('opfs-async-shutdown', 0);
+    },
+    mkdir: async (dirname)=>{
+      mTimeStart('mkdir');
+      let rc = 0;
+      wTimeStart('mkdir');
       try {
-        await getSyncHandle(fh,'xLock');
-        __implicitLocks.delete(fid);
+        await getDirForFilename(dirname+"/filepart", true);
       }catch(e){
-        state.s11n.storeException(1,e);
-        rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_LOCK);
-        fh.xLock = oldLockType;
+        state.s11n.storeException(2,e);
+        rc = state.sq3Codes.SQLITE_IOERR;
+      }finally{
+        wTimeEnd();
       }
-      wTimeEnd();
-    }
-    storeAndNotify('xLock',rc);
-    mTimeEnd();
-  },
-  xOpen: async function(fid/*sqlite3_file pointer*/, filename,
-                        flags/*SQLITE_OPEN_...*/,
-                        opfsFlags/*OPFS_...*/){
-    const opName = 'xOpen';
-    mTimeStart(opName);
-    const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags);
-    wTimeStart('xOpen');
-    try{
-      let hDir, filenamePart;
-      try {
-        [hDir, filenamePart] = await getDirForFilename(filename, !!create);
+      storeAndNotify('mkdir', rc);
+      mTimeEnd();
+    },
+    xAccess: async (filename)=>{
+      mTimeStart('xAccess');
+      /* OPFS cannot support the full range of xAccess() queries sqlite3
+         calls for. We can essentially just tell if the file is
+         accessible, but if it is it's automatically writable (unless
+         it's locked, which we cannot(?) know without trying to open
+         it). OPFS does not have the notion of read-only.
+
+         The return semantics of this function differ from sqlite3's
+         xAccess semantics because we are limited in what we can
+         communicate back to our synchronous communication partner: 0 =
+         accessible, non-0 means not accessible.
+      */
+      let rc = 0;
+      wTimeStart('xAccess');
+      try{
+        const [dh, fn] = await getDirForFilename(filename);
+        await dh.getFileHandle(fn);
       }catch(e){
-        state.s11n.storeException(1,e);
-        storeAndNotify(opName, state.sq3Codes.SQLITE_NOTFOUND);
-        mTimeEnd();
+        state.s11n.storeException(2,e);
+        rc = state.sq3Codes.SQLITE_IOERR;
+      }finally{
         wTimeEnd();
-        return;
       }
-      const hFile = await hDir.getFileHandle(filenamePart, {create});
+      storeAndNotify('xAccess', rc);
+      mTimeEnd();
+    },
+    xClose: async function(fid/*sqlite3_file pointer*/){
+      const opName = 'xClose';
+      mTimeStart(opName);
+      __implicitLocks.delete(fid);
+      const fh = __openFiles[fid];
+      let rc = 0;
+      wTimeStart(opName);
+      if(fh){
+        delete __openFiles[fid];
+        await closeSyncHandle(fh);
+        if(fh.deleteOnClose){
+          try{ await fh.dirHandle.removeEntry(fh.filenamePart) }
+          catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) }
+        }
+      }else{
+        state.s11n.serialize();
+        rc = state.sq3Codes.SQLITE_NOTFOUND;
+      }
       wTimeEnd();
-      const fh = Object.assign(Object.create(null),{
-        fid: fid,
-        filenameAbs: filename,
-        filenamePart: filenamePart,
-        dirHandle: hDir,
-        fileHandle: hFile,
-        sabView: state.sabFileBufView,
-        readOnly: create
-          ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags),
-        deleteOnClose: !!(state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags)
-      });
-      fh.releaseImplicitLocks =
-        (opfsFlags & state.opfsFlags.OPFS_UNLOCK_ASAP)
-        || state.opfsFlags.defaultUnlockAsap;
-      if(0 /* this block is modelled after something wa-sqlite
-              does but it leads to immediate contention on journal files. */
-         && (0===(flags & state.sq3Codes.SQLITE_OPEN_MAIN_DB))){
-        /* sqlite does not lock these files, so go ahead and grab an OPFS
-           lock.
-
-           https://www.sqlite.org/uri.html
-        */
-        fh.xLock = "xOpen"/* Truthy value to keep entry from getting
-                             flagged as auto-locked. String value so
-                             that we can easily distinguish is later
-                             if needed. */;
-        await getSyncHandle(fh,'xOpen');
+      storeAndNotify(opName, rc);
+      mTimeEnd();
+    },
+    xDelete: async function(...args){
+      mTimeStart('xDelete');
+      const rc = await vfsAsyncImpls.xDeleteNoWait(...args);
+      storeAndNotify('xDelete', rc);
+      mTimeEnd();
+    },
+    xDeleteNoWait: async function(filename, syncDir = 0, recursive = false){
+      /* The syncDir flag is, for purposes of the VFS API's semantics,
+         ignored here. However, if it has the value 0x1234 then: after
+         deleting the given file, recursively try to delete any empty
+         directories left behind in its wake (ignoring any errors and
+         stopping at the first failure).
+
+         That said: we don't know for sure that removeEntry() fails if
+         the dir is not empty because the API is not documented. It has,
+         however, a "recursive" flag which defaults to false, so
+         presumably it will fail if the dir is not empty and that flag
+         is false.
+      */
+      let rc = 0;
+      wTimeStart('xDelete');
+      try {
+        while(filename){
+          const [hDir, filenamePart] = await getDirForFilename(filename, false);
+          if(!filenamePart) break;
+          await hDir.removeEntry(filenamePart, {recursive});
+          if(0x1234 !== syncDir) break;
+          recursive = false;
+          filename = getResolvedPath(filename, true);
+          filename.pop();
+          filename = filename.join('/');
+        }
+      }catch(e){
+        state.s11n.storeException(2,e);
+        rc = state.sq3Codes.SQLITE_IOERR_DELETE;
       }
-      __openFiles[fid] = fh;
-      storeAndNotify(opName, 0);
-    }catch(e){
       wTimeEnd();
-      error(opName,e);
-      state.s11n.storeException(1,e);
-      storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
-    }
-    mTimeEnd();
-  },
-  xRead: async function(fid/*sqlite3_file pointer*/,n,offset64){
-    mTimeStart('xRead');
-    let rc = 0, nRead;
-    const fh = __openFiles[fid];
-    try{
-      affirmLocked('xRead',fh);
-      wTimeStart('xRead');
-      nRead = (await getSyncHandle(fh,'xRead')).read(
-        fh.sabView.subarray(0, n),
-        {at: Number(offset64)}
-      );
+      return rc;
+    },
+    xFileSize: async function(fid/*sqlite3_file pointer*/){
+      mTimeStart('xFileSize');
+      const fh = __openFiles[fid];
+      let rc;
+      wTimeStart('xFileSize');
+      try{
+        affirmLocked('xFileSize',fh);
+        const sz = await (await getSyncHandle(fh,'xFileSize')).getSize();
+        state.s11n.serialize(Number(sz));
+        rc = 0;
+      }catch(e){
+        state.s11n.storeException(2,e);
+        rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR);
+      }
+      await releaseImplicitLock(fh);
       wTimeEnd();
-      if(nRead < n){/* Zero-fill remaining bytes */
-        fh.sabView.fill(0, nRead, n);
-        rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
+      storeAndNotify('xFileSize', rc);
+      mTimeEnd();
+    },
+    xLock: async function(fid/*sqlite3_file pointer*/,
+                          lockType/*SQLITE_LOCK_...*/){
+      mTimeStart('xLock');
+      const fh = __openFiles[fid];
+      let rc = 0;
+      const oldLockType = fh.xLock;
+      fh.xLock = lockType;
+      if( !fh.syncHandle ){
+        wTimeStart('xLock');
+        try {
+          await getSyncHandle(fh,'xLock');
+          __implicitLocks.delete(fid);
+        }catch(e){
+          state.s11n.storeException(1,e);
+          rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_LOCK);
+          fh.xLock = oldLockType;
+        }
+        wTimeEnd();
       }
-    }catch(e){
-      if(undefined===nRead) wTimeEnd();
-      error("xRead() failed",e,fh);
-      state.s11n.storeException(1,e);
-      rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_READ);
-    }
-    await releaseImplicitLock(fh);
-    storeAndNotify('xRead',rc);
-    mTimeEnd();
-  },
-  xSync: async function(fid/*sqlite3_file pointer*/,flags/*ignored*/){
-    mTimeStart('xSync');
-    const fh = __openFiles[fid];
-    let rc = 0;
-    if(!fh.readOnly && fh.syncHandle){
-      try {
-        wTimeStart('xSync');
-        await fh.syncHandle.flush();
+      storeAndNotify('xLock',rc);
+      mTimeEnd();
+    },
+    xOpen: async function(fid/*sqlite3_file pointer*/, filename,
+                          flags/*SQLITE_OPEN_...*/,
+                          opfsFlags/*OPFS_...*/){
+      const opName = 'xOpen';
+      mTimeStart(opName);
+      const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags);
+      wTimeStart('xOpen');
+      try{
+        let hDir, filenamePart;
+        try {
+          [hDir, filenamePart] = await getDirForFilename(filename, !!create);
+        }catch(e){
+          state.s11n.storeException(1,e);
+          storeAndNotify(opName, state.sq3Codes.SQLITE_NOTFOUND);
+          mTimeEnd();
+          wTimeEnd();
+          return;
+        }
+        const hFile = await hDir.getFileHandle(filenamePart, {create});
+        wTimeEnd();
+        const fh = Object.assign(Object.create(null),{
+          fid: fid,
+          filenameAbs: filename,
+          filenamePart: filenamePart,
+          dirHandle: hDir,
+          fileHandle: hFile,
+          sabView: state.sabFileBufView,
+          readOnly: create
+            ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags),
+          deleteOnClose: !!(state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags)
+        });
+        fh.releaseImplicitLocks =
+          (opfsFlags & state.opfsFlags.OPFS_UNLOCK_ASAP)
+          || state.opfsFlags.defaultUnlockAsap;
+        if(0 /* this block is modelled after something wa-sqlite
+                does but it leads to immediate contention on journal files. */
+           && (0===(flags & state.sq3Codes.SQLITE_OPEN_MAIN_DB))){
+          /* sqlite does not lock these files, so go ahead and grab an OPFS
+             lock.
+
+             https://www.sqlite.org/uri.html
+          */
+          fh.xLock = "xOpen"/* Truthy value to keep entry from getting
+                               flagged as auto-locked. String value so
+                               that we can easily distinguish is later
+                               if needed. */;
+          await getSyncHandle(fh,'xOpen');
+        }
+        __openFiles[fid] = fh;
+        storeAndNotify(opName, 0);
       }catch(e){
+        wTimeEnd();
+        error(opName,e);
+        state.s11n.storeException(1,e);
+        storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
+      }
+      mTimeEnd();
+    },
+    xRead: async function(fid/*sqlite3_file pointer*/,n,offset64){
+      mTimeStart('xRead');
+      let rc = 0, nRead;
+      const fh = __openFiles[fid];
+      try{
+        affirmLocked('xRead',fh);
+        wTimeStart('xRead');
+        nRead = (await getSyncHandle(fh,'xRead')).read(
+          fh.sabView.subarray(0, n),
+          {at: Number(offset64)}
+        );
+        wTimeEnd();
+        if(nRead < n){/* Zero-fill remaining bytes */
+          fh.sabView.fill(0, nRead, n);
+          rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
+        }
+      }catch(e){
+        if(undefined===nRead) wTimeEnd();
+        error("xRead() failed",e,fh);
+        state.s11n.storeException(1,e);
+        rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_READ);
+      }
+      await releaseImplicitLock(fh);
+      storeAndNotify('xRead',rc);
+      mTimeEnd();
+    },
+    xSync: async function(fid/*sqlite3_file pointer*/,flags/*ignored*/){
+      mTimeStart('xSync');
+      const fh = __openFiles[fid];
+      let rc = 0;
+      if(!fh.readOnly && fh.syncHandle){
+        try {
+          wTimeStart('xSync');
+          await fh.syncHandle.flush();
+        }catch(e){
+          state.s11n.storeException(2,e);
+          rc = state.sq3Codes.SQLITE_IOERR_FSYNC;
+        }
+        wTimeEnd();
+      }
+      storeAndNotify('xSync',rc);
+      mTimeEnd();
+    },
+    xTruncate: async function(fid/*sqlite3_file pointer*/,size){
+      mTimeStart('xTruncate');
+      let rc = 0;
+      const fh = __openFiles[fid];
+      wTimeStart('xTruncate');
+      try{
+        affirmLocked('xTruncate',fh);
+        affirmNotRO('xTruncate', fh);
+        await (await getSyncHandle(fh,'xTruncate')).truncate(size);
+      }catch(e){
+        error("xTruncate():",e,fh);
         state.s11n.storeException(2,e);
-        rc = state.sq3Codes.SQLITE_IOERR_FSYNC;
+        rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_TRUNCATE);
       }
+      await releaseImplicitLock(fh);
       wTimeEnd();
-    }
-    storeAndNotify('xSync',rc);
-    mTimeEnd();
-  },
-  xTruncate: async function(fid/*sqlite3_file pointer*/,size){
-    mTimeStart('xTruncate');
-    let rc = 0;
-    const fh = __openFiles[fid];
-    wTimeStart('xTruncate');
-    try{
-      affirmLocked('xTruncate',fh);
-      affirmNotRO('xTruncate', fh);
-      await (await getSyncHandle(fh,'xTruncate')).truncate(size);
-    }catch(e){
-      error("xTruncate():",e,fh);
-      state.s11n.storeException(2,e);
-      rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_TRUNCATE);
-    }
-    await releaseImplicitLock(fh);
-    wTimeEnd();
-    storeAndNotify('xTruncate',rc);
-    mTimeEnd();
-  },
-  xUnlock: async function(fid/*sqlite3_file pointer*/,
-                          lockType/*SQLITE_LOCK_...*/){
-    mTimeStart('xUnlock');
-    let rc = 0;
-    const fh = __openFiles[fid];
-    if( state.sq3Codes.SQLITE_LOCK_NONE===lockType
-        && fh.syncHandle ){
-      wTimeStart('xUnlock');
-      try { await closeSyncHandle(fh) }
-      catch(e){
+      storeAndNotify('xTruncate',rc);
+      mTimeEnd();
+    },
+    xUnlock: async function(fid/*sqlite3_file pointer*/,
+                            lockType/*SQLITE_LOCK_...*/){
+      mTimeStart('xUnlock');
+      let rc = 0;
+      const fh = __openFiles[fid];
+      if( state.sq3Codes.SQLITE_LOCK_NONE===lockType
+          && fh.syncHandle ){
+        wTimeStart('xUnlock');
+        try { await closeSyncHandle(fh) }
+        catch(e){
+          state.s11n.storeException(1,e);
+          rc = state.sq3Codes.SQLITE_IOERR_UNLOCK;
+        }
+        wTimeEnd();
+      }
+      storeAndNotify('xUnlock',rc);
+      mTimeEnd();
+    },
+    xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){
+      mTimeStart('xWrite');
+      let rc;
+      const fh = __openFiles[fid];
+      wTimeStart('xWrite');
+      try{
+        affirmLocked('xWrite',fh);
+        affirmNotRO('xWrite', fh);
+        rc = (
+          n === (await getSyncHandle(fh,'xWrite'))
+            .write(fh.sabView.subarray(0, n),
+                   {at: Number(offset64)})
+        ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
+      }catch(e){
+        error("xWrite():",e,fh);
         state.s11n.storeException(1,e);
-        rc = state.sq3Codes.SQLITE_IOERR_UNLOCK;
+        rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_WRITE);
       }
+      await releaseImplicitLock(fh);
       wTimeEnd();
+      storeAndNotify('xWrite',rc);
+      mTimeEnd();
     }
-    storeAndNotify('xUnlock',rc);
-    mTimeEnd();
-  },
-  xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){
-    mTimeStart('xWrite');
-    let rc;
-    const fh = __openFiles[fid];
-    wTimeStart('xWrite');
-    try{
-      affirmLocked('xWrite',fh);
-      affirmNotRO('xWrite', fh);
-      rc = (
-        n === (await getSyncHandle(fh,'xWrite'))
-          .write(fh.sabView.subarray(0, n),
-                 {at: Number(offset64)})
-      ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
-    }catch(e){
-      error("xWrite():",e,fh);
-      state.s11n.storeException(1,e);
-      rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_WRITE);
-    }
-    await releaseImplicitLock(fh);
-    wTimeEnd();
-    storeAndNotify('xWrite',rc);
-    mTimeEnd();
-  }
-}/*vfsAsyncImpls*/;
+  }/*vfsAsyncImpls*/;
 
-const initS11n = ()=>{
-  /**
-     ACHTUNG: this code is 100% duplicated in the other half of this
-     proxy! The documentation is maintained in the "synchronous half".
-  */
-  if(state.s11n) return state.s11n;
-  const textDecoder = new TextDecoder(),
-  textEncoder = new TextEncoder('utf-8'),
-  viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize),
-  viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
-  state.s11n = Object.create(null);
-  const TypeIds = Object.create(null);
-  TypeIds.number  = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' };
-  TypeIds.bigint  = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' };
-  TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' };
-  TypeIds.string =  { id: 4 };
-  const getTypeId = (v)=>(
-    TypeIds[typeof v]
-      || toss("Maintenance required: this value type cannot be serialized.",v)
-  );
-  const getTypeIdById = (tid)=>{
-    switch(tid){
-      case TypeIds.number.id: return TypeIds.number;
-      case TypeIds.bigint.id: return TypeIds.bigint;
-      case TypeIds.boolean.id: return TypeIds.boolean;
-      case TypeIds.string.id: return TypeIds.string;
-      default: toss("Invalid type ID:",tid);
-    }
-  };
-  state.s11n.deserialize = function(clear=false){
-    ++metrics.s11n.deserialize.count;
-    const t = performance.now();
-    const argc = viewU8[0];
-    const rc = argc ? [] : null;
-    if(argc){
-      const typeIds = [];
-      let offset = 1, i, n, v;
-      for(i = 0; i < argc; ++i, ++offset){
-        typeIds.push(getTypeIdById(viewU8[offset]));
+  const initS11n = ()=>{
+    /**
+       ACHTUNG: this code is 100% duplicated in the other half of this
+       proxy! The documentation is maintained in the "synchronous half".
+    */
+    if(state.s11n) return state.s11n;
+    const textDecoder = new TextDecoder(),
+          textEncoder = new TextEncoder('utf-8'),
+          viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize),
+          viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
+    state.s11n = Object.create(null);
+    const TypeIds = Object.create(null);
+    TypeIds.number  = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' };
+    TypeIds.bigint  = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' };
+    TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' };
+    TypeIds.string =  { id: 4 };
+    const getTypeId = (v)=>(
+      TypeIds[typeof v]
+        || toss("Maintenance required: this value type cannot be serialized.",v)
+    );
+    const getTypeIdById = (tid)=>{
+      switch(tid){
+          case TypeIds.number.id: return TypeIds.number;
+          case TypeIds.bigint.id: return TypeIds.bigint;
+          case TypeIds.boolean.id: return TypeIds.boolean;
+          case TypeIds.string.id: return TypeIds.string;
+          default: toss("Invalid type ID:",tid);
       }
-      for(i = 0; i < argc; ++i){
-        const t = typeIds[i];
-        if(t.getter){
-          v = viewDV[t.getter](offset, state.littleEndian);
-          offset += t.size;
-        }else{/*String*/
-          n = viewDV.getInt32(offset, state.littleEndian);
-          offset += 4;
-          v = textDecoder.decode(viewU8.slice(offset, offset+n));
-          offset += n;
+    };
+    state.s11n.deserialize = function(clear=false){
+      ++metrics.s11n.deserialize.count;
+      const t = performance.now();
+      const argc = viewU8[0];
+      const rc = argc ? [] : null;
+      if(argc){
+        const typeIds = [];
+        let offset = 1, i, n, v;
+        for(i = 0; i < argc; ++i, ++offset){
+          typeIds.push(getTypeIdById(viewU8[offset]));
+        }
+        for(i = 0; i < argc; ++i){
+          const t = typeIds[i];
+          if(t.getter){
+            v = viewDV[t.getter](offset, state.littleEndian);
+            offset += t.size;
+          }else{/*String*/
+            n = viewDV.getInt32(offset, state.littleEndian);
+            offset += 4;
+            v = textDecoder.decode(viewU8.slice(offset, offset+n));
+            offset += n;
+          }
+          rc.push(v);
         }
-        rc.push(v);
       }
-    }
-    if(clear) viewU8[0] = 0;
-    //log("deserialize:",argc, rc);
-    metrics.s11n.deserialize.time += performance.now() - t;
-    return rc;
-  };
-  state.s11n.serialize = function(...args){
-    const t = performance.now();
-    ++metrics.s11n.serialize.count;
-    if(args.length){
-      //log("serialize():",args);
-      const typeIds = [];
-      let i = 0, offset = 1;
-      viewU8[0] = args.length & 0xff /* header = # of args */;
-      for(; i < args.length; ++i, ++offset){
-        /* Write the TypeIds.id value into the next args.length
-           bytes. */
-        typeIds.push(getTypeId(args[i]));
-        viewU8[offset] = typeIds[i].id;
+      if(clear) viewU8[0] = 0;
+      //log("deserialize:",argc, rc);
+      metrics.s11n.deserialize.time += performance.now() - t;
+      return rc;
+    };
+    state.s11n.serialize = function(...args){
+      const t = performance.now();
+      ++metrics.s11n.serialize.count;
+      if(args.length){
+        //log("serialize():",args);
+        const typeIds = [];
+        let i = 0, offset = 1;
+        viewU8[0] = args.length & 0xff /* header = # of args */;
+        for(; i < args.length; ++i, ++offset){
+          /* Write the TypeIds.id value into the next args.length
+             bytes. */
+          typeIds.push(getTypeId(args[i]));
+          viewU8[offset] = typeIds[i].id;
+        }
+        for(i = 0; i < args.length; ++i) {
+          /* Deserialize the following bytes based on their
+             corresponding TypeIds.id from the header. */
+          const t = typeIds[i];
+          if(t.setter){
+            viewDV[t.setter](offset, args[i], state.littleEndian);
+            offset += t.size;
+          }else{/*String*/
+            const s = textEncoder.encode(args[i]);
+            viewDV.setInt32(offset, s.byteLength, state.littleEndian);
+            offset += 4;
+            viewU8.set(s, offset);
+            offset += s.byteLength;
+          }
+        }
+        //log("serialize() result:",viewU8.slice(0,offset));
+      }else{
+        viewU8[0] = 0;
       }
-      for(i = 0; i < args.length; ++i) {
-        /* Deserialize the following bytes based on their
-           corresponding TypeIds.id from the header. */
-        const t = typeIds[i];
-        if(t.setter){
-          viewDV[t.setter](offset, args[i], state.littleEndian);
-          offset += t.size;
-        }else{/*String*/
-          const s = textEncoder.encode(args[i]);
-          viewDV.setInt32(offset, s.byteLength, state.littleEndian);
-          offset += 4;
-          viewU8.set(s, offset);
-          offset += s.byteLength;
+      metrics.s11n.serialize.time += performance.now() - t;
+    };
+
+    state.s11n.storeException = state.asyncS11nExceptions
+      ? ((priority,e)=>{
+        if(priority<=state.asyncS11nExceptions){
+          state.s11n.serialize([e.name,': ',e.message].join(""));
         }
+      })
+      : ()=>{};
+
+    return state.s11n;
+  }/*initS11n()*/;
+
+  const waitLoop = async function f(){
+    const opHandlers = Object.create(null);
+    for(let k of Object.keys(state.opIds)){
+      const vi = vfsAsyncImpls[k];
+      if(!vi) continue;
+      const o = Object.create(null);
+      opHandlers[state.opIds[k]] = o;
+      o.key = k;
+      o.f = vi;
+    }
+    /**
+       waitTime is how long (ms) to wait for each Atomics.wait().
+       We need to wake up periodically to give the thread a chance
+       to do other things. If this is too high (e.g. 500ms) then
+       even two workers/tabs can easily run into locking errors.
+    */
+    const waitTime = 100;
+    while(!flagAsyncShutdown){
+      try {
+        if('timed-out'===Atomics.wait(
+          state.sabOPView, state.opIds.whichOp, 0, waitTime
+        )){
+          await releaseImplicitLocks();
+          continue;
+        }
+        const opId = Atomics.load(state.sabOPView, state.opIds.whichOp);
+        Atomics.store(state.sabOPView, state.opIds.whichOp, 0);
+        const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId);
+        const args = state.s11n.deserialize(
+          true /* clear s11n to keep the caller from confusing this with
+                  an exception string written by the upcoming
+                  operation */
+        ) || [];
+        //warn("waitLoop() whichOp =",opId, hnd, args);
+        if(hnd.f) await hnd.f(...args);
+        else error("Missing callback for opId",opId);
+      }catch(e){
+        error('in waitLoop():',e);
       }
-      //log("serialize() result:",viewU8.slice(0,offset));
-    }else{
-      viewU8[0] = 0;
     }
-    metrics.s11n.serialize.time += performance.now() - t;
   };
 
-  state.s11n.storeException = state.asyncS11nExceptions
-    ? ((priority,e)=>{
-      if(priority<=state.asyncS11nExceptions){
-        state.s11n.serialize([e.name,': ',e.message].join(""));
-      }
-    })
-    : ()=>{};
-
-  return state.s11n;
-}/*initS11n()*/;
-
-const waitLoop = async function f(){
-  const opHandlers = Object.create(null);
-  for(let k of Object.keys(state.opIds)){
-    const vi = vfsAsyncImpls[k];
-    if(!vi) continue;
-    const o = Object.create(null);
-    opHandlers[state.opIds[k]] = o;
-    o.key = k;
-    o.f = vi;
-  }
-  /**
-     waitTime is how long (ms) to wait for each Atomics.wait().
-     We need to wake up periodically to give the thread a chance
-     to do other things. If this is too high (e.g. 500ms) then
-     even two workers/tabs can easily run into locking errors.
-  */
-  const waitTime = 100;
-  while(!flagAsyncShutdown){
-    try {
-      if('timed-out'===Atomics.wait(
-        state.sabOPView, state.opIds.whichOp, 0, waitTime
-      )){
-        await releaseImplicitLocks();
-        continue;
-      }
-      const opId = Atomics.load(state.sabOPView, state.opIds.whichOp);
-      Atomics.store(state.sabOPView, state.opIds.whichOp, 0);
-      const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId);
-      const args = state.s11n.deserialize(
-        true /* clear s11n to keep the caller from confusing this with
-                an exception string written by the upcoming
-                operation */
-      ) || [];
-      //warn("waitLoop() whichOp =",opId, hnd, args);
-      if(hnd.f) await hnd.f(...args);
-      else error("Missing callback for opId",opId);
-    }catch(e){
-      error('in waitLoop():',e);
-    }
-  }
-};
-
-navigator.storage.getDirectory().then(function(d){
-  const wMsg = (type)=>postMessage({type});
-  state.rootDir = d;
-  self.onmessage = function({data}){
-    switch(data.type){
-        case 'opfs-async-init':{
-          /* Receive shared state from synchronous partner */
-          const opt = data.args;
-          state.littleEndian = opt.littleEndian;
-          state.asyncS11nExceptions = opt.asyncS11nExceptions;
-          state.verbose = opt.verbose ?? 1;
-          state.fileBufferSize = opt.fileBufferSize;
-          state.sabS11nOffset = opt.sabS11nOffset;
-          state.sabS11nSize = opt.sabS11nSize;
-          state.sabOP = opt.sabOP;
-          state.sabOPView = new Int32Array(state.sabOP);
-          state.sabIO = opt.sabIO;
-          state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
-          state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
-          state.opIds = opt.opIds;
-          state.sq3Codes = opt.sq3Codes;
-          state.opfsFlags = opt.opfsFlags;
-          Object.keys(vfsAsyncImpls).forEach((k)=>{
-            if(!Number.isFinite(state.opIds[k])){
-              toss("Maintenance required: missing state.opIds[",k,"]");
-            }
-          });
-          initS11n();
-          metrics.reset();
-          log("init state",state);
-          wMsg('opfs-async-inited');
-          waitLoop();
-          break;
-        }
-        case 'opfs-async-restart':
-          if(flagAsyncShutdown){
-            warn("Restarting after opfs-async-shutdown. Might or might not work.");
-            flagAsyncShutdown = false;
+  navigator.storage.getDirectory().then(function(d){
+    state.rootDir = d;
+    self.onmessage = function({data}){
+      switch(data.type){
+          case 'opfs-async-init':{
+            /* Receive shared state from synchronous partner */
+            const opt = data.args;
+            state.littleEndian = opt.littleEndian;
+            state.asyncS11nExceptions = opt.asyncS11nExceptions;
+            state.verbose = opt.verbose ?? 1;
+            state.fileBufferSize = opt.fileBufferSize;
+            state.sabS11nOffset = opt.sabS11nOffset;
+            state.sabS11nSize = opt.sabS11nSize;
+            state.sabOP = opt.sabOP;
+            state.sabOPView = new Int32Array(state.sabOP);
+            state.sabIO = opt.sabIO;
+            state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
+            state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
+            state.opIds = opt.opIds;
+            state.sq3Codes = opt.sq3Codes;
+            state.opfsFlags = opt.opfsFlags;
+            Object.keys(vfsAsyncImpls).forEach((k)=>{
+              if(!Number.isFinite(state.opIds[k])){
+                toss("Maintenance required: missing state.opIds[",k,"]");
+              }
+            });
+            initS11n();
+            metrics.reset();
+            log("init state",state);
+            wPost('opfs-async-inited');
             waitLoop();
+            break;
           }
-          break;
-        case 'opfs-async-metrics':
-          metrics.dump();
-          break;
-    }
-  };
-  wMsg('opfs-async-loaded');
-}).catch((e)=>error("error initializing OPFS asyncer:",e));
+          case 'opfs-async-restart':
+            if(flagAsyncShutdown){
+              warn("Restarting after opfs-async-shutdown. Might or might not work.");
+              flagAsyncShutdown = false;
+              waitLoop();
+            }
+            break;
+          case 'opfs-async-metrics':
+            metrics.dump();
+            break;
+      }
+    };
+    wPost('opfs-async-loaded');
+  }).catch((e)=>error("error initializing OPFS asyncer:",e));
+}/*installAsyncProxy()*/;
+if(!self.SharedArrayBuffer){
+  wPost('opfs-unavailable', "Missing SharedArrayBuffer API.",
+        "The server must emit the COOP/COEP response headers to enable that.");
+}else if(!self.Atomics){
+  wPost('opfs-unavailable', "Missing Atomics API.",
+        "The server must emit the COOP/COEP response headers to enable that.");
+}else if(!self.FileSystemHandle ||
+         !self.FileSystemDirectoryHandle ||
+         !self.FileSystemFileHandle ||
+         !self.FileSystemFileHandle.prototype.createSyncAccessHandle ||
+         !navigator.storage.getDirectory){
+  wPost('opfs-unavailable',"Missing required OPFS APIs.");
+}else{
+  installAsyncProxy(self);
+}
index 5bb5959396f42ef57dbb752c1df0ba9b96a80b8e..2fc6752694fd37c881959abade164864a463aa33 100644 (file)
@@ -1784,13 +1784,12 @@ self.sqlite3InitModule = sqlite3InitModule;
     .t({
       name: 'OPFS sanity checks',
       test: async function(sqlite3){
-        const opfs = sqlite3.opfs;
         const filename = 'sqlite3-tester1.db';
         const pVfs = capi.sqlite3_vfs_find('opfs');
         T.assert(pVfs);
         const unlink = (fn=filename)=>wasm.sqlite3_wasm_vfs_unlink(pVfs,fn);
         unlink();
-        let db = new opfs.OpfsDb(filename);
+        let db = new sqlite3.oo1.OpfsDb(filename);
         try {
           db.exec([
             'create table p(a);',
@@ -1798,7 +1797,7 @@ self.sqlite3InitModule = sqlite3InitModule;
           ]);
           T.assert(3 === db.selectValue('select count(*) from p'));
           db.close();
-          db = new opfs.OpfsDb(filename);
+          db = new sqlite3.oo1.OpfsDb(filename);
           db.exec('insert into p(a) values(4),(5),(6)');
           T.assert(6 === db.selectValue('select count(*) from p'));
         }finally{
@@ -1806,8 +1805,9 @@ self.sqlite3InitModule = sqlite3InitModule;
           unlink();
         }
 
-        if(1){
+        if(sqlite3.opfs){
           // Sanity-test sqlite3_wasm_vfs_create_file()...
+          const opfs = sqlite3.opfs;
           const fSize = 1379;
           let sh;
           try{
@@ -1824,20 +1824,20 @@ self.sqlite3InitModule = sqlite3InitModule;
             if(sh) await sh.close();
             unlink();
           }
-        }
 
-        // Some sanity checks of the opfs utility functions...
-        const testDir = '/sqlite3-opfs-'+opfs.randomFilename(12);
-        const aDir = testDir+'/test/dir';
-        T.assert(await opfs.mkdir(aDir), "mkdir failed")
-          .assert(await opfs.mkdir(aDir), "mkdir must pass if the dir exists")
-          .assert(!(await opfs.unlink(testDir+'/test')), "delete 1 should have failed (dir not empty)")
-          .assert((await opfs.unlink(testDir+'/test/dir')), "delete 2 failed")
-          .assert(!(await opfs.unlink(testDir+'/test/dir')),
-                  "delete 2b should have failed (dir already deleted)")
-          .assert((await opfs.unlink(testDir, true)), "delete 3 failed")
-          .assert(!(await opfs.entryExists(testDir)),
-                  "entryExists(",testDir,") should have failed");
+          // Some sanity checks of the opfs utility functions...
+          const testDir = '/sqlite3-opfs-'+opfs.randomFilename(12);
+          const aDir = testDir+'/test/dir';
+          T.assert(await opfs.mkdir(aDir), "mkdir failed")
+            .assert(await opfs.mkdir(aDir), "mkdir must pass if the dir exists")
+            .assert(!(await opfs.unlink(testDir+'/test')), "delete 1 should have failed (dir not empty)")
+            .assert((await opfs.unlink(testDir+'/test/dir')), "delete 2 failed")
+            .assert(!(await opfs.unlink(testDir+'/test/dir')),
+                    "delete 2b should have failed (dir already deleted)")
+            .assert((await opfs.unlink(testDir, true)), "delete 3 failed")
+            .assert(!(await opfs.entryExists(testDir)),
+                    "entryExists(",testDir,") should have failed");
+        }
       }
     }/*OPFS sanity checks*/)
   ;/* end OPFS tests */
index e19f6a8da68b515942c4e25eee9ed5641a293b6d..d5aa51dc936d4dc9f8f41e0aacf9578b97ec4d29 100644 (file)
       with <code>unlock-asap=0-1</code>.
     </p>
     <p>Achtung: if it does not start to do anything within a couple of
-      seconds, check the dev console: Chrome often fails with "cannot allocate
-      WasmMemory" at startup. Closing and re-opening the tab usually resolves
-      it.
+      seconds, check the dev console: Chrome sometimes fails to load
+      the wasm module due to "cannot allocate WasmMemory." Closing and
+      re-opening the tab usually resolves it, but sometimes restarting
+      the browser is required.
     </p>
     <div class='input-wrapper'>
       <input type='checkbox' id='cb-log-reverse'>
index 01aaf2a316a1b81167a619d8f49f251ff11b1030..4dc42b8b8c5068d5c55488bcd8b10aca0ad79806 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Add\san\sexplicit\swarning\sabout\sthe\scurrent\sAPI-instability\sof\sthe\ssqlite3.opfs\snamespace,\swhich\smay\sneed\sto\sbe\seliminated\sbased\son\sre-thinking\sof\show\sthe\sOPFS\ssqlite3_vfs\sis\sregistered.\sComment\schanges\sonly\s-\sno\scode.
-D 2022-11-29T02:23:12.943
+C Internal\srestructuring\sof\sthe\sOPFS\ssqlite3_vfs\sin\sorder\sto\sfacilitate\scertain\sexperimentation\sand\simprove\serror\sreporting/hints\sif\sit\scannot\sbe\sactivated.\sDeprecate\sthe\sname\ssqlite3.opfs.OpfsDb,\spreferring\ssqlite3.oo1.OpfsDb\sfor\sconsistency\swith\sJsStorageDb\sand\sany\sfuture\sDB\ssubclasses.
+D 2022-11-29T05:25:08.036
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
 F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
@@ -505,11 +505,11 @@ F ext/wasm/api/pre-js.js b88499dc303c21fc3f55f2c364a0f814f587b60a95784303881169f
 F ext/wasm/api/sqlite3-api-cleanup.js ecdc69dbfccfe26146f04799fcfd4a6f5790d46e7e3b9b6e9b0491f92ed8ae34
 F ext/wasm/api/sqlite3-api-glue.js 056f44b82c126358a0175e08a892d56fadfce177b0d7a0012502a6acf67ea6d5
 F ext/wasm/api/sqlite3-api-oo1.js 06ad2079368e16cb9f182c18cd37bdc3932536856dff4f60582d0ca5f6c491a8
-F ext/wasm/api/sqlite3-api-opfs.js 3cdae7e98c500f89f9468a260e2a0e1b528c845a107bf72d368e5222769214d3
+F ext/wasm/api/sqlite3-api-opfs.js 583650ffdc1452496df6b9459d018fa2aede221ae6ea0cbbbe83bd2e1bdba966
 F ext/wasm/api/sqlite3-api-prologue.js 7fce4c6a138ec3d7c285b7c125cee809e6b668d2cb0d2328a1b790b7037765bd
 F ext/wasm/api/sqlite3-api-worker1.js e94ba98e44afccfa482874cd9acb325883ade50ed1f9f9526beb9de1711f182f
 F ext/wasm/api/sqlite3-license-version-header.js a661182fc93fc2cf212dfd0b987f8e138a3ac98f850b1112e29b5fbdaecc87c3
-F ext/wasm/api/sqlite3-opfs-async-proxy.js 798383f6b46fd5dac122d6e35962d25b10401ddb825b5c66df1d21e6b1d8aacc
+F ext/wasm/api/sqlite3-opfs-async-proxy.js b5dd7eda8e74e07453457925a0dd793d7785da720954e0e37e847c5c6e4d9526
 F ext/wasm/api/sqlite3-wasi.h 25356084cfe0d40458a902afb465df8c21fc4152c1d0a59b563a3fba59a068f9
 F ext/wasm/api/sqlite3-wasm.c 8b32787a3b6bb2990cbaba2304bd5b75a9652acbc8d29909b3279019b6cbaef5
 F ext/wasm/api/sqlite3-worker1-promiser.js 0c7a9826dbf82a5ed4e4f7bf7816e825a52aff253afbf3350431f5773faf0e4b
@@ -554,8 +554,8 @@ F ext/wasm/test-opfs-vfs.html 1f2d672f3f3fce810dfd48a8d56914aba22e45c6834e262555
 F ext/wasm/test-opfs-vfs.js 44363db07b2a20e73b0eb1808de4400ca71b703af718d0fa6d962f15e73bf2ac
 F ext/wasm/tester1-worker.html 5ef353348c37cf2e4fd0b23da562d3275523e036260b510734e9a3239ba8c987
 F ext/wasm/tester1.c-pp.html 74aa9b31c75f12490653f814b53c3dd39f40cd3f70d6a53a716f4e8587107399
-F ext/wasm/tester1.c-pp.js 3b91f192c159088004fba6fe3441edea58421a8b88bccf3dd20978a077648d19
-F ext/wasm/tests/opfs/concurrency/index.html e8fec75ea6eddc600c8a382da7ea2579feece2263a2fb4417f2cf3e9d451744c
+F ext/wasm/tester1.c-pp.js a4b6a165aafcd3b86118efaec6b47c70fbb6c64b5ab86d21ca8c250d42617dfa
+F ext/wasm/tests/opfs/concurrency/index.html 2b1cda51d6c786102875a28eba22f0da3eecb732a5e677b0d1ecdb53546d1a62
 F ext/wasm/tests/opfs/concurrency/test.js bfc3d7e27b207f0827f12568986b8d516a744529550b449314f5c21c9e9faf4a
 F ext/wasm/tests/opfs/concurrency/worker.js 0eff027cbd3a495acb2ac94f57ca9e4d21125ab9fda07d45f3701b0efe82d450
 F ext/wasm/version-info.c 3b36468a90faf1bbd59c65fd0eb66522d9f941eedd364fabccd72273503ae7d5
@@ -2064,8 +2064,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
 F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
 F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
-P 46cdd3637d6a206ad2bcf8653cc6f2c7a886a16cc7685c45967938609941a755
-R 3870d04bfd54da096e986662fe29b1c8
+P 0cb2fd14179397051a25d066256a553fc198656d5668c7010c016f2b8f495bf4
+R 115b7898d7b2ce79a9261f36a9b959d1
 U stephan
-Z ad4a8c5f45a34a10f588c0c6dc455846
+Z f4ff31d5e2499971cf67cb62dbdd0ac3
 # Remove this line to create a well-formed Fossil manifest.
index 361de4178ee3f05e1da89bd4461a5cd9d71535de..fb6fdac183f9816ef8aba1a118f9c0f51d9aa928 100644 (file)
@@ -1 +1 @@
-0cb2fd14179397051a25d066256a553fc198656d5668c7010c016f2b8f495bf4
\ No newline at end of file
+0c5c51f4fb04a4b90c50ec9704cfea9a3fb7d7d0ee55c1b0d4476129188217a6
\ No newline at end of file