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