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