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