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*/
21
22'use strict';
23self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
24/**
25   sqlite3.installOpfsVfs() returns a Promise which, on success, installs
26   an sqlite3_vfs named "opfs", suitable for use with all sqlite3 APIs
27   which accept a VFS. It uses the Origin-Private FileSystem API for
28   all file storage. On error it is rejected with an exception
29   explaining the problem. Reasons for rejection include, but are
30   not limited to:
31
32   - The counterpart Worker (see below) could not be loaded.
33
34   - The environment does not support OPFS. That includes when
35     this function is called from the main window thread.
36
37
38  Significant notes and limitations:
39
40  - As of this writing, OPFS is still very much in flux and only
41    available in bleeding-edge versions of Chrome (v102+, noting that
42    that number will increase as the OPFS API matures).
43
44  - The OPFS features used here are only available in dedicated Worker
45    threads. This file tries to detect that case, resulting in a
46    rejected Promise if those features do not seem to be available.
47
48  - It requires the SharedArrayBuffer and Atomics classes, and the
49    former is only available if the HTTP server emits the so-called
50    COOP and COEP response headers. These features are required for
51    proxying OPFS's synchronous API via the synchronous interface
52    required by the sqlite3_vfs API.
53
54  - This function may only be called a single time and it must be
55    called from the client, as opposed to the library initialization,
56    in case the client requires a custom path for this API's
57    "counterpart": this function's argument is the relative URI to
58    this module's "asynchronous half". When called, this function removes
59    itself from the sqlite3 object.
60
61   The argument may optionally be a plain object with the following
62   configuration options:
63
64   - proxyUri: as described above
65
66   - verbose (=2): an integer 0-3. 0 disables all logging, 1 enables
67     logging of errors. 2 enables logging of warnings and errors. 3
68     additionally enables debugging info.
69
70   - sanityChecks (=false): if true, some basic sanity tests are
71     run on the OPFS VFS API after it's initialized, before the
72     returned Promise resolves.
73
74   On success, the Promise resolves to the top-most sqlite3 namespace
75   object and that object gets a new object installed in its
76   `opfs` property, containing several OPFS-specific utilities.
77*/
78sqlite3.installOpfsVfs = function callee(asyncProxyUri = callee.defaultProxyUri){
79  delete sqlite3.installOpfsVfs;
80  if(self.window===self ||
81     !self.SharedArrayBuffer ||
82     !self.FileSystemHandle ||
83     !self.FileSystemDirectoryHandle ||
84     !self.FileSystemFileHandle ||
85     !self.FileSystemFileHandle.prototype.createSyncAccessHandle ||
86     !navigator.storage.getDirectory){
87    return Promise.reject(
88      new Error("This environment does not have OPFS support.")
89    );
90  }
91  const options = (asyncProxyUri && 'object'===asyncProxyUri) ? asyncProxyUri : {
92    proxyUri: asyncProxyUri
93  };
94  const urlParams = new URL(self.location.href).searchParams;
95  if(undefined===options.verbose){
96    options.verbose = urlParams.has('opfs-verbose') ? 3 : 2;
97  }
98  if(undefined===options.sanityChecks){
99    options.sanityChecks = urlParams.has('opfs-sanity-check');
100  }
101  if(undefined===options.proxyUri){
102    options.proxyUri = callee.defaultProxyUri;
103  }
104
105  const thePromise = new Promise(function(promiseResolve, promiseReject){
106    const loggers = {
107      0:console.error.bind(console),
108      1:console.warn.bind(console),
109      2:console.log.bind(console)
110    };
111    const logImpl = (level,...args)=>{
112      if(options.verbose>level) loggers[level]("OPFS syncer:",...args);
113    };
114    const log =    (...args)=>logImpl(2, ...args);
115    const warn =   (...args)=>logImpl(1, ...args);
116    const error =  (...args)=>logImpl(0, ...args);
117    warn("The OPFS VFS feature is very much experimental and under construction.");
118    const toss = function(...args){throw new Error(args.join(' '))};
119    const capi = sqlite3.capi;
120    const wasm = capi.wasm;
121    const sqlite3_vfs = capi.sqlite3_vfs;
122    const sqlite3_file = capi.sqlite3_file;
123    const sqlite3_io_methods = capi.sqlite3_io_methods;
124    const W = new Worker(options.proxyUri);
125    W._originalOnError = W.onerror /* will be restored later */;
126    W.onerror = function(err){
127      // The error object doesn't contain any useful info when the
128      // failure is, e.g., that the remote script is 404.
129      promiseReject(new Error("Loading OPFS async Worker failed for unknown reasons."));
130    };
131    /**
132       Generic utilities for working with OPFS. This will get filled out
133       by the Promise setup and, on success, installed as sqlite3.opfs.
134    */
135    const opfsUtil = Object.create(null);
136    /**
137       Not part of the public API. Solely for internal/development
138       use.
139    */
140    opfsUtil.metrics = {
141      dump: function(){
142        let k, n = 0, t = 0, w = 0;
143        for(k in state.opIds){
144          const m = metrics[k];
145          n += m.count;
146          t += m.time;
147          w += m.wait;
148          m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
149          m.avgWait = (m.count && m.wait) ? (m.wait / m.count) : 0;
150        }
151        console.log(self.location.href,
152                    "metrics for",self.location.href,":",metrics,
153                    "\nTotal of",n,"op(s) for",t,
154                    "ms (incl. "+w+" ms of waiting on the async side)");
155        console.log("Serialization metrics:",JSON.stringify(metrics.s11n,0,2));
156      },
157      reset: function(){
158        let k;
159        const r = (m)=>(m.count = m.time = m.wait = 0);
160        for(k in state.opIds){
161          r(metrics[k] = Object.create(null));
162        }
163        let s = metrics.s11n = Object.create(null);
164        s = s.serialize = Object.create(null);
165        s.count = s.time = 0;
166        s = metrics.s11n.deserialize = Object.create(null);
167        s.count = s.time = 0;
168        //[ // timed routines which are not in state.opIds
169        //  'xFileControl'
170        //].forEach((k)=>r(metrics[k] = Object.create(null)));
171      }
172    }/*metrics*/;
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.littleEndian = true;
227    state.verbose = options.verbose;
228    /* Size of file I/O buffer block. 64k = max sqlite3 page size. */
229    state.fileBufferSize =
230      1024 * 64;
231    state.sabS11nOffset = state.fileBufferSize;
232    /**
233       The size of the block in our SAB for serializing arguments and
234       result values. Need to be large enough to hold serialized
235       values of any of the proxied APIs. Filenames are the largest
236       part but are limited to opfsVfs.$mxPathname bytes.
237    */
238    state.sabS11nSize = opfsVfs.$mxPathname * 2;
239    /**
240       The SAB used for all data I/O (files and arg/result s11n).
241    */
242    state.sabIO = new SharedArrayBuffer(
243      state.fileBufferSize/* file i/o block */
244      + state.sabS11nSize/* argument/result serialization block */
245    );
246    state.opIds = Object.create(null);
247    const metrics = Object.create(null);
248    {
249      /* Indexes for use in our SharedArrayBuffer... */
250      let i = 0;
251      /* SAB slot used to communicate which operation is desired
252         between both workers. This worker writes to it and the other
253         listens for changes. */
254      state.opIds.whichOp = i++;
255      /* Slot for storing return values. This work listens to that
256         slot and the other worker writes to it. */
257      state.opIds.rc = i++;
258      /* Each function gets an ID which this worker writes to
259         the whichOp slot. The async-api worker uses Atomic.wait()
260         on the whichOp slot to figure out which operation to run
261         next. */
262      state.opIds.xAccess = i++;
263      state.opIds.xClose = i++;
264      state.opIds.xDelete = i++;
265      state.opIds.xDeleteNoWait = i++;
266      state.opIds.xFileSize = i++;
267      state.opIds.xOpen = i++;
268      state.opIds.xRead = i++;
269      state.opIds.xSleep = i++;
270      state.opIds.xSync = i++;
271      state.opIds.xTruncate = i++;
272      state.opIds.xWrite = i++;
273      state.opIds.mkdir = i++;
274      state.opIds.xFileControl = i++;
275      state.sabOP = new SharedArrayBuffer(i * 4/*sizeof int32*/);
276      opfsUtil.metrics.reset();
277    }
278
279    /**
280       SQLITE_xxx constants to export to the async worker
281       counterpart...
282    */
283    state.sq3Codes = Object.create(null);
284    state.sq3Codes._reverse = Object.create(null);
285    [
286      'SQLITE_ERROR', 'SQLITE_IOERR',
287      'SQLITE_NOTFOUND', 'SQLITE_MISUSE',
288      'SQLITE_IOERR_READ', 'SQLITE_IOERR_SHORT_READ',
289      'SQLITE_IOERR_WRITE', 'SQLITE_IOERR_FSYNC',
290      'SQLITE_IOERR_TRUNCATE', 'SQLITE_IOERR_DELETE',
291      'SQLITE_IOERR_ACCESS', 'SQLITE_IOERR_CLOSE',
292      'SQLITE_IOERR_DELETE',
293      'SQLITE_OPEN_CREATE', 'SQLITE_OPEN_DELETEONCLOSE',
294      'SQLITE_OPEN_READONLY'
295    ].forEach(function(k){
296      state.sq3Codes[k] = capi[k] || toss("Maintenance required: not found:",k);
297      state.sq3Codes._reverse[capi[k]] = k;
298    });
299
300    const isWorkerErrCode = (n)=>!!state.sq3Codes._reverse[n];
301
302    /**
303       Runs the given operation (by name) in the async worker
304       counterpart, waits for its response, and returns the result
305       which the async worker writes to SAB[state.opIds.rc]. The
306       2nd and subsequent arguments must be the aruguments for the
307       async op.
308    */
309    const opRun = (op,...args)=>{
310      const opNdx = state.opIds[op] || toss("Invalid op ID:",op);
311      state.s11n.serialize(...args);
312      Atomics.store(state.sabOPView, state.opIds.rc, -1);
313      Atomics.store(state.sabOPView, state.opIds.whichOp, opNdx);
314      Atomics.notify(state.sabOPView, state.opIds.whichOp) /* async thread will take over here */;
315      const t = performance.now();
316      Atomics.wait(state.sabOPView, state.opIds.rc, -1);
317      const rc = Atomics.load(state.sabOPView, state.opIds.rc);
318      metrics[op].wait += performance.now() - t;
319      return rc;
320    };
321
322    const initS11n = ()=>{
323      /**
324         ACHTUNG: this code is 100% duplicated in the other half of
325         this proxy!
326
327         Historical note: this impl was initially about 5% this size by using
328         using JSON.stringify/parse(), but using fit-to-purpose serialization
329         saves considerable runtime.
330      */
331      if(state.s11n) return state.s11n;
332      const textDecoder = new TextDecoder(),
333      textEncoder = new TextEncoder('utf-8'),
334      viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize),
335      viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
336      state.s11n = Object.create(null);
337      const TypeIds = Object.create(null);
338      TypeIds.number  = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' };
339      TypeIds.bigint  = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' };
340      TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' };
341      TypeIds.string =  { id: 4 };
342      const getTypeId = (v)=>{
343        return TypeIds[typeof v] || toss("This value type cannot be serialized.",v);
344      };
345      const getTypeIdById = (tid)=>{
346        switch(tid){
347        case TypeIds.number.id: return TypeIds.number;
348        case TypeIds.bigint.id: return TypeIds.bigint;
349        case TypeIds.boolean.id: return TypeIds.boolean;
350        case TypeIds.string.id: return TypeIds.string;
351        default: toss("Invalid type ID:",tid);
352        }
353      };
354      /**
355         Returns an array of the state serialized by the most recent
356         serialize() operation (here or in the counterpart thread), or
357         null if the serialization buffer is empty.
358      */
359      state.s11n.deserialize = function(){
360        ++metrics.s11n.deserialize.count;
361        const t = performance.now();
362        let rc = null;
363        const argc = viewU8[0];
364        if(argc){
365          rc = [];
366          let offset = 1, i, n, v, typeIds = [];
367          for(i = 0; i < argc; ++i, ++offset){
368            typeIds.push(getTypeIdById(viewU8[offset]));
369          }
370          for(i = 0; i < argc; ++i){
371            const t = typeIds[i];
372            if(t.getter){
373              v = viewDV[t.getter](offset, state.littleEndian);
374              offset += t.size;
375            }else{
376              n = viewDV.getInt32(offset, state.littleEndian);
377              offset += 4;
378              v = textDecoder.decode(viewU8.slice(offset, offset+n));
379              offset += n;
380            }
381            rc.push(v);
382          }
383        }
384        //log("deserialize:",argc, rc);
385        metrics.s11n.deserialize.time += performance.now() - t;
386        return rc;
387      };
388      /**
389         Serializes all arguments to the shared buffer for consumption
390         by the counterpart thread.
391
392         This routine is only intended for serializing OPFS VFS
393         arguments and (in at least one special case) result values,
394         and the buffer is sized to be able to comfortably handle
395         those.
396
397         If passed no arguments then it zeroes out the serialization
398         state.
399      */
400      state.s11n.serialize = function(...args){
401        ++metrics.s11n.serialize.count;
402        const t = performance.now();
403        if(args.length){
404          //log("serialize():",args);
405          let i = 0, offset = 1, typeIds = [];
406          viewU8[0] = args.length & 0xff;
407          for(; i < args.length; ++i, ++offset){
408            typeIds.push(getTypeId(args[i]));
409            viewU8[offset] = typeIds[i].id;
410          }
411          for(i = 0; i < args.length; ++i) {
412            const t = typeIds[i];
413            if(t.setter){
414              viewDV[t.setter](offset, args[i], state.littleEndian);
415              offset += t.size;
416            }else{
417              const s = textEncoder.encode(args[i]);
418              viewDV.setInt32(offset, s.byteLength, state.littleEndian);
419              offset += 4;
420              viewU8.set(s, offset);
421              offset += s.byteLength;
422            }
423          }
424          //log("serialize() result:",viewU8.slice(0,offset));
425        }else{
426          viewU8[0] = 0;
427        }
428        metrics.s11n.serialize.time += performance.now() - t;
429      };
430      return state.s11n;
431    }/*initS11n()*/;
432
433    /**
434       Generates a random ASCII string len characters long, intended for
435       use as a temporary file name.
436    */
437    const randomFilename = function f(len=16){
438      if(!f._chars){
439        f._chars = "abcdefghijklmnopqrstuvwxyz"+
440          "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+
441          "012346789";
442        f._n = f._chars.length;
443      }
444      const a = [];
445      let i = 0;
446      for( ; i < len; ++i){
447        const ndx = Math.random() * (f._n * 64) % f._n | 0;
448        a[i] = f._chars[ndx];
449      }
450      return a.join('');
451    };
452
453    /**
454       Map of sqlite3_file pointers to objects constructed by xOpen().
455    */
456    const __openFiles = Object.create(null);
457
458    /**
459       Installs a StructBinder-bound function pointer member of the
460       given name and function in the given StructType target object.
461       It creates a WASM proxy for the given function and arranges for
462       that proxy to be cleaned up when tgt.dispose() is called.  Throws
463       on the slightest hint of error (e.g. tgt is-not-a StructType,
464       name does not map to a struct-bound member, etc.).
465
466       Returns a proxy for this function which is bound to tgt and takes
467       2 args (name,func). That function returns the same thing,
468       permitting calls to be chained.
469
470       If called with only 1 arg, it has no side effects but returns a
471       func with the same signature as described above.
472    */
473    const installMethod = function callee(tgt, name, func){
474      if(!(tgt instanceof sqlite3.StructBinder.StructType)){
475        toss("Usage error: target object is-not-a StructType.");
476      }
477      if(1===arguments.length){
478        return (n,f)=>callee(tgt,n,f);
479      }
480      if(!callee.argcProxy){
481        callee.argcProxy = function(func,sig){
482          return function(...args){
483            if(func.length!==arguments.length){
484              toss("Argument mismatch. Native signature is:",sig);
485            }
486            return func.apply(this, args);
487          }
488        };
489        callee.removeFuncList = function(){
490          if(this.ondispose.__removeFuncList){
491            this.ondispose.__removeFuncList.forEach(
492              (v,ndx)=>{
493                if('number'===typeof v){
494                  try{wasm.uninstallFunction(v)}
495                  catch(e){/*ignore*/}
496                }
497                /* else it's a descriptive label for the next number in
498                   the list. */
499              }
500            );
501            delete this.ondispose.__removeFuncList;
502          }
503        };
504      }/*static init*/
505      const sigN = tgt.memberSignature(name);
506      if(sigN.length<2){
507        toss("Member",name," is not a function pointer. Signature =",sigN);
508      }
509      const memKey = tgt.memberKey(name);
510      //log("installMethod",tgt, name, sigN);
511      const fProxy = 0
512      // We can remove this proxy middle-man once the VFS is working
513            ? callee.argcProxy(func, sigN)
514            : func;
515      const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
516      tgt[memKey] = pFunc;
517      if(!tgt.ondispose) tgt.ondispose = [];
518      if(!tgt.ondispose.__removeFuncList){
519        tgt.ondispose.push('ondispose.__removeFuncList handler',
520                           callee.removeFuncList);
521        tgt.ondispose.__removeFuncList = [];
522      }
523      tgt.ondispose.__removeFuncList.push(memKey, pFunc);
524      return (n,f)=>callee(tgt, n, f);
525    }/*installMethod*/;
526
527    const opTimer = Object.create(null);
528    opTimer.op = undefined;
529    opTimer.start = undefined;
530    const mTimeStart = (op)=>{
531      opTimer.start = performance.now();
532      opTimer.op = op;
533      //metrics[op] || toss("Maintenance required: missing metrics for",op);
534      ++metrics[op].count;
535    };
536    const mTimeEnd = ()=>(
537      metrics[opTimer.op].time += performance.now() - opTimer.start
538    );
539
540    /**
541       Impls for the sqlite3_io_methods methods. Maintenance reminder:
542       members are in alphabetical order to simplify finding them.
543    */
544    const ioSyncWrappers = {
545      xCheckReservedLock: function(pFile,pOut){
546        // Exclusive lock is automatically acquired when opened
547        //warn("xCheckReservedLock(",arguments,") is a no-op");
548        wasm.setMemValue(pOut,1,'i32');
549        return 0;
550      },
551      xClose: function(pFile){
552        mTimeStart('xClose');
553        let rc = 0;
554        const f = __openFiles[pFile];
555        if(f){
556          delete __openFiles[pFile];
557          rc = opRun('xClose', pFile);
558          if(f.sq3File) f.sq3File.dispose();
559        }
560        mTimeEnd();
561        return rc;
562      },
563      xDeviceCharacteristics: function(pFile){
564        //debug("xDeviceCharacteristics(",pFile,")");
565        return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
566      },
567      xFileControl: function(pFile, opId, pArg){
568        mTimeStart('xFileControl');
569        const rc = (capi.SQLITE_FCNTL_SYNC===opId)
570              ? opRun('xSync', pFile, 0)
571              : capi.SQLITE_NOTFOUND;
572        mTimeEnd();
573        return rc;
574      },
575      xFileSize: function(pFile,pSz64){
576        mTimeStart('xFileSize');
577        const rc = opRun('xFileSize', pFile);
578        if(!isWorkerErrCode(rc)){
579          const sz = state.s11n.deserialize()[0];
580          wasm.setMemValue(pSz64, BigInt(sz), 'i64');
581        }
582        mTimeEnd();
583        return rc;
584      },
585      xLock: function(pFile,lockType){
586        //2022-09: OPFS handles lock when opened
587        //warn("xLock(",arguments,") is a no-op");
588        return 0;
589      },
590      xRead: function(pFile,pDest,n,offset64){
591        /* int (*xRead)(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst) */
592        mTimeStart('xRead');
593        const f = __openFiles[pFile];
594        let rc;
595        try {
596          rc = opRun('xRead',pFile, n, Number(offset64));
597          if(0===rc || capi.SQLITE_IOERR_SHORT_READ===rc){
598            // set() seems to be the fastest way to copy this...
599            wasm.heap8u().set(f.sabView.subarray(0, n), pDest);
600          }
601        }catch(e){
602          error("xRead(",arguments,") failed:",e,f);
603          rc = capi.SQLITE_IOERR_READ;
604        }
605        mTimeEnd();
606        return rc;
607      },
608      xSync: function(pFile,flags){
609        ++metrics.xSync.count;
610        return 0; // impl'd in xFileControl()
611      },
612      xTruncate: function(pFile,sz64){
613        mTimeStart('xTruncate');
614        const rc = opRun('xTruncate', pFile, Number(sz64));
615        mTimeEnd();
616        return rc;
617      },
618      xUnlock: function(pFile,lockType){
619        //2022-09: OPFS handles lock when opened
620        //warn("xUnlock(",arguments,") is a no-op");
621        return 0;
622      },
623      xWrite: function(pFile,pSrc,n,offset64){
624        /* int (*xWrite)(sqlite3_file*, const void*, int iAmt, sqlite3_int64 iOfst) */
625        mTimeStart('xWrite');
626        const f = __openFiles[pFile];
627        let rc;
628        try {
629          f.sabView.set(wasm.heap8u().subarray(pSrc, pSrc+n));
630          rc = opRun('xWrite', pFile, n, Number(offset64));
631        }catch(e){
632          error("xWrite(",arguments,") failed:",e,f);
633          rc = capi.SQLITE_IOERR_WRITE;
634        }
635        mTimeEnd();
636        return rc;
637      }
638    }/*ioSyncWrappers*/;
639
640    /**
641       Impls for the sqlite3_vfs methods. Maintenance reminder: members
642       are in alphabetical order to simplify finding them.
643    */
644    const vfsSyncWrappers = {
645      xAccess: function(pVfs,zName,flags,pOut){
646        mTimeStart('xAccess');
647        const rc = opRun('xAccess', wasm.cstringToJs(zName));
648        wasm.setMemValue( pOut, (rc ? 0 : 1), 'i32' );
649        mTimeEnd();
650        return 0;
651      },
652      xCurrentTime: function(pVfs,pOut){
653        /* If it turns out that we need to adjust for timezone, see:
654           https://stackoverflow.com/a/11760121/1458521 */
655        wasm.setMemValue(pOut, 2440587.5 + (new Date().getTime()/86400000),
656                         'double');
657        return 0;
658      },
659      xCurrentTimeInt64: function(pVfs,pOut){
660        // TODO: confirm that this calculation is correct
661        wasm.setMemValue(pOut, (2440587.5 * 86400000) + new Date().getTime(),
662                         'i64');
663        return 0;
664      },
665      xDelete: function(pVfs, zName, doSyncDir){
666        mTimeStart('xDelete');
667        opRun('xDelete', wasm.cstringToJs(zName), doSyncDir, false);
668        /* We're ignoring errors because we cannot yet differentiate
669           between harmless and non-harmless failures. */
670        mTimeEnd();
671        return 0;
672      },
673      xFullPathname: function(pVfs,zName,nOut,pOut){
674        /* Until/unless we have some notion of "current dir"
675           in OPFS, simply copy zName to pOut... */
676        const i = wasm.cstrncpy(pOut, zName, nOut);
677        return i<nOut ? 0 : capi.SQLITE_CANTOPEN
678        /*CANTOPEN is required by the docs but SQLITE_RANGE would be a closer match*/;
679      },
680      xGetLastError: function(pVfs,nOut,pOut){
681        /* TODO: store exception.message values from the async
682           partner in a dedicated SharedArrayBuffer, noting that we'd have
683           to encode them... TextEncoder can do that for us. */
684        warn("OPFS xGetLastError() has nothing sensible to return.");
685        return 0;
686      },
687      //xSleep is optionally defined below
688      xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){
689        mTimeStart('xOpen');
690        if(!f._){
691          f._ = {
692            fileTypes: {
693              SQLITE_OPEN_MAIN_DB: 'mainDb',
694              SQLITE_OPEN_MAIN_JOURNAL: 'mainJournal',
695              SQLITE_OPEN_TEMP_DB: 'tempDb',
696              SQLITE_OPEN_TEMP_JOURNAL: 'tempJournal',
697              SQLITE_OPEN_TRANSIENT_DB: 'transientDb',
698              SQLITE_OPEN_SUBJOURNAL: 'subjournal',
699              SQLITE_OPEN_SUPER_JOURNAL: 'superJournal',
700              SQLITE_OPEN_WAL: 'wal'
701            },
702            getFileType: function(filename,oflags){
703              const ft = f._.fileTypes;
704              for(let k of Object.keys(ft)){
705                if(oflags & capi[k]) return ft[k];
706              }
707              warn("Cannot determine fileType based on xOpen() flags for file",filename);
708              return '???';
709            }
710          };
711        }
712        if(0===zName){
713          zName = randomFilename();
714        }else if('number'===typeof zName){
715          zName = wasm.cstringToJs(zName);
716        }
717        const fh = Object.create(null);
718        fh.fid = pFile;
719        fh.filename = zName;
720        fh.sab = new SharedArrayBuffer(state.fileBufferSize);
721        fh.flags = flags;
722        const rc = opRun('xOpen', pFile, zName, flags);
723        if(!rc){
724          /* Recall that sqlite3_vfs::xClose() will be called, even on
725             error, unless pFile->pMethods is NULL. */
726          if(fh.readOnly){
727            wasm.setMemValue(pOutFlags, capi.SQLITE_OPEN_READONLY, 'i32');
728          }
729          __openFiles[pFile] = fh;
730          fh.sabView = state.sabFileBufView;
731          fh.sq3File = new sqlite3_file(pFile);
732          fh.sq3File.$pMethods = opfsIoMethods.pointer;
733        }
734        mTimeEnd();
735        return rc;
736      }/*xOpen()*/
737    }/*vfsSyncWrappers*/;
738
739    if(dVfs){
740      opfsVfs.$xRandomness = dVfs.$xRandomness;
741      opfsVfs.$xSleep = dVfs.$xSleep;
742    }
743    if(!opfsVfs.$xRandomness){
744      /* If the default VFS has no xRandomness(), add a basic JS impl... */
745      vfsSyncWrappers.xRandomness = function(pVfs, nOut, pOut){
746        const heap = wasm.heap8u();
747        let i = 0;
748        for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF;
749        return i;
750      };
751    }
752    if(!opfsVfs.$xSleep){
753      /* If we can inherit an xSleep() impl from the default VFS then
754         assume it's sane and use it, otherwise install a JS-based
755         one. */
756      vfsSyncWrappers.xSleep = function(pVfs,ms){
757        Atomics.wait(state.sabOPView, state.opIds.xSleep, 0, ms);
758        return 0;
759      };
760    }
761
762    /* Install the vfs/io_methods into their C-level shared instances... */
763    let inst = installMethod(opfsIoMethods);
764    for(let k of Object.keys(ioSyncWrappers)) inst(k, ioSyncWrappers[k]);
765    inst = installMethod(opfsVfs);
766    for(let k of Object.keys(vfsSyncWrappers)) inst(k, vfsSyncWrappers[k]);
767
768    /**
769       Syncronously deletes the given OPFS filesystem entry, ignoring
770       any errors. As this environment has no notion of "current
771       directory", the given name must be an absolute path. If the 2nd
772       argument is truthy, deletion is recursive (use with caution!).
773
774       Returns true if the deletion succeeded and fails if it fails,
775       but cannot report the nature of the failure.
776    */
777    opfsUtil.deleteEntry = function(fsEntryName,recursive=false){
778      return 0===opRun('xDelete', fsEntryName, 0, recursive);
779    };
780    /**
781       Synchronously creates the given directory name, recursively, in
782       the OPFS filesystem. Returns true if it succeeds or the
783       directory already exists, else false.
784    */
785    opfsUtil.mkdir = function(absDirName){
786      return 0===opRun('mkdir', absDirName);
787    };
788    /**
789       Synchronously checks whether the given OPFS filesystem exists,
790       returning true if it does, false if it doesn't.
791    */
792    opfsUtil.entryExists = function(fsEntryName){
793      return 0===opRun('xAccess', fsEntryName);
794    };
795
796    /**
797       Generates a random ASCII string, intended for use as a
798       temporary file name. Its argument is the length of the string,
799       defaulting to 16.
800    */
801    opfsUtil.randomFilename = randomFilename;
802
803    if(sqlite3.oo1){
804      opfsUtil.OpfsDb = function(...args){
805        const opt = sqlite3.oo1.dbCtorHelper.normalizeArgs(...args);
806        opt.vfs = opfsVfs.$zName;
807        sqlite3.oo1.dbCtorHelper.call(this, opt);
808      };
809      opfsUtil.OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype);
810    }
811
812    /**
813       Potential TODOs:
814
815       - Expose one or both of the Worker objects via opfsUtil and
816         publish an interface for proxying the higher-level OPFS
817         features like getting a directory listing.
818    */
819
820    const sanityCheck = function(){
821      const scope = wasm.scopedAllocPush();
822      const sq3File = new sqlite3_file();
823      try{
824        const fid = sq3File.pointer;
825        const openFlags = capi.SQLITE_OPEN_CREATE
826              | capi.SQLITE_OPEN_READWRITE
827        //| capi.SQLITE_OPEN_DELETEONCLOSE
828              | capi.SQLITE_OPEN_MAIN_DB;
829        const pOut = wasm.scopedAlloc(8);
830        const dbFile = "/sanity/check/file"+randomFilename(8);
831        const zDbFile = wasm.scopedAllocCString(dbFile);
832        let rc;
833        vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
834        rc = wasm.getMemValue(pOut,'i32');
835        log("xAccess(",dbFile,") exists ?=",rc);
836        rc = vfsSyncWrappers.xOpen(opfsVfs.pointer, zDbFile,
837                                   fid, openFlags, pOut);
838        log("open rc =",rc,"state.sabOPView[xOpen] =",
839            state.sabOPView[state.opIds.xOpen]);
840        if(isWorkerErrCode(rc)){
841          error("open failed with code",rc);
842          return;
843        }
844        vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
845        rc = wasm.getMemValue(pOut,'i32');
846        if(!rc) toss("xAccess() failed to detect file.");
847        rc = ioSyncWrappers.xSync(sq3File.pointer, 0);
848        if(rc) toss('sync failed w/ rc',rc);
849        rc = ioSyncWrappers.xTruncate(sq3File.pointer, 1024);
850        if(rc) toss('truncate failed w/ rc',rc);
851        wasm.setMemValue(pOut,0,'i64');
852        rc = ioSyncWrappers.xFileSize(sq3File.pointer, pOut);
853        if(rc) toss('xFileSize failed w/ rc',rc);
854        log("xFileSize says:",wasm.getMemValue(pOut, 'i64'));
855        rc = ioSyncWrappers.xWrite(sq3File.pointer, zDbFile, 10, 1);
856        if(rc) toss("xWrite() failed!");
857        const readBuf = wasm.scopedAlloc(16);
858        rc = ioSyncWrappers.xRead(sq3File.pointer, readBuf, 6, 2);
859        wasm.setMemValue(readBuf+6,0);
860        let jRead = wasm.cstringToJs(readBuf);
861        log("xRead() got:",jRead);
862        if("sanity"!==jRead) toss("Unexpected xRead() value.");
863        if(vfsSyncWrappers.xSleep){
864          log("xSleep()ing before close()ing...");
865          vfsSyncWrappers.xSleep(opfsVfs.pointer,2000);
866          log("waking up from xSleep()");
867        }
868        rc = ioSyncWrappers.xClose(fid);
869        log("xClose rc =",rc,"sabOPView =",state.sabOPView);
870        log("Deleting file:",dbFile);
871        vfsSyncWrappers.xDelete(opfsVfs.pointer, zDbFile, 0x1234);
872        vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
873        rc = wasm.getMemValue(pOut,'i32');
874        if(rc) toss("Expecting 0 from xAccess(",dbFile,") after xDelete().");
875        warn("End of OPFS sanity checks.");
876      }finally{
877        sq3File.dispose();
878        wasm.scopedAllocPop(scope);
879      }
880    }/*sanityCheck()*/;
881
882
883    W.onmessage = function({data}){
884      //log("Worker.onmessage:",data);
885      switch(data.type){
886          case 'opfs-async-loaded':
887            /*Pass our config and shared state on to the async worker.*/
888            W.postMessage({type: 'opfs-async-init',args: state});
889            break;
890          case 'opfs-async-inited':{
891            /*Indicates that the async partner has received the 'init',
892              so we now know that the state object is no longer subject to
893              being copied by a pending postMessage() call.*/
894            try {
895              const rc = capi.sqlite3_vfs_register(opfsVfs.pointer, 0);
896              if(rc){
897                opfsVfs.dispose();
898                toss("sqlite3_vfs_register(OPFS) failed with rc",rc);
899              }
900              if(opfsVfs.pointer !== capi.sqlite3_vfs_find("opfs")){
901                toss("BUG: sqlite3_vfs_find() failed for just-installed OPFS VFS");
902              }
903              capi.sqlite3_vfs_register.addReference(opfsVfs, opfsIoMethods);
904              state.sabOPView = new Int32Array(state.sabOP);
905              state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
906              state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
907              initS11n();
908              if(options.sanityChecks){
909                warn("Running sanity checks because of opfs-sanity-check URL arg...");
910                sanityCheck();
911              }
912              W.onerror = W._originalOnError;
913              delete W._originalOnError;
914              sqlite3.opfs = opfsUtil;
915              log("End of OPFS sqlite3_vfs setup.", opfsVfs);
916              promiseResolve(sqlite3);
917            }catch(e){
918              error(e);
919              promiseReject(e);
920            }
921            break;
922          }
923          default:
924            promiseReject(e);
925            error("Unexpected message from the async worker:",data);
926            break;
927      }
928    };
929  })/*thePromise*/;
930  return thePromise;
931}/*installOpfsVfs()*/;
932sqlite3.installOpfsVfs.defaultProxyUri = "sqlite3-opfs-async-proxy.js";
933}/*sqlite3ApiBootstrap.initializers.push()*/);
934