1/*
2  2022-09-18
3
4  The author disclaims copyright to this source code.  In place of a
5  legal notice, here is a blessing:
6
7  *   May you do good and not evil.
8  *   May you find forgiveness for yourself and forgive others.
9  *   May you share freely, never taking more than you give.
10
11  ***********************************************************************
12
13  This file holds the synchronous half of an sqlite3_vfs
14  implementation which proxies, in a synchronous fashion, the
15  asynchronous Origin-Private FileSystem (OPFS) APIs using a second
16  Worker, implemented in sqlite3-opfs-async-proxy.js.  This file is
17  intended to be appended to the main sqlite3 JS deliverable somewhere
18  after sqlite3-api-glue.js and before sqlite3-api-cleanup.js.
19
20*/
21
22'use strict';
23self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
24/**
25   sqlite3.installOpfsVfs() returns a Promise which, on success, installs
26   an sqlite3_vfs named "opfs", suitable for use with all sqlite3 APIs
27   which accept a VFS. It uses the Origin-Private FileSystem API for
28   all file storage. On error it is rejected with an exception
29   explaining the problem. Reasons for rejection include, but are
30   not limited to:
31
32   - The counterpart Worker (see below) could not be loaded.
33
34   - The environment does not support OPFS. That includes when
35     this function is called from the main window thread.
36
37
38  Significant notes and limitations:
39
40  - As of this writing, OPFS is still very much in flux and only
41    available in bleeding-edge versions of Chrome (v102+, noting that
42    that number will increase as the OPFS API matures).
43
44  - The OPFS features used here are only available in dedicated Worker
45    threads. This file tries to detect that case, resulting in a
46    rejected Promise if those features do not seem to be available.
47
48  - It requires the SharedArrayBuffer and Atomics classes, and the
49    former is only available if the HTTP server emits the so-called
50    COOP and COEP response headers. These features are required for
51    proxying OPFS's synchronous API via the synchronous interface
52    required by the sqlite3_vfs API.
53
54  - This function may only be called a single time and it must be
55    called from the client, as opposed to the library initialization,
56    in case the client requires a custom path for this API's
57    "counterpart": this function's argument is the relative URI to
58    this module's "asynchronous half". When called, this function removes
59    itself from the sqlite3 object.
60
61   The argument may optionally be a plain object with the following
62   configuration options:
63
64   - proxyUri: as described above
65
66   - verbose (=2): an integer 0-3. 0 disables all logging, 1 enables
67     logging of errors. 2 enables logging of warnings and errors. 3
68     additionally enables debugging info.
69
70   - sanityChecks (=false): if true, some basic sanity tests are
71     run on the OPFS VFS API after it's initialized, before the
72     returned Promise resolves.
73
74   On success, the Promise resolves to the top-most sqlite3 namespace
75   object and that object gets a new object installed in its
76   `opfs` property, containing several OPFS-specific utilities.
77*/
78sqlite3.installOpfsVfs = function callee(asyncProxyUri = callee.defaultProxyUri){
79  delete sqlite3.installOpfsVfs;
80  if(!self.SharedArrayBuffer ||
81     !self.FileSystemHandle ||
82     !self.FileSystemDirectoryHandle ||
83     !self.FileSystemFileHandle ||
84     !self.FileSystemFileHandle.prototype.createSyncAccessHandle ||
85     !navigator.storage.getDirectory){
86    return Promise.reject(
87      new Error("This environment does not have OPFS support.")
88    );
89  }
90  const options = (asyncProxyUri && 'object'===asyncProxyUri) ? asyncProxyUri : {
91    proxyUri: asyncProxyUri
92  };
93  const urlParams = new URL(self.location.href).searchParams;
94  if(undefined===options.verbose){
95    options.verbose = urlParams.has('opfs-verbose') ? 3 : 2;
96  }
97  if(undefined===options.sanityChecks){
98    options.sanityChecks = urlParams.has('opfs-sanity-check');
99  }
100  if(undefined===options.proxyUri){
101    options.proxyUri = callee.defaultProxyUri;
102  }
103
104  const thePromise = new Promise(function(promiseResolve, promiseReject){
105    const loggers = {
106      0:console.error.bind(console),
107      1:console.warn.bind(console),
108      2:console.log.bind(console)
109    };
110    const logImpl = (level,...args)=>{
111      if(options.verbose>level) loggers[level]("OPFS syncer:",...args);
112    };
113    const log =    (...args)=>logImpl(2, ...args);
114    const warn =   (...args)=>logImpl(1, ...args);
115    const error =  (...args)=>logImpl(0, ...args);
116    warn("The OPFS VFS feature is very much experimental and under construction.");
117    const toss = function(...args){throw new Error(args.join(' '))};
118    const capi = sqlite3.capi;
119    const wasm = capi.wasm;
120    const sqlite3_vfs = capi.sqlite3_vfs;
121    const sqlite3_file = capi.sqlite3_file;
122    const sqlite3_io_methods = capi.sqlite3_io_methods;
123    const W = new Worker(options.proxyUri);
124    W._originalOnError = W.onerror /* will be restored later */;
125    W.onerror = function(err){
126      // The error object doesn't contain any useful info when the
127      // failure is, e.g., that the remote script is 404.
128      promiseReject(new Error("Loading OPFS async Worker failed for unknown reasons."));
129    };
130    /**
131       Generic utilities for working with OPFS. This will get filled out
132       by the Promise setup and, on success, installed as sqlite3.opfs.
133    */
134    const opfsUtil = Object.create(null);
135    /**
136       Not part of the public API. Solely for internal/development
137       use.
138    */
139    opfsUtil.metrics = {
140      dump: function(){
141        let k, n = 0, t = 0, w = 0;
142        for(k in state.opIds){
143          const m = metrics[k];
144          n += m.count;
145          t += m.time;
146          w += m.wait;
147          m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
148          m.avgWait = (m.count && m.wait) ? (m.wait / m.count) : 0;
149        }
150        console.log(self.location.href,
151                    "metrics for",self.location.href,":",metrics,
152                    "\nTotal of",n,"op(s) for",t,
153                    "ms (incl. "+w+" ms of waiting on the async side)");
154        console.log("Serialization metrics:",JSON.stringify(metrics.s11n,0,2));
155      },
156      reset: function(){
157        let k;
158        const r = (m)=>(m.count = m.time = m.wait = 0);
159        for(k in state.opIds){
160          r(metrics[k] = Object.create(null));
161        }
162        let s = metrics.s11n = Object.create(null);
163        s = s.serialize = Object.create(null);
164        s.count = s.time = 0;
165        s = metrics.s11n.deserialize = Object.create(null);
166        s.count = s.time = 0;
167        //[ // timed routines which are not in state.opIds
168        //  'xFileControl'
169        //].forEach((k)=>r(metrics[k] = Object.create(null)));
170      }
171    }/*metrics*/;
172
173    const pDVfs = capi.sqlite3_vfs_find(null)/*pointer to default VFS*/;
174    const dVfs = pDVfs
175          ? new sqlite3_vfs(pDVfs)
176          : null /* dVfs will be null when sqlite3 is built with
177                    SQLITE_OS_OTHER. Though we cannot currently handle
178                    that case, the hope is to eventually be able to. */;
179    const opfsVfs = new sqlite3_vfs();
180    const opfsIoMethods = new sqlite3_io_methods();
181    opfsVfs.$iVersion = 2/*yes, two*/;
182    opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof;
183    opfsVfs.$mxPathname = 1024/*sure, why not?*/;
184    opfsVfs.$zName = wasm.allocCString("opfs");
185    // All C-side memory of opfsVfs is zeroed out, but just to be explicit:
186    opfsVfs.$xDlOpen = opfsVfs.$xDlError = opfsVfs.$xDlSym = opfsVfs.$xDlClose = null;
187    opfsVfs.ondispose = [
188      '$zName', opfsVfs.$zName,
189      'cleanup default VFS wrapper', ()=>(dVfs ? dVfs.dispose() : null),
190      'cleanup opfsIoMethods', ()=>opfsIoMethods.dispose()
191    ];
192    /**
193       Pedantic sidebar about opfsVfs.ondispose: the entries in that array
194       are items to clean up when opfsVfs.dispose() is called, but in this
195       environment it will never be called. The VFS instance simply
196       hangs around until the WASM module instance is cleaned up. We
197       "could" _hypothetically_ clean it up by "importing" an
198       sqlite3_os_end() impl into the wasm build, but the shutdown order
199       of the wasm engine and the JS one are undefined so there is no
200       guaranty that the opfsVfs instance would be available in one
201       environment or the other when sqlite3_os_end() is called (_if_ it
202       gets called at all in a wasm build, which is undefined).
203    */
204
205    /**
206       State which we send to the async-api Worker or share with it.
207       This object must initially contain only cloneable or sharable
208       objects. After the worker's "inited" message arrives, other types
209       of data may be added to it.
210
211       For purposes of Atomics.wait() and Atomics.notify(), we use a
212       SharedArrayBuffer with one slot reserved for each of the API
213       proxy's methods. The sync side of the API uses Atomics.wait()
214       on the corresponding slot and the async side uses
215       Atomics.notify() on that slot.
216
217       The approach of using a single SAB to serialize comms for all
218       instances might(?) lead to deadlock situations in multi-db
219       cases. We should probably have one SAB here with a single slot
220       for locking a per-file initialization step and then allocate a
221       separate SAB like the above one for each file. That will
222       require a bit of acrobatics but should be feasible.
223    */
224    const state = Object.create(null);
225    state.littleEndian = true;
226    state.verbose = options.verbose;
227    /* Size of file I/O buffer block. 64k = max sqlite3 page size. */
228    state.fileBufferSize =
229      1024 * 64;
230    state.sabS11nOffset = state.fileBufferSize;
231    /**
232       The size of the block in our SAB for serializing arguments and
233       result values. Need to be large enough to hold serialized
234       values of any of the proxied APIs. Filenames are the largest
235       part but are limited to opfsVfs.$mxPathname bytes.
236    */
237    state.sabS11nSize = opfsVfs.$mxPathname * 2;
238    /**
239       The SAB used for all data I/O (files and arg/result s11n).
240    */
241    state.sabIO = new SharedArrayBuffer(
242      state.fileBufferSize/* file i/o block */
243      + state.sabS11nSize/* argument/result serialization block */
244    );
245    state.opIds = Object.create(null);
246    const metrics = Object.create(null);
247    {
248      /* Indexes for use in our SharedArrayBuffer... */
249      let i = 0;
250      /* SAB slot used to communicate which operation is desired
251         between both workers. This worker writes to it and the other
252         listens for changes. */
253      state.opIds.whichOp = i++;
254      /* Slot for storing return values. This work listens to that
255         slot and the other worker writes to it. */
256      state.opIds.rc = i++;
257      /* Each function gets an ID which this worker writes to
258         the whichOp slot. The async-api worker uses Atomic.wait()
259         on the whichOp slot to figure out which operation to run
260         next. */
261      state.opIds.xAccess = i++;
262      state.opIds.xClose = i++;
263      state.opIds.xDelete = i++;
264      state.opIds.xDeleteNoWait = i++;
265      state.opIds.xFileSize = i++;
266      state.opIds.xOpen = i++;
267      state.opIds.xRead = i++;
268      state.opIds.xSleep = i++;
269      state.opIds.xSync = i++;
270      state.opIds.xTruncate = i++;
271      state.opIds.xWrite = i++;
272      state.opIds.mkdir = i++;
273      state.opIds.xFileControl = i++;
274      state.sabOP = new SharedArrayBuffer(i * 4/*sizeof int32*/);
275      opfsUtil.metrics.reset();
276    }
277
278    /**
279       SQLITE_xxx constants to export to the async worker
280       counterpart...
281    */
282    state.sq3Codes = Object.create(null);
283    state.sq3Codes._reverse = Object.create(null);
284    [
285      'SQLITE_ERROR', 'SQLITE_IOERR',
286      'SQLITE_NOTFOUND', 'SQLITE_MISUSE',
287      'SQLITE_IOERR_READ', 'SQLITE_IOERR_SHORT_READ',
288      'SQLITE_IOERR_WRITE', 'SQLITE_IOERR_FSYNC',
289      'SQLITE_IOERR_TRUNCATE', 'SQLITE_IOERR_DELETE',
290      'SQLITE_IOERR_ACCESS', 'SQLITE_IOERR_CLOSE',
291      'SQLITE_IOERR_DELETE',
292      'SQLITE_OPEN_CREATE', 'SQLITE_OPEN_DELETEONCLOSE',
293      'SQLITE_OPEN_READONLY'
294    ].forEach(function(k){
295      state.sq3Codes[k] = capi[k] || toss("Maintenance required: not found:",k);
296      state.sq3Codes._reverse[capi[k]] = k;
297    });
298
299    const isWorkerErrCode = (n)=>!!state.sq3Codes._reverse[n];
300
301    /**
302       Runs the given operation (by name) in the async worker
303       counterpart, waits for its response, and returns the result
304       which the async worker writes to SAB[state.opIds.rc]. The
305       2nd and subsequent arguments must be the aruguments for the
306       async op.
307    */
308    const opRun = (op,...args)=>{
309      const opNdx = state.opIds[op] || toss("Invalid op ID:",op);
310      state.s11n.serialize(...args);
311      Atomics.store(state.sabOPView, state.opIds.rc, -1);
312      Atomics.store(state.sabOPView, state.opIds.whichOp, opNdx);
313      Atomics.notify(state.sabOPView, state.opIds.whichOp) /* async thread will take over here */;
314      const t = performance.now();
315      Atomics.wait(state.sabOPView, state.opIds.rc, -1);
316      const rc = Atomics.load(state.sabOPView, state.opIds.rc);
317      metrics[op].wait += performance.now() - t;
318      return rc;
319    };
320
321    const initS11n = ()=>{
322      /**
323         ACHTUNG: this code is 100% duplicated in the other half of
324         this proxy!
325
326         Historical note: this impl was initially about 5% this size by using
327         using JSON.stringify/parse(), but using fit-to-purpose serialization
328         saves considerable runtime.
329      */
330      if(state.s11n) return state.s11n;
331      const textDecoder = new TextDecoder(),
332      textEncoder = new TextEncoder('utf-8'),
333      viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize),
334      viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
335      state.s11n = Object.create(null);
336      const TypeIds = Object.create(null);
337      TypeIds.number  = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' };
338      TypeIds.bigint  = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' };
339      TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' };
340      TypeIds.string =  { id: 4 };
341      const getTypeId = (v)=>{
342        return TypeIds[typeof v] || toss("This value type cannot be serialized.",v);
343      };
344      const getTypeIdById = (tid)=>{
345        switch(tid){
346        case TypeIds.number.id: return TypeIds.number;
347        case TypeIds.bigint.id: return TypeIds.bigint;
348        case TypeIds.boolean.id: return TypeIds.boolean;
349        case TypeIds.string.id: return TypeIds.string;
350        default: toss("Invalid type ID:",tid);
351        }
352      };
353      /**
354         Returns an array of the state serialized by the most recent
355         serialize() operation (here or in the counterpart thread), or
356         null if the serialization buffer is empty.
357      */
358      state.s11n.deserialize = function(){
359        ++metrics.s11n.deserialize.count;
360        const t = performance.now();
361        let rc = null;
362        const argc = viewU8[0];
363        if(argc){
364          rc = [];
365          let offset = 1, i, n, v, typeIds = [];
366          for(i = 0; i < argc; ++i, ++offset){
367            typeIds.push(getTypeIdById(viewU8[offset]));
368          }
369          for(i = 0; i < argc; ++i){
370            const t = typeIds[i];
371            if(t.getter){
372              v = viewDV[t.getter](offset, state.littleEndian);
373              offset += t.size;
374            }else{
375              n = viewDV.getInt32(offset, state.littleEndian);
376              offset += 4;
377              v = textDecoder.decode(viewU8.slice(offset, offset+n));
378              offset += n;
379            }
380            rc.push(v);
381          }
382        }
383        //log("deserialize:",argc, rc);
384        metrics.s11n.deserialize.time += performance.now() - t;
385        return rc;
386      };
387      /**
388         Serializes all arguments to the shared buffer for consumption
389         by the counterpart thread.
390
391         This routine is only intended for serializing OPFS VFS
392         arguments and (in at least one special case) result values,
393         and the buffer is sized to be able to comfortably handle
394         those.
395
396         If passed no arguments then it zeroes out the serialization
397         state.
398      */
399      state.s11n.serialize = function(...args){
400        ++metrics.s11n.serialize.count;
401        const t = performance.now();
402        if(args.length){
403          //log("serialize():",args);
404          let i = 0, offset = 1, typeIds = [];
405          viewU8[0] = args.length & 0xff;
406          for(; i < args.length; ++i, ++offset){
407            typeIds.push(getTypeId(args[i]));
408            viewU8[offset] = typeIds[i].id;
409          }
410          for(i = 0; i < args.length; ++i) {
411            const t = typeIds[i];
412            if(t.setter){
413              viewDV[t.setter](offset, args[i], state.littleEndian);
414              offset += t.size;
415            }else{
416              const s = textEncoder.encode(args[i]);
417              viewDV.setInt32(offset, s.byteLength, state.littleEndian);
418              offset += 4;
419              viewU8.set(s, offset);
420              offset += s.byteLength;
421            }
422          }
423          //log("serialize() result:",viewU8.slice(0,offset));
424        }else{
425          viewU8[0] = 0;
426        }
427        metrics.s11n.serialize.time += performance.now() - t;
428      };
429      return state.s11n;
430    }/*initS11n()*/;
431
432    /**
433       Generates a random ASCII string len characters long, intended for
434       use as a temporary file name.
435    */
436    const randomFilename = function f(len=16){
437      if(!f._chars){
438        f._chars = "abcdefghijklmnopqrstuvwxyz"+
439          "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+
440          "012346789";
441        f._n = f._chars.length;
442      }
443      const a = [];
444      let i = 0;
445      for( ; i < len; ++i){
446        const ndx = Math.random() * (f._n * 64) % f._n | 0;
447        a[i] = f._chars[ndx];
448      }
449      return a.join('');
450    };
451
452    /**
453       Map of sqlite3_file pointers to objects constructed by xOpen().
454    */
455    const __openFiles = Object.create(null);
456
457    /**
458       Installs a StructBinder-bound function pointer member of the
459       given name and function in the given StructType target object.
460       It creates a WASM proxy for the given function and arranges for
461       that proxy to be cleaned up when tgt.dispose() is called.  Throws
462       on the slightest hint of error (e.g. tgt is-not-a StructType,
463       name does not map to a struct-bound member, etc.).
464
465       Returns a proxy for this function which is bound to tgt and takes
466       2 args (name,func). That function returns the same thing,
467       permitting calls to be chained.
468
469       If called with only 1 arg, it has no side effects but returns a
470       func with the same signature as described above.
471    */
472    const installMethod = function callee(tgt, name, func){
473      if(!(tgt instanceof sqlite3.StructBinder.StructType)){
474        toss("Usage error: target object is-not-a StructType.");
475      }
476      if(1===arguments.length){
477        return (n,f)=>callee(tgt,n,f);
478      }
479      if(!callee.argcProxy){
480        callee.argcProxy = function(func,sig){
481          return function(...args){
482            if(func.length!==arguments.length){
483              toss("Argument mismatch. Native signature is:",sig);
484            }
485            return func.apply(this, args);
486          }
487        };
488        callee.removeFuncList = function(){
489          if(this.ondispose.__removeFuncList){
490            this.ondispose.__removeFuncList.forEach(
491              (v,ndx)=>{
492                if('number'===typeof v){
493                  try{wasm.uninstallFunction(v)}
494                  catch(e){/*ignore*/}
495                }
496                /* else it's a descriptive label for the next number in
497                   the list. */
498              }
499            );
500            delete this.ondispose.__removeFuncList;
501          }
502        };
503      }/*static init*/
504      const sigN = tgt.memberSignature(name);
505      if(sigN.length<2){
506        toss("Member",name," is not a function pointer. Signature =",sigN);
507      }
508      const memKey = tgt.memberKey(name);
509      //log("installMethod",tgt, name, sigN);
510      const fProxy = 0
511      // We can remove this proxy middle-man once the VFS is working
512            ? callee.argcProxy(func, sigN)
513            : func;
514      const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
515      tgt[memKey] = pFunc;
516      if(!tgt.ondispose) tgt.ondispose = [];
517      if(!tgt.ondispose.__removeFuncList){
518        tgt.ondispose.push('ondispose.__removeFuncList handler',
519                           callee.removeFuncList);
520        tgt.ondispose.__removeFuncList = [];
521      }
522      tgt.ondispose.__removeFuncList.push(memKey, pFunc);
523      return (n,f)=>callee(tgt, n, f);
524    }/*installMethod*/;
525
526    const opTimer = Object.create(null);
527    opTimer.op = undefined;
528    opTimer.start = undefined;
529    const mTimeStart = (op)=>{
530      opTimer.start = performance.now();
531      opTimer.op = op;
532      //metrics[op] || toss("Maintenance required: missing metrics for",op);
533      ++metrics[op].count;
534    };
535    const mTimeEnd = ()=>(
536      metrics[opTimer.op].time += performance.now() - opTimer.start
537    );
538
539    /**
540       Impls for the sqlite3_io_methods methods. Maintenance reminder:
541       members are in alphabetical order to simplify finding them.
542    */
543    const ioSyncWrappers = {
544      xCheckReservedLock: function(pFile,pOut){
545        // Exclusive lock is automatically acquired when opened
546        //warn("xCheckReservedLock(",arguments,") is a no-op");
547        wasm.setMemValue(pOut,1,'i32');
548        return 0;
549      },
550      xClose: function(pFile){
551        mTimeStart('xClose');
552        let rc = 0;
553        const f = __openFiles[pFile];
554        if(f){
555          delete __openFiles[pFile];
556          rc = opRun('xClose', pFile);
557          if(f.sq3File) f.sq3File.dispose();
558        }
559        mTimeEnd();
560        return rc;
561      },
562      xDeviceCharacteristics: function(pFile){
563        //debug("xDeviceCharacteristics(",pFile,")");
564        return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
565      },
566      xFileControl: function(pFile, opId, pArg){
567        mTimeStart('xFileControl');
568        const rc = (capi.SQLITE_FCNTL_SYNC===opId)
569              ? opRun('xSync', pFile, 0)
570              : capi.SQLITE_NOTFOUND;
571        mTimeEnd();
572        return rc;
573      },
574      xFileSize: function(pFile,pSz64){
575        mTimeStart('xFileSize');
576        const rc = opRun('xFileSize', pFile);
577        if(!isWorkerErrCode(rc)){
578          const sz = state.s11n.deserialize()[0];
579          wasm.setMemValue(pSz64, BigInt(sz), 'i64');
580        }
581        mTimeEnd();
582        return rc;
583      },
584      xLock: function(pFile,lockType){
585        //2022-09: OPFS handles lock when opened
586        //warn("xLock(",arguments,") is a no-op");
587        return 0;
588      },
589      xRead: function(pFile,pDest,n,offset64){
590        /* int (*xRead)(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst) */
591        mTimeStart('xRead');
592        const f = __openFiles[pFile];
593        let rc;
594        try {
595          rc = opRun('xRead',pFile, n, Number(offset64));
596          if(0===rc || capi.SQLITE_IOERR_SHORT_READ===rc){
597            // set() seems to be the fastest way to copy this...
598            wasm.heap8u().set(f.sabView.subarray(0, n), pDest);
599          }
600        }catch(e){
601          error("xRead(",arguments,") failed:",e,f);
602          rc = capi.SQLITE_IOERR_READ;
603        }
604        mTimeEnd();
605        return rc;
606      },
607      xSync: function(pFile,flags){
608        ++metrics.xSync.count;
609        return 0; // impl'd in xFileControl()
610      },
611      xTruncate: function(pFile,sz64){
612        mTimeStart('xTruncate');
613        const rc = opRun('xTruncate', pFile, Number(sz64));
614        mTimeEnd();
615        return rc;
616      },
617      xUnlock: function(pFile,lockType){
618        //2022-09: OPFS handles lock when opened
619        //warn("xUnlock(",arguments,") is a no-op");
620        return 0;
621      },
622      xWrite: function(pFile,pSrc,n,offset64){
623        /* int (*xWrite)(sqlite3_file*, const void*, int iAmt, sqlite3_int64 iOfst) */
624        mTimeStart('xWrite');
625        const f = __openFiles[pFile];
626        let rc;
627        try {
628          f.sabView.set(wasm.heap8u().subarray(pSrc, pSrc+n));
629          rc = opRun('xWrite', pFile, n, Number(offset64));
630        }catch(e){
631          error("xWrite(",arguments,") failed:",e,f);
632          rc = capi.SQLITE_IOERR_WRITE;
633        }
634        mTimeEnd();
635        return rc;
636      }
637    }/*ioSyncWrappers*/;
638
639    /**
640       Impls for the sqlite3_vfs methods. Maintenance reminder: members
641       are in alphabetical order to simplify finding them.
642    */
643    const vfsSyncWrappers = {
644      xAccess: function(pVfs,zName,flags,pOut){
645        mTimeStart('xAccess');
646        const rc = opRun('xAccess', wasm.cstringToJs(zName));
647        wasm.setMemValue( pOut, (rc ? 0 : 1), 'i32' );
648        mTimeEnd();
649        return 0;
650      },
651      xCurrentTime: function(pVfs,pOut){
652        /* If it turns out that we need to adjust for timezone, see:
653           https://stackoverflow.com/a/11760121/1458521 */
654        wasm.setMemValue(pOut, 2440587.5 + (new Date().getTime()/86400000),
655                         'double');
656        return 0;
657      },
658      xCurrentTimeInt64: function(pVfs,pOut){
659        // TODO: confirm that this calculation is correct
660        wasm.setMemValue(pOut, (2440587.5 * 86400000) + new Date().getTime(),
661                         'i64');
662        return 0;
663      },
664      xDelete: function(pVfs, zName, doSyncDir){
665        mTimeStart('xDelete');
666        opRun('xDelete', wasm.cstringToJs(zName), doSyncDir, false);
667        /* We're ignoring errors because we cannot yet differentiate
668           between harmless and non-harmless failures. */
669        mTimeEnd();
670        return 0;
671      },
672      xFullPathname: function(pVfs,zName,nOut,pOut){
673        /* Until/unless we have some notion of "current dir"
674           in OPFS, simply copy zName to pOut... */
675        const i = wasm.cstrncpy(pOut, zName, nOut);
676        return i<nOut ? 0 : capi.SQLITE_CANTOPEN
677        /*CANTOPEN is required by the docs but SQLITE_RANGE would be a closer match*/;
678      },
679      xGetLastError: function(pVfs,nOut,pOut){
680        /* TODO: store exception.message values from the async
681           partner in a dedicated SharedArrayBuffer, noting that we'd have
682           to encode them... TextEncoder can do that for us. */
683        warn("OPFS xGetLastError() has nothing sensible to return.");
684        return 0;
685      },
686      //xSleep is optionally defined below
687      xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){
688        mTimeStart('xOpen');
689        if(!f._){
690          f._ = {
691            fileTypes: {
692              SQLITE_OPEN_MAIN_DB: 'mainDb',
693              SQLITE_OPEN_MAIN_JOURNAL: 'mainJournal',
694              SQLITE_OPEN_TEMP_DB: 'tempDb',
695              SQLITE_OPEN_TEMP_JOURNAL: 'tempJournal',
696              SQLITE_OPEN_TRANSIENT_DB: 'transientDb',
697              SQLITE_OPEN_SUBJOURNAL: 'subjournal',
698              SQLITE_OPEN_SUPER_JOURNAL: 'superJournal',
699              SQLITE_OPEN_WAL: 'wal'
700            },
701            getFileType: function(filename,oflags){
702              const ft = f._.fileTypes;
703              for(let k of Object.keys(ft)){
704                if(oflags & capi[k]) return ft[k];
705              }
706              warn("Cannot determine fileType based on xOpen() flags for file",filename);
707              return '???';
708            }
709          };
710        }
711        if(0===zName){
712          zName = randomFilename();
713        }else if('number'===typeof zName){
714          zName = wasm.cstringToJs(zName);
715        }
716        const fh = Object.create(null);
717        fh.fid = pFile;
718        fh.filename = zName;
719        fh.sab = new SharedArrayBuffer(state.fileBufferSize);
720        fh.flags = flags;
721        const rc = opRun('xOpen', pFile, zName, flags);
722        if(!rc){
723          /* Recall that sqlite3_vfs::xClose() will be called, even on
724             error, unless pFile->pMethods is NULL. */
725          if(fh.readOnly){
726            wasm.setMemValue(pOutFlags, capi.SQLITE_OPEN_READONLY, 'i32');
727          }
728          __openFiles[pFile] = fh;
729          fh.sabView = state.sabFileBufView;
730          fh.sq3File = new sqlite3_file(pFile);
731          fh.sq3File.$pMethods = opfsIoMethods.pointer;
732        }
733        mTimeEnd();
734        return rc;
735      }/*xOpen()*/
736    }/*vfsSyncWrappers*/;
737
738    if(dVfs){
739      opfsVfs.$xRandomness = dVfs.$xRandomness;
740      opfsVfs.$xSleep = dVfs.$xSleep;
741    }
742    if(!opfsVfs.$xRandomness){
743      /* If the default VFS has no xRandomness(), add a basic JS impl... */
744      vfsSyncWrappers.xRandomness = function(pVfs, nOut, pOut){
745        const heap = wasm.heap8u();
746        let i = 0;
747        for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF;
748        return i;
749      };
750    }
751    if(!opfsVfs.$xSleep){
752      /* If we can inherit an xSleep() impl from the default VFS then
753         assume it's sane and use it, otherwise install a JS-based
754         one. */
755      vfsSyncWrappers.xSleep = function(pVfs,ms){
756        Atomics.wait(state.sabOPView, state.opIds.xSleep, 0, ms);
757        return 0;
758      };
759    }
760
761    /* Install the vfs/io_methods into their C-level shared instances... */
762    let inst = installMethod(opfsIoMethods);
763    for(let k of Object.keys(ioSyncWrappers)) inst(k, ioSyncWrappers[k]);
764    inst = installMethod(opfsVfs);
765    for(let k of Object.keys(vfsSyncWrappers)) inst(k, vfsSyncWrappers[k]);
766
767    /**
768       Syncronously deletes the given OPFS filesystem entry, ignoring
769       any errors. As this environment has no notion of "current
770       directory", the given name must be an absolute path. If the 2nd
771       argument is truthy, deletion is recursive (use with caution!).
772
773       Returns true if the deletion succeeded and fails if it fails,
774       but cannot report the nature of the failure.
775    */
776    opfsUtil.deleteEntry = function(fsEntryName,recursive=false){
777      return 0===opRun('xDelete', fsEntryName, 0, recursive);
778    };
779    /**
780       Synchronously creates the given directory name, recursively, in
781       the OPFS filesystem. Returns true if it succeeds or the
782       directory already exists, else false.
783    */
784    opfsUtil.mkdir = function(absDirName){
785      return 0===opRun('mkdir', absDirName);
786    };
787    /**
788       Synchronously checks whether the given OPFS filesystem exists,
789       returning true if it does, false if it doesn't.
790    */
791    opfsUtil.entryExists = function(fsEntryName){
792      return 0===opRun('xAccess', fsEntryName);
793    };
794
795    /**
796       Generates a random ASCII string, intended for use as a
797       temporary file name. Its argument is the length of the string,
798       defaulting to 16.
799    */
800    opfsUtil.randomFilename = randomFilename;
801
802    if(sqlite3.oo1){
803      opfsUtil.OpfsDb = function(...args){
804        const opt = sqlite3.oo1.dbCtorHelper.normalizeArgs(...args);
805        opt.vfs = opfsVfs.$zName;
806        sqlite3.oo1.dbCtorHelper.call(this, opt);
807      };
808      opfsUtil.OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype);
809    }
810
811    /**
812       Potential TODOs:
813
814       - Expose one or both of the Worker objects via opfsUtil and
815         publish an interface for proxying the higher-level OPFS
816         features like getting a directory listing.
817    */
818
819    const sanityCheck = function(){
820      const scope = wasm.scopedAllocPush();
821      const sq3File = new sqlite3_file();
822      try{
823        const fid = sq3File.pointer;
824        const openFlags = capi.SQLITE_OPEN_CREATE
825              | capi.SQLITE_OPEN_READWRITE
826        //| capi.SQLITE_OPEN_DELETEONCLOSE
827              | capi.SQLITE_OPEN_MAIN_DB;
828        const pOut = wasm.scopedAlloc(8);
829        const dbFile = "/sanity/check/file"+randomFilename(8);
830        const zDbFile = wasm.scopedAllocCString(dbFile);
831        let rc;
832        vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
833        rc = wasm.getMemValue(pOut,'i32');
834        log("xAccess(",dbFile,") exists ?=",rc);
835        rc = vfsSyncWrappers.xOpen(opfsVfs.pointer, zDbFile,
836                                   fid, openFlags, pOut);
837        log("open rc =",rc,"state.sabOPView[xOpen] =",
838            state.sabOPView[state.opIds.xOpen]);
839        if(isWorkerErrCode(rc)){
840          error("open failed with code",rc);
841          return;
842        }
843        vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
844        rc = wasm.getMemValue(pOut,'i32');
845        if(!rc) toss("xAccess() failed to detect file.");
846        rc = ioSyncWrappers.xSync(sq3File.pointer, 0);
847        if(rc) toss('sync failed w/ rc',rc);
848        rc = ioSyncWrappers.xTruncate(sq3File.pointer, 1024);
849        if(rc) toss('truncate failed w/ rc',rc);
850        wasm.setMemValue(pOut,0,'i64');
851        rc = ioSyncWrappers.xFileSize(sq3File.pointer, pOut);
852        if(rc) toss('xFileSize failed w/ rc',rc);
853        log("xFileSize says:",wasm.getMemValue(pOut, 'i64'));
854        rc = ioSyncWrappers.xWrite(sq3File.pointer, zDbFile, 10, 1);
855        if(rc) toss("xWrite() failed!");
856        const readBuf = wasm.scopedAlloc(16);
857        rc = ioSyncWrappers.xRead(sq3File.pointer, readBuf, 6, 2);
858        wasm.setMemValue(readBuf+6,0);
859        let jRead = wasm.cstringToJs(readBuf);
860        log("xRead() got:",jRead);
861        if("sanity"!==jRead) toss("Unexpected xRead() value.");
862        if(vfsSyncWrappers.xSleep){
863          log("xSleep()ing before close()ing...");
864          vfsSyncWrappers.xSleep(opfsVfs.pointer,2000);
865          log("waking up from xSleep()");
866        }
867        rc = ioSyncWrappers.xClose(fid);
868        log("xClose rc =",rc,"sabOPView =",state.sabOPView);
869        log("Deleting file:",dbFile);
870        vfsSyncWrappers.xDelete(opfsVfs.pointer, zDbFile, 0x1234);
871        vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
872        rc = wasm.getMemValue(pOut,'i32');
873        if(rc) toss("Expecting 0 from xAccess(",dbFile,") after xDelete().");
874        warn("End of OPFS sanity checks.");
875      }finally{
876        sq3File.dispose();
877        wasm.scopedAllocPop(scope);
878      }
879    }/*sanityCheck()*/;
880
881
882    W.onmessage = function({data}){
883      //log("Worker.onmessage:",data);
884      switch(data.type){
885          case 'opfs-async-loaded':
886            /*Pass our config and shared state on to the async worker.*/
887            W.postMessage({type: 'opfs-async-init',args: state});
888            break;
889          case 'opfs-async-inited':{
890            /*Indicates that the async partner has received the 'init',
891              so we now know that the state object is no longer subject to
892              being copied by a pending postMessage() call.*/
893            try {
894              const rc = capi.sqlite3_vfs_register(opfsVfs.pointer, 0);
895              if(rc){
896                opfsVfs.dispose();
897                toss("sqlite3_vfs_register(OPFS) failed with rc",rc);
898              }
899              if(opfsVfs.pointer !== capi.sqlite3_vfs_find("opfs")){
900                toss("BUG: sqlite3_vfs_find() failed for just-installed OPFS VFS");
901              }
902              capi.sqlite3_vfs_register.addReference(opfsVfs, opfsIoMethods);
903              state.sabOPView = new Int32Array(state.sabOP);
904              state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
905              state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
906              initS11n();
907              if(options.sanityChecks){
908                warn("Running sanity checks because of opfs-sanity-check URL arg...");
909                sanityCheck();
910              }
911              W.onerror = W._originalOnError;
912              delete W._originalOnError;
913              sqlite3.opfs = opfsUtil;
914              log("End of OPFS sqlite3_vfs setup.", opfsVfs);
915              promiseResolve(sqlite3);
916            }catch(e){
917              error(e);
918              promiseReject(e);
919            }
920            break;
921          }
922          default:
923            promiseReject(e);
924            error("Unexpected message from the async worker:",data);
925            break;
926      }
927    };
928  })/*thePromise*/;
929  return thePromise;
930}/*installOpfsVfs()*/;
931sqlite3.installOpfsVfs.defaultProxyUri = "sqlite3-opfs-async-proxy.js";
932}/*sqlite3ApiBootstrap.initializers.push()*/);
933