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   sqlite3.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
36  Significant notes and limitations:
37
38  - As of this writing, OPFS is still very much in flux and only
39    available in bleeding-edge versions of Chrome (v102+, noting that
40    that number will increase as the OPFS API matures).
41
42  - The OPFS features used here are only available in dedicated Worker
43    threads. This file tries to detect that case, resulting in a
44    rejected Promise if those features do not seem to be available.
45
46  - It requires the SharedArrayBuffer and Atomics classes, and the
47    former is only available if the HTTP server emits the so-called
48    COOP and COEP response headers. These features are required for
49    proxying OPFS's synchronous API via the synchronous interface
50    required by the sqlite3_vfs API.
51
52  - This function may only be called a single time and it must be
53    called from the client, as opposed to the library initialization,
54    in case the client requires a custom path for this API's
55    "counterpart": this function's argument is the relative URI to
56    this module's "asynchronous half". When called, this function removes
57    itself from the sqlite3 object.
58
59   The argument may optionally be a plain object with the following
60   configuration options:
61
62   - proxyUri: as described above
63
64   - verbose (=2): an integer 0-3. 0 disables all logging, 1 enables
65     logging of errors. 2 enables logging of warnings and errors. 3
66     additionally enables debugging info.
67
68   - sanityChecks (=false): if true, some basic sanity tests are
69     run on the OPFS VFS API after it's initialized, before the
70     returned Promise resolves.
71
72   On success, the Promise resolves to the top-most sqlite3 namespace
73   object and that object gets a new object installed in its
74   `opfs` property, containing several OPFS-specific utilities.
75*/
76sqlite3.installOpfsVfs = function callee(asyncProxyUri = callee.defaultProxyUri){
77  delete sqlite3.installOpfsVfs;
78  if(!self.SharedArrayBuffer ||
79     !self.FileSystemHandle ||
80     !self.FileSystemDirectoryHandle ||
81     !self.FileSystemFileHandle ||
82     !self.FileSystemFileHandle.prototype.createSyncAccessHandle ||
83     !navigator.storage.getDirectory){
84    return Promise.reject(
85      new Error("This environment does not have OPFS support.")
86    );
87  }
88  const options = (asyncProxyUri && 'object'===asyncProxyUri) ? asyncProxyUri : {
89    proxyUri: asyncProxyUri
90  };
91  const urlParams = new URL(self.location.href).searchParams;
92  if(undefined===options.verbose){
93    options.verbose = urlParams.has('opfs-verbose') ? 3 : 2;
94  }
95  if(undefined===options.sanityChecks){
96    options.sanityChecks = urlParams.has('opfs-sanity-check');
97  }
98  if(undefined===options.proxyUri){
99    options.proxyUri = callee.defaultProxyUri;
100  }
101
102  const thePromise = new Promise(function(promiseResolve, promiseReject_){
103    const loggers = {
104      0:console.error.bind(console),
105      1:console.warn.bind(console),
106      2:console.log.bind(console)
107    };
108    const logImpl = (level,...args)=>{
109      if(options.verbose>level) loggers[level]("OPFS syncer:",...args);
110    };
111    const log =    (...args)=>logImpl(2, ...args);
112    const warn =   (...args)=>logImpl(1, ...args);
113    const error =  (...args)=>logImpl(0, ...args);
114    warn("The OPFS VFS feature is very much experimental and under construction.");
115    const toss = function(...args){throw new Error(args.join(' '))};
116    const capi = sqlite3.capi;
117    const wasm = capi.wasm;
118    const sqlite3_vfs = capi.sqlite3_vfs;
119    const sqlite3_file = capi.sqlite3_file;
120    const sqlite3_io_methods = capi.sqlite3_io_methods;
121    /**
122       Generic utilities for working with OPFS. This will get filled out
123       by the Promise setup and, on success, installed as sqlite3.opfs.
124    */
125    const opfsUtil = Object.create(null);
126    /**
127       Not part of the public API. Solely for internal/development
128       use.
129    */
130    opfsUtil.metrics = {
131      dump: function(){
132        let k, n = 0, t = 0, w = 0;
133        for(k in state.opIds){
134          const m = metrics[k];
135          n += m.count;
136          t += m.time;
137          w += m.wait;
138          m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
139          m.avgWait = (m.count && m.wait) ? (m.wait / m.count) : 0;
140        }
141        console.log(self.location.href,
142                    "metrics for",self.location.href,":",metrics,
143                    "\nTotal of",n,"op(s) for",t,
144                    "ms (incl. "+w+" ms of waiting on the async side)");
145        console.log("Serialization metrics:",metrics.s11n);
146        opRun('async-metrics');
147      },
148      reset: function(){
149        let k;
150        const r = (m)=>(m.count = m.time = m.wait = 0);
151        for(k in state.opIds){
152          r(metrics[k] = Object.create(null));
153        }
154        let s = metrics.s11n = Object.create(null);
155        s = s.serialize = Object.create(null);
156        s.count = s.time = 0;
157        s = metrics.s11n.deserialize = Object.create(null);
158        s.count = s.time = 0;
159        //[ // timed routines which are not in state.opIds
160        //  'xFileControl'
161        //].forEach((k)=>r(metrics[k] = Object.create(null)));
162      }
163    }/*metrics*/;
164    const promiseReject = function(err){
165      opfsVfs.dispose();
166      return promiseReject_(err);
167    };
168    const W = new Worker(options.proxyUri);
169    W._originalOnError = W.onerror /* will be restored later */;
170    W.onerror = function(err){
171      // The error object doesn't contain any useful info when the
172      // failure is, e.g., that the remote script is 404.
173      promiseReject(new Error("Loading OPFS async Worker failed for unknown reasons."));
174    };
175    const pDVfs = capi.sqlite3_vfs_find(null)/*pointer to default VFS*/;
176    const dVfs = pDVfs
177          ? new sqlite3_vfs(pDVfs)
178          : null /* dVfs will be null when sqlite3 is built with
179                    SQLITE_OS_OTHER. Though we cannot currently handle
180                    that case, the hope is to eventually be able to. */;
181    const opfsVfs = new sqlite3_vfs();
182    const opfsIoMethods = new sqlite3_io_methods();
183    opfsVfs.$iVersion = 2/*yes, two*/;
184    opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof;
185    opfsVfs.$mxPathname = 1024/*sure, why not?*/;
186    opfsVfs.$zName = wasm.allocCString("opfs");
187    // All C-side memory of opfsVfs is zeroed out, but just to be explicit:
188    opfsVfs.$xDlOpen = opfsVfs.$xDlError = opfsVfs.$xDlSym = opfsVfs.$xDlClose = null;
189    opfsVfs.ondispose = [
190      '$zName', opfsVfs.$zName,
191      'cleanup default VFS wrapper', ()=>(dVfs ? dVfs.dispose() : null),
192      'cleanup opfsIoMethods', ()=>opfsIoMethods.dispose()
193    ];
194    /**
195       Pedantic sidebar about opfsVfs.ondispose: the entries in that array
196       are items to clean up when opfsVfs.dispose() is called, but in this
197       environment it will never be called. The VFS instance simply
198       hangs around until the WASM module instance is cleaned up. We
199       "could" _hypothetically_ clean it up by "importing" an
200       sqlite3_os_end() impl into the wasm build, but the shutdown order
201       of the wasm engine and the JS one are undefined so there is no
202       guaranty that the opfsVfs instance would be available in one
203       environment or the other when sqlite3_os_end() is called (_if_ it
204       gets called at all in a wasm build, which is undefined).
205    */
206
207    /**
208       State which we send to the async-api Worker or share with it.
209       This object must initially contain only cloneable or sharable
210       objects. After the worker's "inited" message arrives, other types
211       of data may be added to it.
212
213       For purposes of Atomics.wait() and Atomics.notify(), we use a
214       SharedArrayBuffer with one slot reserved for each of the API
215       proxy's methods. The sync side of the API uses Atomics.wait()
216       on the corresponding slot and the async side uses
217       Atomics.notify() on that slot.
218
219       The approach of using a single SAB to serialize comms for all
220       instances might(?) lead to deadlock situations in multi-db
221       cases. We should probably have one SAB here with a single slot
222       for locking a per-file initialization step and then allocate a
223       separate SAB like the above one for each file. That will
224       require a bit of acrobatics but should be feasible.
225    */
226    const state = Object.create(null);
227    state.verbose = options.verbose;
228    state.littleEndian = true;
229    /** Whether the async counterpart should log exceptions to
230        the serialization channel. That produces a great deal of
231        noise for seemingly innocuous things like xAccess() checks
232        for missing files, so this option may have one of 3 values:
233
234        0 = no exception logging
235
236        1 = only log exceptions for "significant" ops like xOpen(),
237        xRead(), and xWrite().
238
239        2 = log all exceptions.
240    */
241    state.asyncS11nExceptions = 1;
242    /* Size of file I/O buffer block. 64k = max sqlite3 page size. */
243    state.fileBufferSize =
244      1024 * 64;
245    state.sabS11nOffset = state.fileBufferSize;
246    /**
247       The size of the block in our SAB for serializing arguments and
248       result values. Needs to be large enough to hold serialized
249       values of any of the proxied APIs. Filenames are the largest
250       part but are limited to opfsVfs.$mxPathname bytes.
251    */
252    state.sabS11nSize = opfsVfs.$mxPathname * 2;
253    /**
254       The SAB used for all data I/O (files and arg/result s11n).
255    */
256    state.sabIO = new SharedArrayBuffer(
257      state.fileBufferSize/* file i/o block */
258      + state.sabS11nSize/* argument/result serialization block */
259    );
260    state.opIds = Object.create(null);
261    const metrics = Object.create(null);
262    {
263      /* Indexes for use in our SharedArrayBuffer... */
264      let i = 0;
265      /* SAB slot used to communicate which operation is desired
266         between both workers. This worker writes to it and the other
267         listens for changes. */
268      state.opIds.whichOp = i++;
269      /* Slot for storing return values. This work listens to that
270         slot and the other worker writes to it. */
271      state.opIds.rc = i++;
272      /* Each function gets an ID which this worker writes to
273         the whichOp slot. The async-api worker uses Atomic.wait()
274         on the whichOp slot to figure out which operation to run
275         next. */
276      state.opIds.xAccess = i++;
277      state.opIds.xClose = i++;
278      state.opIds.xDelete = i++;
279      state.opIds.xDeleteNoWait = i++;
280      state.opIds.xFileControl = i++;
281      state.opIds.xFileSize = i++;
282      state.opIds.xOpen = i++;
283      state.opIds.xRead = i++;
284      state.opIds.xSleep = i++;
285      state.opIds.xSync = i++;
286      state.opIds.xTruncate = i++;
287      state.opIds.xWrite = i++;
288      state.opIds.mkdir = i++;
289      state.opIds['async-metrics'] = 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    //TODO to support fiddle db upload:
893    //opfsUtil.createFile = function(absName, content=undefined){...}
894
895    if(sqlite3.oo1){
896      opfsUtil.OpfsDb = function(...args){
897        const opt = sqlite3.oo1.dbCtorHelper.normalizeArgs(...args);
898        opt.vfs = opfsVfs.$zName;
899        sqlite3.oo1.dbCtorHelper.call(this, opt);
900      };
901      opfsUtil.OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype);
902    }
903
904    /**
905       Potential TODOs:
906
907       - Expose one or both of the Worker objects via opfsUtil and
908         publish an interface for proxying the higher-level OPFS
909         features like getting a directory listing.
910    */
911
912    const sanityCheck = function(){
913      const scope = wasm.scopedAllocPush();
914      const sq3File = new sqlite3_file();
915      try{
916        const fid = sq3File.pointer;
917        const openFlags = capi.SQLITE_OPEN_CREATE
918              | capi.SQLITE_OPEN_READWRITE
919        //| capi.SQLITE_OPEN_DELETEONCLOSE
920              | capi.SQLITE_OPEN_MAIN_DB;
921        const pOut = wasm.scopedAlloc(8);
922        const dbFile = "/sanity/check/file"+randomFilename(8);
923        const zDbFile = wasm.scopedAllocCString(dbFile);
924        let rc;
925        state.s11n.serialize("This is ä string.");
926        rc = state.s11n.deserialize();
927        log("deserialize() says:",rc);
928        if("This is ä string."!==rc[0]) toss("String d13n error.");
929        vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
930        rc = wasm.getMemValue(pOut,'i32');
931        log("xAccess(",dbFile,") exists ?=",rc);
932        rc = vfsSyncWrappers.xOpen(opfsVfs.pointer, zDbFile,
933                                   fid, openFlags, pOut);
934        log("open rc =",rc,"state.sabOPView[xOpen] =",
935            state.sabOPView[state.opIds.xOpen]);
936        if(0!==rc){
937          error("open failed with code",rc);
938          return;
939        }
940        vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
941        rc = wasm.getMemValue(pOut,'i32');
942        if(!rc) toss("xAccess() failed to detect file.");
943        rc = ioSyncWrappers.xSync(sq3File.pointer, 0);
944        if(rc) toss('sync failed w/ rc',rc);
945        rc = ioSyncWrappers.xTruncate(sq3File.pointer, 1024);
946        if(rc) toss('truncate failed w/ rc',rc);
947        wasm.setMemValue(pOut,0,'i64');
948        rc = ioSyncWrappers.xFileSize(sq3File.pointer, pOut);
949        if(rc) toss('xFileSize failed w/ rc',rc);
950        log("xFileSize says:",wasm.getMemValue(pOut, 'i64'));
951        rc = ioSyncWrappers.xWrite(sq3File.pointer, zDbFile, 10, 1);
952        if(rc) toss("xWrite() failed!");
953        const readBuf = wasm.scopedAlloc(16);
954        rc = ioSyncWrappers.xRead(sq3File.pointer, readBuf, 6, 2);
955        wasm.setMemValue(readBuf+6,0);
956        let jRead = wasm.cstringToJs(readBuf);
957        log("xRead() got:",jRead);
958        if("sanity"!==jRead) toss("Unexpected xRead() value.");
959        if(vfsSyncWrappers.xSleep){
960          log("xSleep()ing before close()ing...");
961          vfsSyncWrappers.xSleep(opfsVfs.pointer,2000);
962          log("waking up from xSleep()");
963        }
964        rc = ioSyncWrappers.xClose(fid);
965        log("xClose rc =",rc,"sabOPView =",state.sabOPView);
966        log("Deleting file:",dbFile);
967        vfsSyncWrappers.xDelete(opfsVfs.pointer, zDbFile, 0x1234);
968        vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
969        rc = wasm.getMemValue(pOut,'i32');
970        if(rc) toss("Expecting 0 from xAccess(",dbFile,") after xDelete().");
971        warn("End of OPFS sanity checks.");
972      }finally{
973        sq3File.dispose();
974        wasm.scopedAllocPop(scope);
975      }
976    }/*sanityCheck()*/;
977
978    W.onmessage = function({data}){
979      //log("Worker.onmessage:",data);
980      switch(data.type){
981          case 'opfs-async-loaded':
982            /*Arrives as soon as the asyc proxy finishes loading.
983              Pass our config and shared state on to the async worker.*/
984            W.postMessage({type: 'opfs-async-init',args: state});
985            break;
986          case 'opfs-async-inited':{
987            /*Indicates that the async partner has received the 'init'
988              and has finished initializing, so the real work can
989              begin...*/
990            try {
991              const rc = capi.sqlite3_vfs_register(opfsVfs.pointer, 0);
992              if(rc){
993                toss("sqlite3_vfs_register(OPFS) failed with rc",rc);
994              }
995              if(opfsVfs.pointer !== capi.sqlite3_vfs_find("opfs")){
996                toss("BUG: sqlite3_vfs_find() failed for just-installed OPFS VFS");
997              }
998              capi.sqlite3_vfs_register.addReference(opfsVfs, opfsIoMethods);
999              state.sabOPView = new Int32Array(state.sabOP);
1000              state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
1001              state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
1002              initS11n();
1003              if(options.sanityChecks){
1004                warn("Running sanity checks because of opfs-sanity-check URL arg...");
1005                sanityCheck();
1006              }
1007              navigator.storage.getDirectory().then((d)=>{
1008                W.onerror = W._originalOnError;
1009                delete W._originalOnError;
1010                sqlite3.opfs = opfsUtil;
1011                opfsUtil.rootDirectory = d;
1012                log("End of OPFS sqlite3_vfs setup.", opfsVfs);
1013                promiseResolve(sqlite3);
1014              });
1015            }catch(e){
1016              error(e);
1017              promiseReject(e);
1018            }
1019            break;
1020          }
1021          default:
1022            promiseReject(e);
1023            error("Unexpected message from the async worker:",data);
1024            break;
1025      }
1026    };
1027  })/*thePromise*/;
1028  return thePromise;
1029}/*installOpfsVfs()*/;
1030sqlite3.installOpfsVfs.defaultProxyUri = "sqlite3-opfs-async-proxy.js";
1031}/*sqlite3ApiBootstrap.initializers.push()*/);
1032