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