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