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