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