13961b263Sstephan/*
2c5313afeSstephan  2022-09-18
33961b263Sstephan
43961b263Sstephan  The author disclaims copyright to this source code.  In place of a
53961b263Sstephan  legal notice, here is a blessing:
63961b263Sstephan
73961b263Sstephan  *   May you do good and not evil.
83961b263Sstephan  *   May you find forgiveness for yourself and forgive others.
93961b263Sstephan  *   May you share freely, never taking more than you give.
103961b263Sstephan
113961b263Sstephan  ***********************************************************************
123961b263Sstephan
13c5313afeSstephan  This file holds the synchronous half of an sqlite3_vfs
14c5313afeSstephan  implementation which proxies, in a synchronous fashion, the
15c5313afeSstephan  asynchronous Origin-Private FileSystem (OPFS) APIs using a second
16c5313afeSstephan  Worker, implemented in sqlite3-opfs-async-proxy.js.  This file is
17c5313afeSstephan  intended to be appended to the main sqlite3 JS deliverable somewhere
18f861b36bSstephan  after sqlite3-api-oo1.js and before sqlite3-api-cleanup.js.
19c5313afeSstephan*/
20c5313afeSstephan'use strict';
21c5313afeSstephanself.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
22c5313afeSstephan/**
23f861b36bSstephan   installOpfsVfs() returns a Promise which, on success, installs an
24f861b36bSstephan   sqlite3_vfs named "opfs", suitable for use with all sqlite3 APIs
25f861b36bSstephan   which accept a VFS. It is intended to be called via
26f861b36bSstephan   sqlite3ApiBootstrap.initializersAsync or an equivalent mechanism.
27f861b36bSstephan
28f861b36bSstephan   The installed VFS uses the Origin-Private FileSystem API for
29c5313afeSstephan   all file storage. On error it is rejected with an exception
30c5313afeSstephan   explaining the problem. Reasons for rejection include, but are
31c5313afeSstephan   not limited to:
32c5313afeSstephan
33c5313afeSstephan   - The counterpart Worker (see below) could not be loaded.
34c5313afeSstephan
35c5313afeSstephan   - The environment does not support OPFS. That includes when
36c5313afeSstephan     this function is called from the main window thread.
37c5313afeSstephan
383961b263Sstephan  Significant notes and limitations:
393961b263Sstephan
403961b263Sstephan  - As of this writing, OPFS is still very much in flux and only
413961b263Sstephan    available in bleeding-edge versions of Chrome (v102+, noting that
423961b263Sstephan    that number will increase as the OPFS API matures).
433961b263Sstephan
44c5313afeSstephan  - The OPFS features used here are only available in dedicated Worker
45f3860120Sstephan    threads. This file tries to detect that case, resulting in a
46f3860120Sstephan    rejected Promise if those features do not seem to be available.
473961b263Sstephan
48c5313afeSstephan  - It requires the SharedArrayBuffer and Atomics classes, and the
49c5313afeSstephan    former is only available if the HTTP server emits the so-called
50c5313afeSstephan    COOP and COEP response headers. These features are required for
51c5313afeSstephan    proxying OPFS's synchronous API via the synchronous interface
52c5313afeSstephan    required by the sqlite3_vfs API.
53c5313afeSstephan
54f861b36bSstephan  - This function may only be called a single time. When called, this
55f861b36bSstephan    function removes itself from the sqlite3 object.
56f861b36bSstephan
57f861b36bSstephan  All arguments to this function are for internal/development purposes
58f861b36bSstephan  only. They do not constitute a public API and may change at any
59f861b36bSstephan  time.
60c5313afeSstephan
61c5313afeSstephan  The argument may optionally be a plain object with the following
62c5313afeSstephan  configuration options:
63c5313afeSstephan
64c5313afeSstephan  - proxyUri: as described above
65c5313afeSstephan
66c5313afeSstephan  - verbose (=2): an integer 0-3. 0 disables all logging, 1 enables
67c5313afeSstephan    logging of errors. 2 enables logging of warnings and errors. 3
68c5313afeSstephan    additionally enables debugging info.
69c5313afeSstephan
70c5313afeSstephan  - sanityChecks (=false): if true, some basic sanity tests are
71c5313afeSstephan    run on the OPFS VFS API after it's initialized, before the
72c5313afeSstephan    returned Promise resolves.
73c5313afeSstephan
74c5313afeSstephan  On success, the Promise resolves to the top-most sqlite3 namespace
75f3860120Sstephan  object and that object gets a new object installed in its
76f3860120Sstephan  `opfs` property, containing several OPFS-specific utilities.
77c5313afeSstephan*/
78f861b36bSstephanconst installOpfsVfs = function callee(options){
794cffb644Sstephan  if(!self.SharedArrayBuffer ||
80f6c686c9Sstephan     !self.Atomics ||
81c5313afeSstephan     !self.FileSystemHandle ||
82c5313afeSstephan     !self.FileSystemDirectoryHandle ||
83c5313afeSstephan     !self.FileSystemFileHandle ||
84c5313afeSstephan     !self.FileSystemFileHandle.prototype.createSyncAccessHandle ||
85c5313afeSstephan     !navigator.storage.getDirectory){
86509f4052Sstephan    return Promise.reject(
87509f4052Sstephan      new Error("This environment does not have OPFS support.")
88509f4052Sstephan    );
893961b263Sstephan  }
90f861b36bSstephan  if(!options || 'object'!==typeof options){
91f861b36bSstephan    options = Object.create(null);
92f861b36bSstephan  }
93509f4052Sstephan  const urlParams = new URL(self.location.href).searchParams;
94509f4052Sstephan  if(undefined===options.verbose){
95509f4052Sstephan    options.verbose = urlParams.has('opfs-verbose') ? 3 : 2;
96509f4052Sstephan  }
97509f4052Sstephan  if(undefined===options.sanityChecks){
98509f4052Sstephan    options.sanityChecks = urlParams.has('opfs-sanity-check');
99509f4052Sstephan  }
100509f4052Sstephan  if(undefined===options.proxyUri){
101509f4052Sstephan    options.proxyUri = callee.defaultProxyUri;
102509f4052Sstephan  }
103509f4052Sstephan
104cd0df83cSstephan  if('function' === typeof options.proxyUri){
105cd0df83cSstephan    options.proxyUri = options.proxyUri();
106cd0df83cSstephan  }
107e8afca3fSstephan  const thePromise = new Promise(function(promiseResolve, promiseReject_){
108509f4052Sstephan    const loggers = {
109509f4052Sstephan      0:console.error.bind(console),
110509f4052Sstephan      1:console.warn.bind(console),
111509f4052Sstephan      2:console.log.bind(console)
112509f4052Sstephan    };
113509f4052Sstephan    const logImpl = (level,...args)=>{
114509f4052Sstephan      if(options.verbose>level) loggers[level]("OPFS syncer:",...args);
115509f4052Sstephan    };
116509f4052Sstephan    const log =    (...args)=>logImpl(2, ...args);
117509f4052Sstephan    const warn =   (...args)=>logImpl(1, ...args);
118509f4052Sstephan    const error =  (...args)=>logImpl(0, ...args);
119c5313afeSstephan    const toss = function(...args){throw new Error(args.join(' '))};
120c5313afeSstephan    const capi = sqlite3.capi;
1218948fbeeSstephan    const wasm = sqlite3.wasm;
122c5313afeSstephan    const sqlite3_vfs = capi.sqlite3_vfs;
123c5313afeSstephan    const sqlite3_file = capi.sqlite3_file;
124c5313afeSstephan    const sqlite3_io_methods = capi.sqlite3_io_methods;
125509f4052Sstephan    /**
126509f4052Sstephan       Generic utilities for working with OPFS. This will get filled out
127509f4052Sstephan       by the Promise setup and, on success, installed as sqlite3.opfs.
128509f4052Sstephan    */
129509f4052Sstephan    const opfsUtil = Object.create(null);
130f815011aSstephan    /**
131f815011aSstephan       Not part of the public API. Solely for internal/development
132f815011aSstephan       use.
133f815011aSstephan    */
134f815011aSstephan    opfsUtil.metrics = {
135f815011aSstephan      dump: function(){
136aec046a2Sstephan        let k, n = 0, t = 0, w = 0;
137aec046a2Sstephan        for(k in state.opIds){
138f815011aSstephan          const m = metrics[k];
139f815011aSstephan          n += m.count;
140f815011aSstephan          t += m.time;
141aec046a2Sstephan          w += m.wait;
142f815011aSstephan          m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
143f815011aSstephan          m.avgWait = (m.count && m.wait) ? (m.wait / m.count) : 0;
144f815011aSstephan        }
145aec046a2Sstephan        console.log(self.location.href,
146aec046a2Sstephan                    "metrics for",self.location.href,":",metrics,
147aec046a2Sstephan                    "\nTotal of",n,"op(s) for",t,
148aec046a2Sstephan                    "ms (incl. "+w+" ms of waiting on the async side)");
14956fae744Sstephan        console.log("Serialization metrics:",metrics.s11n);
1503c272ba3Sstephan        W.postMessage({type:'opfs-async-metrics'});
151f815011aSstephan      },
152f815011aSstephan      reset: function(){
153f815011aSstephan        let k;
154f815011aSstephan        const r = (m)=>(m.count = m.time = m.wait = 0);
155f815011aSstephan        for(k in state.opIds){
156f815011aSstephan          r(metrics[k] = Object.create(null));
157f815011aSstephan        }
158b8c8d4e4Sstephan        let s = metrics.s11n = Object.create(null);
159b8c8d4e4Sstephan        s = s.serialize = Object.create(null);
160b8c8d4e4Sstephan        s.count = s.time = 0;
161b8c8d4e4Sstephan        s = metrics.s11n.deserialize = Object.create(null);
162b8c8d4e4Sstephan        s.count = s.time = 0;
163f815011aSstephan      }
164f815011aSstephan    }/*metrics*/;
16556fae744Sstephan    const promiseReject = function(err){
16656fae744Sstephan      opfsVfs.dispose();
16756fae744Sstephan      return promiseReject_(err);
16856fae744Sstephan    };
16956fae744Sstephan    const W = new Worker(options.proxyUri);
17056fae744Sstephan    W._originalOnError = W.onerror /* will be restored later */;
17156fae744Sstephan    W.onerror = function(err){
17256fae744Sstephan      // The error object doesn't contain any useful info when the
17356fae744Sstephan      // failure is, e.g., that the remote script is 404.
1749a55773bSstephan      error("Error initializing OPFS asyncer:",err);
17556fae744Sstephan      promiseReject(new Error("Loading OPFS async Worker failed for unknown reasons."));
17656fae744Sstephan    };
177c9e2602eSstephan    const pDVfs = capi.sqlite3_vfs_find(null)/*pointer to default VFS*/;
178c9e2602eSstephan    const dVfs = pDVfs
179c9e2602eSstephan          ? new sqlite3_vfs(pDVfs)
180c9e2602eSstephan          : null /* dVfs will be null when sqlite3 is built with
181c9e2602eSstephan                    SQLITE_OS_OTHER. Though we cannot currently handle
182c9e2602eSstephan                    that case, the hope is to eventually be able to. */;
183c9e2602eSstephan    const opfsVfs = new sqlite3_vfs();
184c9e2602eSstephan    const opfsIoMethods = new sqlite3_io_methods();
185c9e2602eSstephan    opfsVfs.$iVersion = 2/*yes, two*/;
186c9e2602eSstephan    opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof;
187c9e2602eSstephan    opfsVfs.$mxPathname = 1024/*sure, why not?*/;
188c9e2602eSstephan    opfsVfs.$zName = wasm.allocCString("opfs");
189c9e2602eSstephan    // All C-side memory of opfsVfs is zeroed out, but just to be explicit:
190c9e2602eSstephan    opfsVfs.$xDlOpen = opfsVfs.$xDlError = opfsVfs.$xDlSym = opfsVfs.$xDlClose = null;
191c9e2602eSstephan    opfsVfs.ondispose = [
192c9e2602eSstephan      '$zName', opfsVfs.$zName,
193c9e2602eSstephan      'cleanup default VFS wrapper', ()=>(dVfs ? dVfs.dispose() : null),
194c9e2602eSstephan      'cleanup opfsIoMethods', ()=>opfsIoMethods.dispose()
195c9e2602eSstephan    ];
196c9e2602eSstephan    /**
197c9e2602eSstephan       Pedantic sidebar about opfsVfs.ondispose: the entries in that array
198c9e2602eSstephan       are items to clean up when opfsVfs.dispose() is called, but in this
199c9e2602eSstephan       environment it will never be called. The VFS instance simply
200c9e2602eSstephan       hangs around until the WASM module instance is cleaned up. We
201c9e2602eSstephan       "could" _hypothetically_ clean it up by "importing" an
202c9e2602eSstephan       sqlite3_os_end() impl into the wasm build, but the shutdown order
203c9e2602eSstephan       of the wasm engine and the JS one are undefined so there is no
204c9e2602eSstephan       guaranty that the opfsVfs instance would be available in one
205c9e2602eSstephan       environment or the other when sqlite3_os_end() is called (_if_ it
206c9e2602eSstephan       gets called at all in a wasm build, which is undefined).
207c9e2602eSstephan    */
208c5313afeSstephan    /**
209c5313afeSstephan       State which we send to the async-api Worker or share with it.
210c5313afeSstephan       This object must initially contain only cloneable or sharable
211c5313afeSstephan       objects. After the worker's "inited" message arrives, other types
212c5313afeSstephan       of data may be added to it.
213f3860120Sstephan
214f3860120Sstephan       For purposes of Atomics.wait() and Atomics.notify(), we use a
215f3860120Sstephan       SharedArrayBuffer with one slot reserved for each of the API
216f3860120Sstephan       proxy's methods. The sync side of the API uses Atomics.wait()
217f3860120Sstephan       on the corresponding slot and the async side uses
218f3860120Sstephan       Atomics.notify() on that slot.
219f3860120Sstephan
220f3860120Sstephan       The approach of using a single SAB to serialize comms for all
221f3860120Sstephan       instances might(?) lead to deadlock situations in multi-db
222f3860120Sstephan       cases. We should probably have one SAB here with a single slot
223f3860120Sstephan       for locking a per-file initialization step and then allocate a
224f3860120Sstephan       separate SAB like the above one for each file. That will
225f861b36bSstephan       require a bit of acrobatics but should be feasible. The most
226f861b36bSstephan       problematic part is that xOpen() would have to use
227f861b36bSstephan       postMessage() to communicate its SharedArrayBuffer, and mixing
228f861b36bSstephan       that approach with Atomics.wait/notify() gets a bit messy.
229c5313afeSstephan    */
230c5313afeSstephan    const state = Object.create(null);
231c5313afeSstephan    state.verbose = options.verbose;
2329a55773bSstephan    state.littleEndian = (()=>{
2339a55773bSstephan      const buffer = new ArrayBuffer(2);
234f861b36bSstephan      new DataView(buffer).setInt16(0, 256, true /* ==>littleEndian */);
2359a55773bSstephan      // Int16Array uses the platform's endianness.
2369a55773bSstephan      return new Int16Array(buffer)[0] === 256;
2379a55773bSstephan    })();
238f861b36bSstephan    /**
239f861b36bSstephan       Whether the async counterpart should log exceptions to
240e8afca3fSstephan       the serialization channel. That produces a great deal of
241e8afca3fSstephan       noise for seemingly innocuous things like xAccess() checks
24256fae744Sstephan       for missing files, so this option may have one of 3 values:
24356fae744Sstephan
24456fae744Sstephan       0 = no exception logging
24556fae744Sstephan
24656fae744Sstephan       1 = only log exceptions for "significant" ops like xOpen(),
24756fae744Sstephan       xRead(), and xWrite().
24856fae744Sstephan
24956fae744Sstephan       2 = log all exceptions.
25056fae744Sstephan    */
25156fae744Sstephan    state.asyncS11nExceptions = 1;
252f861b36bSstephan    /* Size of file I/O buffer block. 64k = max sqlite3 page size, and
253f861b36bSstephan       xRead/xWrite() will never deal in blocks larger than that. */
254f861b36bSstephan    state.fileBufferSize = 1024 * 64;
255138647a5Sstephan    state.sabS11nOffset = state.fileBufferSize;
256c9e2602eSstephan    /**
257c9e2602eSstephan       The size of the block in our SAB for serializing arguments and
258e8afca3fSstephan       result values. Needs to be large enough to hold serialized
259c9e2602eSstephan       values of any of the proxied APIs. Filenames are the largest
260c9e2602eSstephan       part but are limited to opfsVfs.$mxPathname bytes.
261c9e2602eSstephan    */
262c9e2602eSstephan    state.sabS11nSize = opfsVfs.$mxPathname * 2;
263c9e2602eSstephan    /**
264f861b36bSstephan       The SAB used for all data I/O between the synchronous and
265f861b36bSstephan       async halves (file i/o and arg/result s11n).
266c9e2602eSstephan    */
267c4b87be3Sstephan    state.sabIO = new SharedArrayBuffer(
268c9e2602eSstephan      state.fileBufferSize/* file i/o block */
269c9e2602eSstephan      + state.sabS11nSize/* argument/result serialization block */
270c4b87be3Sstephan    );
271c5313afeSstephan    state.opIds = Object.create(null);
272f815011aSstephan    const metrics = Object.create(null);
273c5313afeSstephan    {
274c9e2602eSstephan      /* Indexes for use in our SharedArrayBuffer... */
275c5313afeSstephan      let i = 0;
276c9e2602eSstephan      /* SAB slot used to communicate which operation is desired
277c9e2602eSstephan         between both workers. This worker writes to it and the other
278c9e2602eSstephan         listens for changes. */
279138647a5Sstephan      state.opIds.whichOp = i++;
2809a55773bSstephan      /* Slot for storing return values. This worker listens to that
281c9e2602eSstephan         slot and the other worker writes to it. */
282c9e2602eSstephan      state.opIds.rc = i++;
283c9e2602eSstephan      /* Each function gets an ID which this worker writes to
284c9e2602eSstephan         the whichOp slot. The async-api worker uses Atomic.wait()
285c9e2602eSstephan         on the whichOp slot to figure out which operation to run
286c9e2602eSstephan         next. */
287c5313afeSstephan      state.opIds.xAccess = i++;
288c5313afeSstephan      state.opIds.xClose = i++;
289c5313afeSstephan      state.opIds.xDelete = i++;
290f3860120Sstephan      state.opIds.xDeleteNoWait = i++;
29156fae744Sstephan      state.opIds.xFileControl = i++;
292c5313afeSstephan      state.opIds.xFileSize = i++;
2939a55773bSstephan      state.opIds.xLock = i++;
294c5313afeSstephan      state.opIds.xOpen = i++;
295c5313afeSstephan      state.opIds.xRead = i++;
296c5313afeSstephan      state.opIds.xSleep = i++;
297c5313afeSstephan      state.opIds.xSync = i++;
298c5313afeSstephan      state.opIds.xTruncate = i++;
2999a55773bSstephan      state.opIds.xUnlock = i++;
300c5313afeSstephan      state.opIds.xWrite = i++;
301f3860120Sstephan      state.opIds.mkdir = i++;
3023c272ba3Sstephan      state.opIds['opfs-async-metrics'] = i++;
3033c272ba3Sstephan      state.opIds['opfs-async-shutdown'] = i++;
304f861b36bSstephan      /* The retry slot is used by the async part for wait-and-retry
305f861b36bSstephan         semantics. Though we could hypothetically use the xSleep slot
306f861b36bSstephan         for that, doing so might lead to undesired side effects. */
307f861b36bSstephan      state.opIds.retry = i++;
308f861b36bSstephan      state.sabOP = new SharedArrayBuffer(
309f861b36bSstephan        i * 4/* ==sizeof int32, noting that Atomics.wait() and friends
310f861b36bSstephan                can only function on Int32Array views of an SAB. */);
311f815011aSstephan      opfsUtil.metrics.reset();
312c5313afeSstephan    }
313c9e2602eSstephan    /**
314c9e2602eSstephan       SQLITE_xxx constants to export to the async worker
315c9e2602eSstephan       counterpart...
316c9e2602eSstephan    */
317c5313afeSstephan    state.sq3Codes = Object.create(null);
318c9e2602eSstephan    [
319f45c3370Sstephan      'SQLITE_ACCESS_EXISTS',
320f45c3370Sstephan      'SQLITE_ACCESS_READWRITE',
32143b442a6Sstephan      'SQLITE_ERROR',
32243b442a6Sstephan      'SQLITE_IOERR',
32343b442a6Sstephan      'SQLITE_IOERR_ACCESS',
32443b442a6Sstephan      'SQLITE_IOERR_CLOSE',
325c4b87be3Sstephan      'SQLITE_IOERR_DELETE',
32643b442a6Sstephan      'SQLITE_IOERR_FSYNC',
32743b442a6Sstephan      'SQLITE_IOERR_LOCK',
32843b442a6Sstephan      'SQLITE_IOERR_READ',
32943b442a6Sstephan      'SQLITE_IOERR_SHORT_READ',
33043b442a6Sstephan      'SQLITE_IOERR_TRUNCATE',
33143b442a6Sstephan      'SQLITE_IOERR_UNLOCK',
33243b442a6Sstephan      'SQLITE_IOERR_WRITE',
3339a55773bSstephan      'SQLITE_LOCK_EXCLUSIVE',
33443b442a6Sstephan      'SQLITE_LOCK_NONE',
33543b442a6Sstephan      'SQLITE_LOCK_PENDING',
33643b442a6Sstephan      'SQLITE_LOCK_RESERVED',
33743b442a6Sstephan      'SQLITE_LOCK_SHARED',
33843b442a6Sstephan      'SQLITE_MISUSE',
33943b442a6Sstephan      'SQLITE_NOTFOUND',
34043b442a6Sstephan      'SQLITE_OPEN_CREATE',
34143b442a6Sstephan      'SQLITE_OPEN_DELETEONCLOSE',
342c4b87be3Sstephan      'SQLITE_OPEN_READONLY'
3439a55773bSstephan    ].forEach((k)=>{
3449a55773bSstephan      if(undefined === (state.sq3Codes[k] = capi[k])){
3459a55773bSstephan        toss("Maintenance required: not found:",k);
3469a55773bSstephan      }
347c5313afeSstephan    });
348c5313afeSstephan
349c5313afeSstephan    /**
350c9e2602eSstephan       Runs the given operation (by name) in the async worker
351c9e2602eSstephan       counterpart, waits for its response, and returns the result
352c9e2602eSstephan       which the async worker writes to SAB[state.opIds.rc]. The
353c9e2602eSstephan       2nd and subsequent arguments must be the aruguments for the
354c9e2602eSstephan       async op.
355c5313afeSstephan    */
356138647a5Sstephan    const opRun = (op,...args)=>{
3575e8bb0aaSstephan      const opNdx = state.opIds[op] || toss("Invalid op ID:",op);
3585e8bb0aaSstephan      state.s11n.serialize(...args);
359c9e2602eSstephan      Atomics.store(state.sabOPView, state.opIds.rc, -1);
3605e8bb0aaSstephan      Atomics.store(state.sabOPView, state.opIds.whichOp, opNdx);
361f861b36bSstephan      Atomics.notify(state.sabOPView, state.opIds.whichOp)
362f861b36bSstephan      /* async thread will take over here */;
363f815011aSstephan      const t = performance.now();
364f861b36bSstephan      Atomics.wait(state.sabOPView, state.opIds.rc, -1)
365f861b36bSstephan      /* When this wait() call returns, the async half will have
366f861b36bSstephan         completed the operation and reported its results. */;
367c9e2602eSstephan      const rc = Atomics.load(state.sabOPView, state.opIds.rc);
368e8afca3fSstephan      metrics[op].wait += performance.now() - t;
369e8afca3fSstephan      if(rc && state.asyncS11nExceptions){
37072ab400dSstephan        const err = state.s11n.deserialize();
37172ab400dSstephan        if(err) error(op+"() async error:",...err);
37272ab400dSstephan      }
3735e8bb0aaSstephan      return rc;
374c5313afeSstephan    };
375c5313afeSstephan
37649048b14Sstephan    /**
37749048b14Sstephan       Not part of the public API. Only for test/development use.
37849048b14Sstephan    */
37949048b14Sstephan    opfsUtil.debug = {
38049048b14Sstephan      asyncShutdown: ()=>{
38149048b14Sstephan        warn("Shutting down OPFS async listener. The OPFS VFS will no longer work.");
38249048b14Sstephan        opRun('opfs-async-shutdown');
38349048b14Sstephan      },
38449048b14Sstephan      asyncRestart: ()=>{
38549048b14Sstephan        warn("Attempting to restart OPFS VFS async listener. Might work, might not.");
38649048b14Sstephan        W.postMessage({type: 'opfs-async-restart'});
38749048b14Sstephan      }
38849048b14Sstephan    };
38949048b14Sstephan
390138647a5Sstephan    const initS11n = ()=>{
391b8c8d4e4Sstephan      /**
392f861b36bSstephan         !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
393f861b36bSstephan         ACHTUNG: this code is 100% duplicated in the other half of
394f861b36bSstephan         this proxy! The documentation is maintained in the
395f861b36bSstephan         "synchronous half".
396f861b36bSstephan         !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
397b8c8d4e4Sstephan
39872ab400dSstephan         This proxy de/serializes cross-thread function arguments and
39972ab400dSstephan         output-pointer values via the state.sabIO SharedArrayBuffer,
40072ab400dSstephan         using the region defined by (state.sabS11nOffset,
40172ab400dSstephan         state.sabS11nOffset]. Only one dataset is recorded at a time.
40272ab400dSstephan
403f861b36bSstephan         This is not a general-purpose format. It only supports the
404f861b36bSstephan         range of operations, and data sizes, needed by the
405f861b36bSstephan         sqlite3_vfs and sqlite3_io_methods operations. Serialized
406f861b36bSstephan         data are transient and this serialization algorithm may
407f861b36bSstephan         change at any time.
40872ab400dSstephan
40972ab400dSstephan         The data format can be succinctly summarized as:
41072ab400dSstephan
41172ab400dSstephan         Nt...Td...D
41272ab400dSstephan
41372ab400dSstephan         Where:
41472ab400dSstephan
41572ab400dSstephan         - N = number of entries (1 byte)
41672ab400dSstephan
41772ab400dSstephan         - t = type ID of first argument (1 byte)
41872ab400dSstephan
41972ab400dSstephan         - ...T = type IDs of the 2nd and subsequent arguments (1 byte
42072ab400dSstephan         each).
42172ab400dSstephan
42272ab400dSstephan         - d = raw bytes of first argument (per-type size).
42372ab400dSstephan
42472ab400dSstephan         - ...D = raw bytes of the 2nd and subsequent arguments (per-type
42572ab400dSstephan         size).
42672ab400dSstephan
42772ab400dSstephan         All types except strings have fixed sizes. Strings are stored
42872ab400dSstephan         using their TextEncoder/TextDecoder representations. It would
42972ab400dSstephan         arguably make more sense to store them as Int16Arrays of
43072ab400dSstephan         their JS character values, but how best/fastest to get that
431f861b36bSstephan         in and out of string form is an open point. Initial
432f861b36bSstephan         experimentation with that approach did not gain us any speed.
43372ab400dSstephan
43472ab400dSstephan         Historical note: this impl was initially about 1% this size by
43572ab400dSstephan         using using JSON.stringify/parse(), but using fit-to-purpose
43672ab400dSstephan         serialization saves considerable runtime.
437b8c8d4e4Sstephan      */
438138647a5Sstephan      if(state.s11n) return state.s11n;
439b8c8d4e4Sstephan      const textDecoder = new TextDecoder(),
440b8c8d4e4Sstephan            textEncoder = new TextEncoder('utf-8'),
441b8c8d4e4Sstephan            viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize),
442b8c8d4e4Sstephan            viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
443138647a5Sstephan      state.s11n = Object.create(null);
44472ab400dSstephan      /* Only arguments and return values of these types may be
44572ab400dSstephan         serialized. This covers the whole range of types needed by the
44672ab400dSstephan         sqlite3_vfs API. */
447b8c8d4e4Sstephan      const TypeIds = Object.create(null);
448b8c8d4e4Sstephan      TypeIds.number  = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' };
449b8c8d4e4Sstephan      TypeIds.bigint  = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' };
450b8c8d4e4Sstephan      TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' };
451b8c8d4e4Sstephan      TypeIds.string =  { id: 4 };
45272ab400dSstephan
45372ab400dSstephan      const getTypeId = (v)=>(
45472ab400dSstephan        TypeIds[typeof v]
45572ab400dSstephan          || toss("Maintenance required: this value type cannot be serialized.",v)
45672ab400dSstephan      );
457b8c8d4e4Sstephan      const getTypeIdById = (tid)=>{
458b8c8d4e4Sstephan        switch(tid){
459b8c8d4e4Sstephan            case TypeIds.number.id: return TypeIds.number;
460b8c8d4e4Sstephan            case TypeIds.bigint.id: return TypeIds.bigint;
461b8c8d4e4Sstephan            case TypeIds.boolean.id: return TypeIds.boolean;
462b8c8d4e4Sstephan            case TypeIds.string.id: return TypeIds.string;
463b8c8d4e4Sstephan            default: toss("Invalid type ID:",tid);
464b8c8d4e4Sstephan        }
465b8c8d4e4Sstephan      };
46672ab400dSstephan
467138647a5Sstephan      /**
46872ab400dSstephan         Returns an array of the deserialized state stored by the most
46972ab400dSstephan         recent serialize() operation (from from this thread or the
470*da264159Sstephan         counterpart thread), or null if the serialization buffer is
471*da264159Sstephan         empty.  If passed a truthy argument, the serialization buffer
472*da264159Sstephan         is cleared after deserialization.
473138647a5Sstephan      */
474*da264159Sstephan      state.s11n.deserialize = function(clear=false){
475b8c8d4e4Sstephan        ++metrics.s11n.deserialize.count;
476b8c8d4e4Sstephan        const t = performance.now();
477b8c8d4e4Sstephan        const argc = viewU8[0];
47872ab400dSstephan        const rc = argc ? [] : null;
479b8c8d4e4Sstephan        if(argc){
48072ab400dSstephan          const typeIds = [];
48172ab400dSstephan          let offset = 1, i, n, v;
482b8c8d4e4Sstephan          for(i = 0; i < argc; ++i, ++offset){
483b8c8d4e4Sstephan            typeIds.push(getTypeIdById(viewU8[offset]));
484138647a5Sstephan          }
485b8c8d4e4Sstephan          for(i = 0; i < argc; ++i){
486b8c8d4e4Sstephan            const t = typeIds[i];
487b8c8d4e4Sstephan            if(t.getter){
488b8c8d4e4Sstephan              v = viewDV[t.getter](offset, state.littleEndian);
489b8c8d4e4Sstephan              offset += t.size;
49072ab400dSstephan            }else{/*String*/
491b8c8d4e4Sstephan              n = viewDV.getInt32(offset, state.littleEndian);
492b8c8d4e4Sstephan              offset += 4;
493b8c8d4e4Sstephan              v = textDecoder.decode(viewU8.slice(offset, offset+n));
494b8c8d4e4Sstephan              offset += n;
495b8c8d4e4Sstephan            }
496b8c8d4e4Sstephan            rc.push(v);
497b8c8d4e4Sstephan          }
498b8c8d4e4Sstephan        }
499*da264159Sstephan        if(clear) viewU8[0] = 0;
500b8c8d4e4Sstephan        //log("deserialize:",argc, rc);
501b8c8d4e4Sstephan        metrics.s11n.deserialize.time += performance.now() - t;
502b8c8d4e4Sstephan        return rc;
503b8c8d4e4Sstephan      };
50472ab400dSstephan
505138647a5Sstephan      /**
506138647a5Sstephan         Serializes all arguments to the shared buffer for consumption
507b8c8d4e4Sstephan         by the counterpart thread.
5085e8bb0aaSstephan
509b8c8d4e4Sstephan         This routine is only intended for serializing OPFS VFS
510b8c8d4e4Sstephan         arguments and (in at least one special case) result values,
511b8c8d4e4Sstephan         and the buffer is sized to be able to comfortably handle
512b8c8d4e4Sstephan         those.
5135e8bb0aaSstephan
5145e8bb0aaSstephan         If passed no arguments then it zeroes out the serialization
5155e8bb0aaSstephan         state.
516138647a5Sstephan      */
517138647a5Sstephan      state.s11n.serialize = function(...args){
518b8c8d4e4Sstephan        const t = performance.now();
51972ab400dSstephan        ++metrics.s11n.serialize.count;
5205e8bb0aaSstephan        if(args.length){
521b8c8d4e4Sstephan          //log("serialize():",args);
52272ab400dSstephan          const typeIds = [];
52372ab400dSstephan          let i = 0, offset = 1;
52472ab400dSstephan          viewU8[0] = args.length & 0xff /* header = # of args */;
525b8c8d4e4Sstephan          for(; i < args.length; ++i, ++offset){
52672ab400dSstephan            /* Write the TypeIds.id value into the next args.length
52772ab400dSstephan               bytes. */
528b8c8d4e4Sstephan            typeIds.push(getTypeId(args[i]));
529b8c8d4e4Sstephan            viewU8[offset] = typeIds[i].id;
5305e8bb0aaSstephan          }
531b8c8d4e4Sstephan          for(i = 0; i < args.length; ++i) {
53272ab400dSstephan            /* Deserialize the following bytes based on their
53372ab400dSstephan               corresponding TypeIds.id from the header. */
534b8c8d4e4Sstephan            const t = typeIds[i];
535b8c8d4e4Sstephan            if(t.setter){
536b8c8d4e4Sstephan              viewDV[t.setter](offset, args[i], state.littleEndian);
537b8c8d4e4Sstephan              offset += t.size;
53872ab400dSstephan            }else{/*String*/
539b8c8d4e4Sstephan              const s = textEncoder.encode(args[i]);
540b8c8d4e4Sstephan              viewDV.setInt32(offset, s.byteLength, state.littleEndian);
541b8c8d4e4Sstephan              offset += 4;
542b8c8d4e4Sstephan              viewU8.set(s, offset);
543b8c8d4e4Sstephan              offset += s.byteLength;
544b8c8d4e4Sstephan            }
545b8c8d4e4Sstephan          }
546b8c8d4e4Sstephan          //log("serialize() result:",viewU8.slice(0,offset));
547b8c8d4e4Sstephan        }else{
548b8c8d4e4Sstephan          viewU8[0] = 0;
549b8c8d4e4Sstephan        }
550b8c8d4e4Sstephan        metrics.s11n.serialize.time += performance.now() - t;
551138647a5Sstephan      };
552138647a5Sstephan      return state.s11n;
553b8c8d4e4Sstephan    }/*initS11n()*/;
554138647a5Sstephan
555c5313afeSstephan    /**
556c5313afeSstephan       Generates a random ASCII string len characters long, intended for
557c5313afeSstephan       use as a temporary file name.
558c5313afeSstephan    */
559c5313afeSstephan    const randomFilename = function f(len=16){
560c5313afeSstephan      if(!f._chars){
561c5313afeSstephan        f._chars = "abcdefghijklmnopqrstuvwxyz"+
562c5313afeSstephan          "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+
563c5313afeSstephan          "012346789";
564c5313afeSstephan        f._n = f._chars.length;
565c5313afeSstephan      }
566c5313afeSstephan      const a = [];
567c5313afeSstephan      let i = 0;
568c5313afeSstephan      for( ; i < len; ++i){
569c5313afeSstephan        const ndx = Math.random() * (f._n * 64) % f._n | 0;
570c5313afeSstephan        a[i] = f._chars[ndx];
571c5313afeSstephan      }
572c5313afeSstephan      return a.join('');
573c5313afeSstephan    };
574c5313afeSstephan
575c5313afeSstephan    /**
576c5313afeSstephan       Map of sqlite3_file pointers to objects constructed by xOpen().
577c5313afeSstephan    */
578c5313afeSstephan    const __openFiles = Object.create(null);
5793961b263Sstephan
5803961b263Sstephan    /**
5813961b263Sstephan       Installs a StructBinder-bound function pointer member of the
5823961b263Sstephan       given name and function in the given StructType target object.
5833961b263Sstephan       It creates a WASM proxy for the given function and arranges for
5843961b263Sstephan       that proxy to be cleaned up when tgt.dispose() is called.  Throws
5853961b263Sstephan       on the slightest hint of error (e.g. tgt is-not-a StructType,
5863961b263Sstephan       name does not map to a struct-bound member, etc.).
5873961b263Sstephan
5883961b263Sstephan       Returns a proxy for this function which is bound to tgt and takes
5893961b263Sstephan       2 args (name,func). That function returns the same thing,
5903961b263Sstephan       permitting calls to be chained.
5913961b263Sstephan
5923961b263Sstephan       If called with only 1 arg, it has no side effects but returns a
5933961b263Sstephan       func with the same signature as described above.
5943961b263Sstephan    */
5953961b263Sstephan    const installMethod = function callee(tgt, name, func){
596f3860120Sstephan      if(!(tgt instanceof sqlite3.StructBinder.StructType)){
5973961b263Sstephan        toss("Usage error: target object is-not-a StructType.");
5983961b263Sstephan      }
5993961b263Sstephan      if(1===arguments.length){
6003961b263Sstephan        return (n,f)=>callee(tgt,n,f);
6013961b263Sstephan      }
6023961b263Sstephan      if(!callee.argcProxy){
6033961b263Sstephan        callee.argcProxy = function(func,sig){
6043961b263Sstephan          return function(...args){
6053961b263Sstephan            if(func.length!==arguments.length){
6063961b263Sstephan              toss("Argument mismatch. Native signature is:",sig);
6073961b263Sstephan            }
6083961b263Sstephan            return func.apply(this, args);
6093961b263Sstephan          }
6103961b263Sstephan        };
6113961b263Sstephan        callee.removeFuncList = function(){
6123961b263Sstephan          if(this.ondispose.__removeFuncList){
6133961b263Sstephan            this.ondispose.__removeFuncList.forEach(
6143961b263Sstephan              (v,ndx)=>{
6153961b263Sstephan                if('number'===typeof v){
6163961b263Sstephan                  try{wasm.uninstallFunction(v)}
6173961b263Sstephan                  catch(e){/*ignore*/}
6183961b263Sstephan                }
6193961b263Sstephan                /* else it's a descriptive label for the next number in
6203961b263Sstephan                   the list. */
6213961b263Sstephan              }
6223961b263Sstephan            );
6233961b263Sstephan            delete this.ondispose.__removeFuncList;
6243961b263Sstephan          }
6253961b263Sstephan        };
6263961b263Sstephan      }/*static init*/
6273961b263Sstephan      const sigN = tgt.memberSignature(name);
6283961b263Sstephan      if(sigN.length<2){
6293961b263Sstephan        toss("Member",name," is not a function pointer. Signature =",sigN);
6303961b263Sstephan      }
6313961b263Sstephan      const memKey = tgt.memberKey(name);
632c2ccd676Sstephan      const fProxy = 0
633f861b36bSstephan      /** This middle-man proxy is only for use during development, to
634f861b36bSstephan          confirm that we always pass the proper number of
635f861b36bSstephan          arguments. We know that the C-level code will always use the
636f861b36bSstephan          correct argument count. */
6373961b263Sstephan            ? callee.argcProxy(func, sigN)
6383961b263Sstephan            : func;
6393961b263Sstephan      const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
6403961b263Sstephan      tgt[memKey] = pFunc;
6413961b263Sstephan      if(!tgt.ondispose) tgt.ondispose = [];
6423961b263Sstephan      if(!tgt.ondispose.__removeFuncList){
6433961b263Sstephan        tgt.ondispose.push('ondispose.__removeFuncList handler',
6443961b263Sstephan                           callee.removeFuncList);
6453961b263Sstephan        tgt.ondispose.__removeFuncList = [];
6463961b263Sstephan      }
6473961b263Sstephan      tgt.ondispose.__removeFuncList.push(memKey, pFunc);
6483961b263Sstephan      return (n,f)=>callee(tgt, n, f);
6493961b263Sstephan    }/*installMethod*/;
6503961b263Sstephan
651f815011aSstephan    const opTimer = Object.create(null);
652f815011aSstephan    opTimer.op = undefined;
653f815011aSstephan    opTimer.start = undefined;
654f815011aSstephan    const mTimeStart = (op)=>{
655f815011aSstephan      opTimer.start = performance.now();
656f815011aSstephan      opTimer.op = op;
657f815011aSstephan      ++metrics[op].count;
658f815011aSstephan    };
659f815011aSstephan    const mTimeEnd = ()=>(
660f815011aSstephan      metrics[opTimer.op].time += performance.now() - opTimer.start
661f815011aSstephan    );
662f815011aSstephan
6633961b263Sstephan    /**
664c5313afeSstephan       Impls for the sqlite3_io_methods methods. Maintenance reminder:
665c5313afeSstephan       members are in alphabetical order to simplify finding them.
6663961b263Sstephan    */
667c5313afeSstephan    const ioSyncWrappers = {
668c5313afeSstephan      xCheckReservedLock: function(pFile,pOut){
669f861b36bSstephan        /**
670f861b36bSstephan           As of late 2022, only a single lock can be held on an OPFS
671f861b36bSstephan           file. We have no way of checking whether any _other_ db
672f861b36bSstephan           connection has a lock except by trying to obtain and (on
673f861b36bSstephan           success) release a sync-handle for it, but doing so would
674f861b36bSstephan           involve an inherent race condition. For the time being,
675f861b36bSstephan           pending a better solution, we simply report whether the
676f861b36bSstephan           given pFile instance has a lock.
677f861b36bSstephan        */
6789a55773bSstephan        const f = __openFiles[pFile];
6799a55773bSstephan        wasm.setMemValue(pOut, f.lockMode ? 1 : 0, 'i32');
680c5313afeSstephan        return 0;
681c5313afeSstephan      },
682c5313afeSstephan      xClose: function(pFile){
683f815011aSstephan        mTimeStart('xClose');
684c5313afeSstephan        let rc = 0;
685c5313afeSstephan        const f = __openFiles[pFile];
686c5313afeSstephan        if(f){
687c5313afeSstephan          delete __openFiles[pFile];
688c5313afeSstephan          rc = opRun('xClose', pFile);
689c5313afeSstephan          if(f.sq3File) f.sq3File.dispose();
6903961b263Sstephan        }
691f815011aSstephan        mTimeEnd();
692c5313afeSstephan        return rc;
693c5313afeSstephan      },
694c5313afeSstephan      xDeviceCharacteristics: function(pFile){
695c5313afeSstephan        //debug("xDeviceCharacteristics(",pFile,")");
696c5313afeSstephan        return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
697c5313afeSstephan      },
698f815011aSstephan      xFileControl: function(pFile, opId, pArg){
699f815011aSstephan        mTimeStart('xFileControl');
700aec046a2Sstephan        const rc = (capi.SQLITE_FCNTL_SYNC===opId)
701138647a5Sstephan              ? opRun('xSync', pFile, 0)
702aec046a2Sstephan              : capi.SQLITE_NOTFOUND;
703f815011aSstephan        mTimeEnd();
704aec046a2Sstephan        return rc;
705c5313afeSstephan      },
706c5313afeSstephan      xFileSize: function(pFile,pSz64){
707f815011aSstephan        mTimeStart('xFileSize');
708c5313afeSstephan        const rc = opRun('xFileSize', pFile);
709e8afca3fSstephan        if(0==rc){
710138647a5Sstephan          const sz = state.s11n.deserialize()[0];
711278d3fafSstephan          wasm.setMemValue(pSz64, sz, 'i64');
712c5313afeSstephan        }
713f815011aSstephan        mTimeEnd();
714c5313afeSstephan        return rc;
715c5313afeSstephan      },
716c5313afeSstephan      xLock: function(pFile,lockType){
7179a55773bSstephan        mTimeStart('xLock');
7189a55773bSstephan        const f = __openFiles[pFile];
7199a55773bSstephan        let rc = 0;
7209a55773bSstephan        if( capi.SQLITE_LOCK_NONE === f.lockType ) {
7219a55773bSstephan          rc = opRun('xLock', pFile, lockType);
7229a55773bSstephan          if( 0===rc ) f.lockType = lockType;
7239a55773bSstephan        }else{
7249a55773bSstephan          f.lockType = lockType;
7259a55773bSstephan        }
7269a55773bSstephan        mTimeEnd();
7279a55773bSstephan        return rc;
728c5313afeSstephan      },
729138647a5Sstephan      xRead: function(pFile,pDest,n,offset64){
730f815011aSstephan        mTimeStart('xRead');
731c5313afeSstephan        const f = __openFiles[pFile];
732c5313afeSstephan        let rc;
733c5313afeSstephan        try {
734138647a5Sstephan          rc = opRun('xRead',pFile, n, Number(offset64));
735862281fcSstephan          if(0===rc || capi.SQLITE_IOERR_SHORT_READ===rc){
736f861b36bSstephan            /**
737f861b36bSstephan               Results get written to the SharedArrayBuffer f.sabView.
738f861b36bSstephan               Because the heap is _not_ a SharedArrayBuffer, we have
739f861b36bSstephan               to copy the results. TypedArray.set() seems to be the
740f861b36bSstephan               fastest way to copy this. */
741f815011aSstephan            wasm.heap8u().set(f.sabView.subarray(0, n), pDest);
742862281fcSstephan          }
743c5313afeSstephan        }catch(e){
744c5313afeSstephan          error("xRead(",arguments,") failed:",e,f);
745c5313afeSstephan          rc = capi.SQLITE_IOERR_READ;
7463961b263Sstephan        }
747f815011aSstephan        mTimeEnd();
748c5313afeSstephan        return rc;
749c5313afeSstephan      },
750c5313afeSstephan      xSync: function(pFile,flags){
751aec046a2Sstephan        ++metrics.xSync.count;
752138647a5Sstephan        return 0; // impl'd in xFileControl()
753c5313afeSstephan      },
754c5313afeSstephan      xTruncate: function(pFile,sz64){
755f815011aSstephan        mTimeStart('xTruncate');
756138647a5Sstephan        const rc = opRun('xTruncate', pFile, Number(sz64));
757f815011aSstephan        mTimeEnd();
758f815011aSstephan        return rc;
759c5313afeSstephan      },
760c5313afeSstephan      xUnlock: function(pFile,lockType){
7619a55773bSstephan        mTimeStart('xUnlock');
7629a55773bSstephan        const f = __openFiles[pFile];
7639a55773bSstephan        let rc = 0;
7649a55773bSstephan        if( capi.SQLITE_LOCK_NONE === lockType
7659a55773bSstephan          && f.lockType ){
7669a55773bSstephan          rc = opRun('xUnlock', pFile, lockType);
7679a55773bSstephan        }
7689a55773bSstephan        if( 0===rc ) f.lockType = lockType;
7699a55773bSstephan        mTimeEnd();
7709a55773bSstephan        return rc;
771c5313afeSstephan      },
772138647a5Sstephan      xWrite: function(pFile,pSrc,n,offset64){
773f815011aSstephan        mTimeStart('xWrite');
774c5313afeSstephan        const f = __openFiles[pFile];
775f815011aSstephan        let rc;
776c5313afeSstephan        try {
777f815011aSstephan          f.sabView.set(wasm.heap8u().subarray(pSrc, pSrc+n));
778138647a5Sstephan          rc = opRun('xWrite', pFile, n, Number(offset64));
779c5313afeSstephan        }catch(e){
780c5313afeSstephan          error("xWrite(",arguments,") failed:",e,f);
781f815011aSstephan          rc = capi.SQLITE_IOERR_WRITE;
782c5313afeSstephan        }
783f815011aSstephan        mTimeEnd();
784f815011aSstephan        return rc;
785c5313afeSstephan      }
786c5313afeSstephan    }/*ioSyncWrappers*/;
787c5313afeSstephan
788c5313afeSstephan    /**
789c5313afeSstephan       Impls for the sqlite3_vfs methods. Maintenance reminder: members
790c5313afeSstephan       are in alphabetical order to simplify finding them.
791c5313afeSstephan    */
792c5313afeSstephan    const vfsSyncWrappers = {
793c5313afeSstephan      xAccess: function(pVfs,zName,flags,pOut){
794f815011aSstephan        mTimeStart('xAccess');
7955e8bb0aaSstephan        const rc = opRun('xAccess', wasm.cstringToJs(zName));
7965e8bb0aaSstephan        wasm.setMemValue( pOut, (rc ? 0 : 1), 'i32' );
797f815011aSstephan        mTimeEnd();
7983961b263Sstephan        return 0;
799c5313afeSstephan      },
800c5313afeSstephan      xCurrentTime: function(pVfs,pOut){
8013961b263Sstephan        /* If it turns out that we need to adjust for timezone, see:
8023961b263Sstephan           https://stackoverflow.com/a/11760121/1458521 */
8033961b263Sstephan        wasm.setMemValue(pOut, 2440587.5 + (new Date().getTime()/86400000),
8043961b263Sstephan                         'double');
8053961b263Sstephan        return 0;
806c5313afeSstephan      },
807c5313afeSstephan      xCurrentTimeInt64: function(pVfs,pOut){
8083961b263Sstephan        // TODO: confirm that this calculation is correct
8093961b263Sstephan        wasm.setMemValue(pOut, (2440587.5 * 86400000) + new Date().getTime(),
8103961b263Sstephan                         'i64');
8113961b263Sstephan        return 0;
812c5313afeSstephan      },
813c5313afeSstephan      xDelete: function(pVfs, zName, doSyncDir){
814f815011aSstephan        mTimeStart('xDelete');
815138647a5Sstephan        opRun('xDelete', wasm.cstringToJs(zName), doSyncDir, false);
816f3860120Sstephan        /* We're ignoring errors because we cannot yet differentiate
817f3860120Sstephan           between harmless and non-harmless failures. */
818f815011aSstephan        mTimeEnd();
819f3860120Sstephan        return 0;
820c5313afeSstephan      },
821c5313afeSstephan      xFullPathname: function(pVfs,zName,nOut,pOut){
822c5313afeSstephan        /* Until/unless we have some notion of "current dir"
823c5313afeSstephan           in OPFS, simply copy zName to pOut... */
824c5313afeSstephan        const i = wasm.cstrncpy(pOut, zName, nOut);
825c5313afeSstephan        return i<nOut ? 0 : capi.SQLITE_CANTOPEN
826c5313afeSstephan        /*CANTOPEN is required by the docs but SQLITE_RANGE would be a closer match*/;
827c5313afeSstephan      },
828c5313afeSstephan      xGetLastError: function(pVfs,nOut,pOut){
829c5313afeSstephan        /* TODO: store exception.message values from the async
830c5313afeSstephan           partner in a dedicated SharedArrayBuffer, noting that we'd have
831c5313afeSstephan           to encode them... TextEncoder can do that for us. */
832c5313afeSstephan        warn("OPFS xGetLastError() has nothing sensible to return.");
8333961b263Sstephan        return 0;
834c5313afeSstephan      },
8358766fd20Sstephan      //xSleep is optionally defined below
836c5313afeSstephan      xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){
837f815011aSstephan        mTimeStart('xOpen');
838c5313afeSstephan        if(0===zName){
839c5313afeSstephan          zName = randomFilename();
840c5313afeSstephan        }else if('number'===typeof zName){
841c5313afeSstephan          zName = wasm.cstringToJs(zName);
842c5313afeSstephan        }
843138647a5Sstephan        const fh = Object.create(null);
844138647a5Sstephan        fh.fid = pFile;
845138647a5Sstephan        fh.filename = zName;
846138647a5Sstephan        fh.sab = new SharedArrayBuffer(state.fileBufferSize);
847138647a5Sstephan        fh.flags = flags;
848138647a5Sstephan        const rc = opRun('xOpen', pFile, zName, flags);
849c5313afeSstephan        if(!rc){
850c5313afeSstephan          /* Recall that sqlite3_vfs::xClose() will be called, even on
851c5313afeSstephan             error, unless pFile->pMethods is NULL. */
852138647a5Sstephan          if(fh.readOnly){
853c5313afeSstephan            wasm.setMemValue(pOutFlags, capi.SQLITE_OPEN_READONLY, 'i32');
854c5313afeSstephan          }
855138647a5Sstephan          __openFiles[pFile] = fh;
856138647a5Sstephan          fh.sabView = state.sabFileBufView;
857138647a5Sstephan          fh.sq3File = new sqlite3_file(pFile);
858138647a5Sstephan          fh.sq3File.$pMethods = opfsIoMethods.pointer;
8599a55773bSstephan          fh.lockType = capi.SQLITE_LOCK_NONE;
860c5313afeSstephan        }
861f815011aSstephan        mTimeEnd();
862c5313afeSstephan        return rc;
863c5313afeSstephan      }/*xOpen()*/
864c5313afeSstephan    }/*vfsSyncWrappers*/;
865c5313afeSstephan
8668766fd20Sstephan    if(dVfs){
8678766fd20Sstephan      opfsVfs.$xRandomness = dVfs.$xRandomness;
8688766fd20Sstephan      opfsVfs.$xSleep = dVfs.$xSleep;
8698766fd20Sstephan    }
870c5313afeSstephan    if(!opfsVfs.$xRandomness){
871c5313afeSstephan      /* If the default VFS has no xRandomness(), add a basic JS impl... */
872c5313afeSstephan      vfsSyncWrappers.xRandomness = function(pVfs, nOut, pOut){
8733961b263Sstephan        const heap = wasm.heap8u();
8743961b263Sstephan        let i = 0;
8753961b263Sstephan        for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF;
8763961b263Sstephan        return i;
877c5313afeSstephan      };
878c5313afeSstephan    }
879c5313afeSstephan    if(!opfsVfs.$xSleep){
880c5313afeSstephan      /* If we can inherit an xSleep() impl from the default VFS then
8818766fd20Sstephan         assume it's sane and use it, otherwise install a JS-based
8828766fd20Sstephan         one. */
8838766fd20Sstephan      vfsSyncWrappers.xSleep = function(pVfs,ms){
884c4b87be3Sstephan        Atomics.wait(state.sabOPView, state.opIds.xSleep, 0, ms);
8858766fd20Sstephan        return 0;
8868766fd20Sstephan      };
8873961b263Sstephan    }
8883961b263Sstephan
889c5313afeSstephan    /* Install the vfs/io_methods into their C-level shared instances... */
890278d3fafSstephan    for(let k of Object.keys(ioSyncWrappers)){
891278d3fafSstephan      installMethod(opfsIoMethods, k, ioSyncWrappers[k]);
892278d3fafSstephan    }
893278d3fafSstephan    for(let k of Object.keys(vfsSyncWrappers)){
894278d3fafSstephan      installMethod(opfsVfs, k, vfsSyncWrappers[k]);
895278d3fafSstephan    }
8963961b263Sstephan
897f3860120Sstephan    /**
89849048b14Sstephan       Expects an OPFS file path. It gets resolved, such that ".."
89949048b14Sstephan       components are properly expanded, and returned. If the 2nd arg
90049048b14Sstephan       is true, the result is returned as an array of path elements,
90149048b14Sstephan       else an absolute path string is returned.
902f3860120Sstephan    */
90349048b14Sstephan    opfsUtil.getResolvedPath = function(filename,splitIt){
90449048b14Sstephan      const p = new URL(filename, "file://irrelevant").pathname;
90549048b14Sstephan      return splitIt ? p.split('/').filter((v)=>!!v) : p;
906f3860120Sstephan    };
90749048b14Sstephan
908f3860120Sstephan    /**
90949048b14Sstephan       Takes the absolute path to a filesystem element. Returns an
91049048b14Sstephan       array of [handleOfContainingDir, filename]. If the 2nd argument
91149048b14Sstephan       is truthy then each directory element leading to the file is
91249048b14Sstephan       created along the way. Throws if any creation or resolution
91349048b14Sstephan       fails.
91449048b14Sstephan    */
91549048b14Sstephan    opfsUtil.getDirForFilename = async function f(absFilename, createDirs = false){
91649048b14Sstephan      const path = opfsUtil.getResolvedPath(absFilename, true);
91749048b14Sstephan      const filename = path.pop();
91849048b14Sstephan      let dh = opfsUtil.rootDirectory;
91949048b14Sstephan      for(const dirName of path){
92049048b14Sstephan        if(dirName){
92149048b14Sstephan          dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs});
92249048b14Sstephan        }
92349048b14Sstephan      }
92449048b14Sstephan      return [dh, filename];
92549048b14Sstephan    };
92649048b14Sstephan
92749048b14Sstephan    /**
92849048b14Sstephan       Creates the given directory name, recursively, in
929f3860120Sstephan       the OPFS filesystem. Returns true if it succeeds or the
930f3860120Sstephan       directory already exists, else false.
931f3860120Sstephan    */
93249048b14Sstephan    opfsUtil.mkdir = async function(absDirName){
93349048b14Sstephan      try {
93449048b14Sstephan        await opfsUtil.getDirForFilename(absDirName+"/filepart", true);
93549048b14Sstephan        return true;
93649048b14Sstephan      }catch(e){
93749048b14Sstephan        //console.warn("mkdir(",absDirName,") failed:",e);
93849048b14Sstephan        return false;
93949048b14Sstephan      }
940f3860120Sstephan    };
941f3860120Sstephan    /**
94249048b14Sstephan       Checks whether the given OPFS filesystem entry exists,
943f3860120Sstephan       returning true if it does, false if it doesn't.
944f3860120Sstephan    */
94549048b14Sstephan    opfsUtil.entryExists = async function(fsEntryName){
94649048b14Sstephan      try {
947f45c3370Sstephan        const [dh, fn] = await opfsUtil.getDirForFilename(fsEntryName);
94849048b14Sstephan        await dh.getFileHandle(fn);
94949048b14Sstephan        return true;
95049048b14Sstephan      }catch(e){
95149048b14Sstephan        return false;
95249048b14Sstephan      }
953f3860120Sstephan    };
954f3860120Sstephan
955f3860120Sstephan    /**
956f3860120Sstephan       Generates a random ASCII string, intended for use as a
957f3860120Sstephan       temporary file name. Its argument is the length of the string,
958f3860120Sstephan       defaulting to 16.
959f3860120Sstephan    */
960f3860120Sstephan    opfsUtil.randomFilename = randomFilename;
961f3860120Sstephan
96256fae744Sstephan    /**
96356fae744Sstephan       Re-registers the OPFS VFS. This is intended only for odd use
96456fae744Sstephan       cases which have to call sqlite3_shutdown() as part of their
96556fae744Sstephan       initialization process, which will unregister the VFS
96656fae744Sstephan       registered by installOpfsVfs(). If passed a truthy value, the
96756fae744Sstephan       OPFS VFS is registered as the default VFS, else it is not made
96856fae744Sstephan       the default. Returns the result of the the
96956fae744Sstephan       sqlite3_vfs_register() call.
97056fae744Sstephan
97156fae744Sstephan       Design note: the problem of having to re-register things after
97256fae744Sstephan       a shutdown/initialize pair is more general. How to best plug
97356fae744Sstephan       that in to the library is unclear. In particular, we cannot
97456fae744Sstephan       hook in to any C-side calls to sqlite3_initialize(), so we
97556fae744Sstephan       cannot add an after-initialize callback mechanism.
97656fae744Sstephan    */
9773d645484Sstephan    opfsUtil.registerVfs = (asDefault=false)=>{
9788948fbeeSstephan      return wasm.exports.sqlite3_vfs_register(
97956fae744Sstephan        opfsVfs.pointer, asDefault ? 1 : 0
98056fae744Sstephan      );
98156fae744Sstephan    };
98256fae744Sstephan
9833c272ba3Sstephan    /**
98449048b14Sstephan       Returns a promise which resolves to an object which represents
98549048b14Sstephan       all files and directories in the OPFS tree. The top-most object
98649048b14Sstephan       has two properties: `dirs` is an array of directory entries
98749048b14Sstephan       (described below) and `files` is a list of file names for all
98849048b14Sstephan       files in that directory.
98949048b14Sstephan
99049048b14Sstephan       Traversal starts at sqlite3.opfs.rootDirectory.
99149048b14Sstephan
99249048b14Sstephan       Each `dirs` entry is an object in this form:
99349048b14Sstephan
99449048b14Sstephan       ```
99549048b14Sstephan       { name: directoryName,
99649048b14Sstephan         dirs: [...subdirs],
99749048b14Sstephan         files: [...file names]
99849048b14Sstephan       }
99949048b14Sstephan       ```
100049048b14Sstephan
100149048b14Sstephan       The `files` and `subdirs` entries are always set but may be
100249048b14Sstephan       empty arrays.
100349048b14Sstephan
100449048b14Sstephan       The returned object has the same structure but its `name` is
100549048b14Sstephan       an empty string. All returned objects are created with
100649048b14Sstephan       Object.create(null), so have no prototype.
100749048b14Sstephan
100849048b14Sstephan       Design note: the entries do not contain more information,
100949048b14Sstephan       e.g. file sizes, because getting such info is not only
101049048b14Sstephan       expensive but is subject to locking-related errors.
10113c272ba3Sstephan    */
101249048b14Sstephan    opfsUtil.treeList = async function(){
101349048b14Sstephan      const doDir = async function callee(dirHandle,tgt){
101449048b14Sstephan        tgt.name = dirHandle.name;
101549048b14Sstephan        tgt.dirs = [];
101649048b14Sstephan        tgt.files = [];
101749048b14Sstephan        for await (const handle of dirHandle.values()){
101849048b14Sstephan          if('directory' === handle.kind){
101949048b14Sstephan            const subDir = Object.create(null);
102049048b14Sstephan            tgt.dirs.push(subDir);
102149048b14Sstephan            await callee(handle, subDir);
102249048b14Sstephan          }else{
102349048b14Sstephan            tgt.files.push(handle.name);
102449048b14Sstephan          }
102549048b14Sstephan        }
102649048b14Sstephan      };
102749048b14Sstephan      const root = Object.create(null);
102849048b14Sstephan      await doDir(opfsUtil.rootDirectory, root);
102949048b14Sstephan      return root;
103049048b14Sstephan    };
103149048b14Sstephan
103249048b14Sstephan    /**
103349048b14Sstephan       Irrevocably deletes _all_ files in the current origin's OPFS.
103449048b14Sstephan       Obviously, this must be used with great caution. It may throw
103549048b14Sstephan       an exception if removal of anything fails (e.g. a file is
103649048b14Sstephan       locked), but the precise conditions under which it will throw
103749048b14Sstephan       are not documented (so we cannot tell you what they are).
103849048b14Sstephan    */
103949048b14Sstephan    opfsUtil.rmfr = async function(){
104049048b14Sstephan      const dir = opfsUtil.rootDirectory, opt = {recurse: true};
104149048b14Sstephan      for await (const handle of dir.values()){
104249048b14Sstephan        dir.removeEntry(handle.name, opt);
10433c272ba3Sstephan      }
10443c272ba3Sstephan    };
10453c272ba3Sstephan
104649048b14Sstephan    /**
104749048b14Sstephan       Deletes the given OPFS filesystem entry.  As this environment
104849048b14Sstephan       has no notion of "current directory", the given name must be an
104949048b14Sstephan       absolute path. If the 2nd argument is truthy, deletion is
105049048b14Sstephan       recursive (use with caution!).
105149048b14Sstephan
105249048b14Sstephan       The returned Promise resolves to true if the deletion was
105349048b14Sstephan       successful, else false (but...). The OPFS API reports the
105449048b14Sstephan       reason for the failure only in human-readable form, not
105549048b14Sstephan       exceptions which can be type-checked to determine the
105649048b14Sstephan       failure. Because of that...
105749048b14Sstephan
105849048b14Sstephan       If the final argument is truthy then this function will
105949048b14Sstephan       propagate any exception on error, rather than returning false.
106049048b14Sstephan    */
106149048b14Sstephan    opfsUtil.unlink = async function(fsEntryName, recursive = false,
106249048b14Sstephan                                          throwOnError = false){
106349048b14Sstephan      try {
106449048b14Sstephan        const [hDir, filenamePart] =
106549048b14Sstephan              await opfsUtil.getDirForFilename(fsEntryName, false);
106649048b14Sstephan        await hDir.removeEntry(filenamePart, {recursive});
106749048b14Sstephan        return true;
106849048b14Sstephan      }catch(e){
106949048b14Sstephan        if(throwOnError){
107049048b14Sstephan          throw new Error("unlink(",arguments[0],") failed: "+e.message,{
107149048b14Sstephan            cause: e
107249048b14Sstephan          });
107349048b14Sstephan        }
107449048b14Sstephan        return false;
107549048b14Sstephan      }
107649048b14Sstephan    };
107749048b14Sstephan
107849048b14Sstephan    /**
107949048b14Sstephan       Traverses the OPFS filesystem, calling a callback for each one.
108049048b14Sstephan       The argument may be either a callback function or an options object
108149048b14Sstephan       with any of the following properties:
108249048b14Sstephan
108349048b14Sstephan       - `callback`: function which gets called for each filesystem
108449048b14Sstephan         entry.  It gets passed 3 arguments: 1) the
108549048b14Sstephan         FileSystemFileHandle or FileSystemDirectoryHandle of each
108649048b14Sstephan         entry (noting that both are instanceof FileSystemHandle). 2)
108749048b14Sstephan         the FileSystemDirectoryHandle of the parent directory. 3) the
108849048b14Sstephan         current depth level, with 0 being at the top of the tree
108949048b14Sstephan         relative to the starting directory. If the callback returns a
109049048b14Sstephan         literal false, as opposed to any other falsy value, traversal
109149048b14Sstephan         stops without an error. Any exceptions it throws are
109249048b14Sstephan         propagated. Results are undefined if the callback manipulate
109349048b14Sstephan         the filesystem (e.g. removing or adding entries) because the
109449048b14Sstephan         how OPFS iterators behave in the face of such changes is
109549048b14Sstephan         undocumented.
109649048b14Sstephan
109749048b14Sstephan       - `recursive` [bool=true]: specifies whether to recurse into
109849048b14Sstephan         subdirectories or not. Whether recursion is depth-first or
109949048b14Sstephan         breadth-first is unspecified!
110049048b14Sstephan
110149048b14Sstephan       - `directory` [FileSystemDirectoryEntry=sqlite3.opfs.rootDirectory]
110249048b14Sstephan         specifies the starting directory.
110349048b14Sstephan
110449048b14Sstephan       If this function is passed a function, it is assumed to be the
110549048b14Sstephan       callback.
110649048b14Sstephan
110749048b14Sstephan       Returns a promise because it has to (by virtue of being async)
110849048b14Sstephan       but that promise has no specific meaning: the traversal it
110949048b14Sstephan       performs is synchronous. The promise must be used to catch any
111049048b14Sstephan       exceptions propagated by the callback, however.
111149048b14Sstephan
111249048b14Sstephan       TODO: add an option which specifies whether to traverse
111349048b14Sstephan       depth-first or breadth-first. We currently do depth-first but
111449048b14Sstephan       an incremental file browsing widget would benefit more from
111549048b14Sstephan       breadth-first.
111649048b14Sstephan    */
111749048b14Sstephan    opfsUtil.traverse = async function(opt){
111849048b14Sstephan      const defaultOpt = {
111949048b14Sstephan        recursive: true,
112049048b14Sstephan        directory: opfsUtil.rootDirectory
112149048b14Sstephan      };
112249048b14Sstephan      if('function'===typeof opt){
112349048b14Sstephan        opt = {callback:opt};
112449048b14Sstephan      }
112549048b14Sstephan      opt = Object.assign(defaultOpt, opt||{});
112649048b14Sstephan      const doDir = async function callee(dirHandle, depth){
112749048b14Sstephan        for await (const handle of dirHandle.values()){
112849048b14Sstephan          if(false === opt.callback(handle, dirHandle, depth)) return false;
112949048b14Sstephan          else if(opt.recursive && 'directory' === handle.kind){
113049048b14Sstephan            if(false === await callee(handle, depth + 1)) break;
113149048b14Sstephan          }
113249048b14Sstephan        }
113349048b14Sstephan      };
113449048b14Sstephan      doDir(opt.directory, 0);
113549048b14Sstephan    };
113649048b14Sstephan
113749048b14Sstephan    //TODO to support fiddle and worker1 db upload:
11383d645484Sstephan    //opfsUtil.createFile = function(absName, content=undefined){...}
11393d645484Sstephan
1140f3860120Sstephan    if(sqlite3.oo1){
1141f3860120Sstephan      opfsUtil.OpfsDb = function(...args){
1142e681b651Sstephan        const opt = sqlite3.oo1.DB.dbCtorHelper.normalizeArgs(...args);
1143f3860120Sstephan        opt.vfs = opfsVfs.$zName;
1144e681b651Sstephan        sqlite3.oo1.DB.dbCtorHelper.call(this, opt);
1145f3860120Sstephan      };
1146f3860120Sstephan      opfsUtil.OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype);
1147e681b651Sstephan      sqlite3.oo1.DB.dbCtorHelper.setVfsPostOpenSql(
11484f5bbedbSstephan        opfsVfs.pointer,
1149c7fb48d4Sstephan        [
11504f5bbedbSstephan          /* Truncate journal mode is faster than delete or wal for
1151c7fb48d4Sstephan             this vfs, per speedtest1. */
1152ed3182f2Sstephan          "pragma journal_mode=truncate;"
1153c7fb48d4Sstephan          /*
1154c7fb48d4Sstephan            This vfs benefits hugely from cache on moderate/large
1155c7fb48d4Sstephan            speedtest1 --size 50 and --size 100 workloads. We currently
1156c7fb48d4Sstephan            rely on setting a non-default cache size when building
1157c7fb48d4Sstephan            sqlite3.wasm. If that policy changes, the cache can
1158c7fb48d4Sstephan            be set here.
1159c7fb48d4Sstephan          */
1160c7fb48d4Sstephan          //"pragma cache_size=-8388608;"
1161c7fb48d4Sstephan        ].join('')
11624f5bbedbSstephan      );
1163f3860120Sstephan    }
1164f3860120Sstephan
1165f3860120Sstephan    /**
1166f3860120Sstephan       Potential TODOs:
1167f3860120Sstephan
1168f3860120Sstephan       - Expose one or both of the Worker objects via opfsUtil and
1169f3860120Sstephan         publish an interface for proxying the higher-level OPFS
1170f3860120Sstephan         features like getting a directory listing.
1171f3860120Sstephan    */
11725e8bb0aaSstephan    const sanityCheck = function(){
1173c5313afeSstephan      const scope = wasm.scopedAllocPush();
1174c5313afeSstephan      const sq3File = new sqlite3_file();
1175c5313afeSstephan      try{
1176c5313afeSstephan        const fid = sq3File.pointer;
1177c5313afeSstephan        const openFlags = capi.SQLITE_OPEN_CREATE
1178c5313afeSstephan              | capi.SQLITE_OPEN_READWRITE
1179c5313afeSstephan        //| capi.SQLITE_OPEN_DELETEONCLOSE
1180c5313afeSstephan              | capi.SQLITE_OPEN_MAIN_DB;
1181c5313afeSstephan        const pOut = wasm.scopedAlloc(8);
1182b8c8d4e4Sstephan        const dbFile = "/sanity/check/file"+randomFilename(8);
1183c5313afeSstephan        const zDbFile = wasm.scopedAllocCString(dbFile);
1184c5313afeSstephan        let rc;
1185e8afca3fSstephan        state.s11n.serialize("This is ä string.");
1186e8afca3fSstephan        rc = state.s11n.deserialize();
1187e8afca3fSstephan        log("deserialize() says:",rc);
1188e8afca3fSstephan        if("This is ä string."!==rc[0]) toss("String d13n error.");
1189c5313afeSstephan        vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
1190c5313afeSstephan        rc = wasm.getMemValue(pOut,'i32');
1191c5313afeSstephan        log("xAccess(",dbFile,") exists ?=",rc);
1192c5313afeSstephan        rc = vfsSyncWrappers.xOpen(opfsVfs.pointer, zDbFile,
1193c5313afeSstephan                                   fid, openFlags, pOut);
1194c4b87be3Sstephan        log("open rc =",rc,"state.sabOPView[xOpen] =",
1195c4b87be3Sstephan            state.sabOPView[state.opIds.xOpen]);
1196e8afca3fSstephan        if(0!==rc){
1197c5313afeSstephan          error("open failed with code",rc);
1198c5313afeSstephan          return;
1199c5313afeSstephan        }
1200c5313afeSstephan        vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
1201c5313afeSstephan        rc = wasm.getMemValue(pOut,'i32');
1202c5313afeSstephan        if(!rc) toss("xAccess() failed to detect file.");
1203c5313afeSstephan        rc = ioSyncWrappers.xSync(sq3File.pointer, 0);
1204c5313afeSstephan        if(rc) toss('sync failed w/ rc',rc);
1205c5313afeSstephan        rc = ioSyncWrappers.xTruncate(sq3File.pointer, 1024);
1206c5313afeSstephan        if(rc) toss('truncate failed w/ rc',rc);
1207c5313afeSstephan        wasm.setMemValue(pOut,0,'i64');
1208c5313afeSstephan        rc = ioSyncWrappers.xFileSize(sq3File.pointer, pOut);
1209c5313afeSstephan        if(rc) toss('xFileSize failed w/ rc',rc);
1210c5313afeSstephan        log("xFileSize says:",wasm.getMemValue(pOut, 'i64'));
1211c5313afeSstephan        rc = ioSyncWrappers.xWrite(sq3File.pointer, zDbFile, 10, 1);
1212c5313afeSstephan        if(rc) toss("xWrite() failed!");
1213c5313afeSstephan        const readBuf = wasm.scopedAlloc(16);
1214c5313afeSstephan        rc = ioSyncWrappers.xRead(sq3File.pointer, readBuf, 6, 2);
1215c5313afeSstephan        wasm.setMemValue(readBuf+6,0);
1216c5313afeSstephan        let jRead = wasm.cstringToJs(readBuf);
1217c5313afeSstephan        log("xRead() got:",jRead);
1218c5313afeSstephan        if("sanity"!==jRead) toss("Unexpected xRead() value.");
12198766fd20Sstephan        if(vfsSyncWrappers.xSleep){
1220c5313afeSstephan          log("xSleep()ing before close()ing...");
12218766fd20Sstephan          vfsSyncWrappers.xSleep(opfsVfs.pointer,2000);
12228766fd20Sstephan          log("waking up from xSleep()");
12238766fd20Sstephan        }
1224c5313afeSstephan        rc = ioSyncWrappers.xClose(fid);
1225c4b87be3Sstephan        log("xClose rc =",rc,"sabOPView =",state.sabOPView);
1226c5313afeSstephan        log("Deleting file:",dbFile);
1227c5313afeSstephan        vfsSyncWrappers.xDelete(opfsVfs.pointer, zDbFile, 0x1234);
1228c5313afeSstephan        vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
1229c5313afeSstephan        rc = wasm.getMemValue(pOut,'i32');
1230c5313afeSstephan        if(rc) toss("Expecting 0 from xAccess(",dbFile,") after xDelete().");
1231c9e2602eSstephan        warn("End of OPFS sanity checks.");
1232c5313afeSstephan      }finally{
1233c5313afeSstephan        sq3File.dispose();
1234c5313afeSstephan        wasm.scopedAllocPop(scope);
1235c5313afeSstephan      }
1236c5313afeSstephan    }/*sanityCheck()*/;
1237c5313afeSstephan
1238c5313afeSstephan    W.onmessage = function({data}){
1239c5313afeSstephan      //log("Worker.onmessage:",data);
1240c5313afeSstephan      switch(data.type){
1241138647a5Sstephan          case 'opfs-async-loaded':
1242e8afca3fSstephan            /*Arrives as soon as the asyc proxy finishes loading.
1243e8afca3fSstephan              Pass our config and shared state on to the async worker.*/
12445e8bb0aaSstephan            W.postMessage({type: 'opfs-async-init',args: state});
1245c5313afeSstephan            break;
1246138647a5Sstephan          case 'opfs-async-inited':{
1247e8afca3fSstephan            /*Indicates that the async partner has received the 'init'
1248e8afca3fSstephan              and has finished initializing, so the real work can
1249e8afca3fSstephan              begin...*/
1250c5313afeSstephan            try {
12510e0687ccSstephan              const rc = capi.sqlite3_vfs_register(opfsVfs.pointer, 0);
12523961b263Sstephan              if(rc){
12533961b263Sstephan                toss("sqlite3_vfs_register(OPFS) failed with rc",rc);
12543961b263Sstephan              }
1255c5313afeSstephan              if(opfsVfs.pointer !== capi.sqlite3_vfs_find("opfs")){
1256c5313afeSstephan                toss("BUG: sqlite3_vfs_find() failed for just-installed OPFS VFS");
1257c5313afeSstephan              }
1258c5313afeSstephan              capi.sqlite3_vfs_register.addReference(opfsVfs, opfsIoMethods);
1259c4b87be3Sstephan              state.sabOPView = new Int32Array(state.sabOP);
1260138647a5Sstephan              state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
1261138647a5Sstephan              state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
1262138647a5Sstephan              initS11n();
1263c5313afeSstephan              if(options.sanityChecks){
1264c5313afeSstephan                warn("Running sanity checks because of opfs-sanity-check URL arg...");
1265c5313afeSstephan                sanityCheck();
1266c5313afeSstephan              }
12671f095d48Sstephan              navigator.storage.getDirectory().then((d)=>{
1268f3860120Sstephan                W.onerror = W._originalOnError;
1269f3860120Sstephan                delete W._originalOnError;
1270f3860120Sstephan                sqlite3.opfs = opfsUtil;
12711f095d48Sstephan                opfsUtil.rootDirectory = d;
1272c5313afeSstephan                log("End of OPFS sqlite3_vfs setup.", opfsVfs);
1273509f4052Sstephan                promiseResolve(sqlite3);
12741f095d48Sstephan              });
1275c5313afeSstephan            }catch(e){
1276c5313afeSstephan              error(e);
1277c5313afeSstephan              promiseReject(e);
1278c5313afeSstephan            }
1279c5313afeSstephan            break;
1280c5313afeSstephan          }
1281c5313afeSstephan          default:
1282c5313afeSstephan            promiseReject(e);
1283c5313afeSstephan            error("Unexpected message from the async worker:",data);
1284c5313afeSstephan            break;
1285f861b36bSstephan      }/*switch(data.type)*/
1286f861b36bSstephan    }/*W.onmessage()*/;
1287c5313afeSstephan  })/*thePromise*/;
1288c5313afeSstephan  return thePromise;
1289c5313afeSstephan}/*installOpfsVfs()*/;
12905b9973d8SstephaninstallOpfsVfs.defaultProxyUri =
12915b9973d8Sstephan  "sqlite3-opfs-async-proxy.js";
12929a55773bSstephanself.sqlite3ApiBootstrap.initializersAsync.push(async (sqlite3)=>{
1293cd0df83cSstephan  if(sqlite3.scriptInfo && !sqlite3.scriptInfo.isWorker){
1294cd0df83cSstephan    return;
1295cd0df83cSstephan  }
12969a55773bSstephan  try{
1297cd0df83cSstephan    let proxyJs = installOpfsVfs.defaultProxyUri;
1298cd0df83cSstephan    if(sqlite3.scriptInfo.sqlite3Dir){
1299cd0df83cSstephan      installOpfsVfs.defaultProxyUri =
1300cd0df83cSstephan        sqlite3.scriptInfo.sqlite3Dir + proxyJs;
1301cd0df83cSstephan      //console.warn("installOpfsVfs.defaultProxyUri =",installOpfsVfs.defaultProxyUri);
1302cd0df83cSstephan    }
1303ff891b4eSstephan    return installOpfsVfs().catch((e)=>{
1304cd0df83cSstephan      console.warn("Ignoring inability to install OPFS sqlite3_vfs:",e.message);
1305ff891b4eSstephan    });
13069a55773bSstephan  }catch(e){
13079a55773bSstephan    console.error("installOpfsVfs() exception:",e);
13089a55773bSstephan    throw e;
13099a55773bSstephan  }
13109a55773bSstephan});
1311c5313afeSstephan}/*sqlite3ApiBootstrap.initializers.push()*/);
1312