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