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, BigInt(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    let inst = installMethod(opfsIoMethods);
824    for(let k of Object.keys(ioSyncWrappers)) inst(k, ioSyncWrappers[k]);
825    inst = installMethod(opfsVfs);
826    for(let k of Object.keys(vfsSyncWrappers)) inst(k, vfsSyncWrappers[k]);
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 fails 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.reregisterVfs = (asDefault=false)=>{
885      return capi.wasm.exports.sqlite3_vfs_register(
886        opfsVfs.pointer, asDefault ? 1 : 0
887      );
888    };
889
890    if(sqlite3.oo1){
891      opfsUtil.OpfsDb = function(...args){
892        const opt = sqlite3.oo1.dbCtorHelper.normalizeArgs(...args);
893        opt.vfs = opfsVfs.$zName;
894        sqlite3.oo1.dbCtorHelper.call(this, opt);
895      };
896      opfsUtil.OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype);
897    }
898
899    /**
900       Potential TODOs:
901
902       - Expose one or both of the Worker objects via opfsUtil and
903         publish an interface for proxying the higher-level OPFS
904         features like getting a directory listing.
905    */
906
907    const sanityCheck = function(){
908      const scope = wasm.scopedAllocPush();
909      const sq3File = new sqlite3_file();
910      try{
911        const fid = sq3File.pointer;
912        const openFlags = capi.SQLITE_OPEN_CREATE
913              | capi.SQLITE_OPEN_READWRITE
914        //| capi.SQLITE_OPEN_DELETEONCLOSE
915              | capi.SQLITE_OPEN_MAIN_DB;
916        const pOut = wasm.scopedAlloc(8);
917        const dbFile = "/sanity/check/file"+randomFilename(8);
918        const zDbFile = wasm.scopedAllocCString(dbFile);
919        let rc;
920        state.s11n.serialize("This is ä string.");
921        rc = state.s11n.deserialize();
922        log("deserialize() says:",rc);
923        if("This is ä string."!==rc[0]) toss("String d13n error.");
924        vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
925        rc = wasm.getMemValue(pOut,'i32');
926        log("xAccess(",dbFile,") exists ?=",rc);
927        rc = vfsSyncWrappers.xOpen(opfsVfs.pointer, zDbFile,
928                                   fid, openFlags, pOut);
929        log("open rc =",rc,"state.sabOPView[xOpen] =",
930            state.sabOPView[state.opIds.xOpen]);
931        if(0!==rc){
932          error("open failed with code",rc);
933          return;
934        }
935        vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
936        rc = wasm.getMemValue(pOut,'i32');
937        if(!rc) toss("xAccess() failed to detect file.");
938        rc = ioSyncWrappers.xSync(sq3File.pointer, 0);
939        if(rc) toss('sync failed w/ rc',rc);
940        rc = ioSyncWrappers.xTruncate(sq3File.pointer, 1024);
941        if(rc) toss('truncate failed w/ rc',rc);
942        wasm.setMemValue(pOut,0,'i64');
943        rc = ioSyncWrappers.xFileSize(sq3File.pointer, pOut);
944        if(rc) toss('xFileSize failed w/ rc',rc);
945        log("xFileSize says:",wasm.getMemValue(pOut, 'i64'));
946        rc = ioSyncWrappers.xWrite(sq3File.pointer, zDbFile, 10, 1);
947        if(rc) toss("xWrite() failed!");
948        const readBuf = wasm.scopedAlloc(16);
949        rc = ioSyncWrappers.xRead(sq3File.pointer, readBuf, 6, 2);
950        wasm.setMemValue(readBuf+6,0);
951        let jRead = wasm.cstringToJs(readBuf);
952        log("xRead() got:",jRead);
953        if("sanity"!==jRead) toss("Unexpected xRead() value.");
954        if(vfsSyncWrappers.xSleep){
955          log("xSleep()ing before close()ing...");
956          vfsSyncWrappers.xSleep(opfsVfs.pointer,2000);
957          log("waking up from xSleep()");
958        }
959        rc = ioSyncWrappers.xClose(fid);
960        log("xClose rc =",rc,"sabOPView =",state.sabOPView);
961        log("Deleting file:",dbFile);
962        vfsSyncWrappers.xDelete(opfsVfs.pointer, zDbFile, 0x1234);
963        vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
964        rc = wasm.getMemValue(pOut,'i32');
965        if(rc) toss("Expecting 0 from xAccess(",dbFile,") after xDelete().");
966        warn("End of OPFS sanity checks.");
967      }finally{
968        sq3File.dispose();
969        wasm.scopedAllocPop(scope);
970      }
971    }/*sanityCheck()*/;
972
973    W.onmessage = function({data}){
974      //log("Worker.onmessage:",data);
975      switch(data.type){
976          case 'opfs-async-loaded':
977            /*Arrives as soon as the asyc proxy finishes loading.
978              Pass our config and shared state on to the async worker.*/
979            W.postMessage({type: 'opfs-async-init',args: state});
980            break;
981          case 'opfs-async-inited':{
982            /*Indicates that the async partner has received the 'init'
983              and has finished initializing, so the real work can
984              begin...*/
985            try {
986              const rc = capi.sqlite3_vfs_register(opfsVfs.pointer, 0);
987              if(rc){
988                toss("sqlite3_vfs_register(OPFS) failed with rc",rc);
989              }
990              if(opfsVfs.pointer !== capi.sqlite3_vfs_find("opfs")){
991                toss("BUG: sqlite3_vfs_find() failed for just-installed OPFS VFS");
992              }
993              capi.sqlite3_vfs_register.addReference(opfsVfs, opfsIoMethods);
994              state.sabOPView = new Int32Array(state.sabOP);
995              state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
996              state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
997              initS11n();
998              if(options.sanityChecks){
999                warn("Running sanity checks because of opfs-sanity-check URL arg...");
1000                sanityCheck();
1001              }
1002              W.onerror = W._originalOnError;
1003              delete W._originalOnError;
1004              sqlite3.opfs = opfsUtil;
1005              log("End of OPFS sqlite3_vfs setup.", opfsVfs);
1006              promiseResolve(sqlite3);
1007            }catch(e){
1008              error(e);
1009              promiseReject(e);
1010            }
1011            break;
1012          }
1013          default:
1014            promiseReject(e);
1015            error("Unexpected message from the async worker:",data);
1016            break;
1017      }
1018    };
1019  })/*thePromise*/;
1020  return thePromise;
1021}/*installOpfsVfs()*/;
1022sqlite3.installOpfsVfs.defaultProxyUri = "sqlite3-opfs-async-proxy.js";
1023}/*sqlite3ApiBootstrap.initializers.push()*/);
1024