1/*
2  2022-09-16
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  A Worker which manages asynchronous OPFS handles on behalf of a
14  synchronous API which controls it via a combination of Worker
15  messages, SharedArrayBuffer, and Atomics. It is the asynchronous
16  counterpart of the API defined in sqlite3-api-opfs.js.
17
18  Highly indebted to:
19
20  https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/OriginPrivateFileSystemVFS.js
21
22  for demonstrating how to use the OPFS APIs.
23
24  This file is to be loaded as a Worker. It does not have any direct
25  access to the sqlite3 JS/WASM bits, so any bits which it needs (most
26  notably SQLITE_xxx integer codes) have to be imported into it via an
27  initialization process.
28
29  This file represents an implementation detail of a larger piece of
30  code, and not a public interface. Its details may change at any time
31  and are not intended to be used by any client-level code.
32*/
33"use strict";
34const toss = function(...args){throw new Error(args.join(' '))};
35if(self.window === self){
36  toss("This code cannot run from the main thread.",
37       "Load it as a Worker from a separate Worker.");
38}else if(!navigator.storage.getDirectory){
39  toss("This API requires navigator.storage.getDirectory.");
40}
41
42/**
43   Will hold state copied to this object from the syncronous side of
44   this API.
45*/
46const state = Object.create(null);
47
48/**
49   verbose:
50
51   0 = no logging output
52   1 = only errors
53   2 = warnings and errors
54   3 = debug, warnings, and errors
55*/
56state.verbose = 2;
57
58const loggers = {
59  0:console.error.bind(console),
60  1:console.warn.bind(console),
61  2:console.log.bind(console)
62};
63const logImpl = (level,...args)=>{
64  if(state.verbose>level) loggers[level]("OPFS asyncer:",...args);
65};
66const log =    (...args)=>logImpl(2, ...args);
67const warn =   (...args)=>logImpl(1, ...args);
68const error =  (...args)=>logImpl(0, ...args);
69const metrics = Object.create(null);
70metrics.reset = ()=>{
71  let k;
72  const r = (m)=>(m.count = m.time = m.wait = 0);
73  for(k in state.opIds){
74    r(metrics[k] = Object.create(null));
75  }
76  let s = metrics.s11n = Object.create(null);
77  s = s.serialize = Object.create(null);
78  s.count = s.time = 0;
79  s = metrics.s11n.deserialize = Object.create(null);
80  s.count = s.time = 0;
81};
82metrics.dump = ()=>{
83  let k, n = 0, t = 0, w = 0;
84  for(k in state.opIds){
85    const m = metrics[k];
86    n += m.count;
87    t += m.time;
88    w += m.wait;
89    m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
90  }
91  console.log(self.location.href,
92              "metrics for",self.location.href,":\n",
93              metrics,
94              "\nTotal of",n,"op(s) for",t,"ms",
95              "approx",w,"ms spent waiting on OPFS APIs.");
96  console.log("Serialization metrics:",metrics.s11n);
97};
98
99/**
100   __openFiles is a map of sqlite3_file pointers (integers) to
101   metadata related to a given OPFS file handles. The pointers are, in
102   this side of the interface, opaque file handle IDs provided by the
103   synchronous part of this constellation. Each value is an object
104   with a structure demonstrated in the xOpen() impl.
105*/
106const __openFiles = Object.create(null);
107/**
108   __autoLocks is a Set of sqlite3_file pointers (integers) which were
109   "auto-locked".  i.e. those for which we obtained a sync access
110   handle without an explicit xLock() call. Such locks will be
111   released during db connection idle time, whereas a sync access
112   handle obtained via xLock(), or subsequently xLock()'d after
113   auto-acquisition, will not be released until xUnlock() is called.
114
115   Maintenance reminder: if we relinquish auto-locks at the end of the
116   operation which acquires them, we pay a massive performance
117   penalty: speedtest1 benchmarks take up to 4x as long. By delaying
118   the lock release until idle time, the hit is negligible.
119*/
120const __autoLocks = new Set();
121
122/**
123   Expects an OPFS file path. It gets resolved, such that ".."
124   components are properly expanded, and returned. If the 2nd arg is
125   true, the result is returned as an array of path elements, else an
126   absolute path string is returned.
127*/
128const getResolvedPath = function(filename,splitIt){
129  const p = new URL(
130    filename, 'file://irrelevant'
131  ).pathname;
132  return splitIt ? p.split('/').filter((v)=>!!v) : p;
133};
134
135/**
136   Takes the absolute path to a filesystem element. Returns an array
137   of [handleOfContainingDir, filename]. If the 2nd argument is truthy
138   then each directory element leading to the file is created along
139   the way. Throws if any creation or resolution fails.
140*/
141const getDirForFilename = async function f(absFilename, createDirs = false){
142  const path = getResolvedPath(absFilename, true);
143  const filename = path.pop();
144  let dh = state.rootDir;
145  for(const dirName of path){
146    if(dirName){
147      dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs});
148    }
149  }
150  return [dh, filename];
151};
152
153/**
154   An error class specifically for use with getSyncHandle(), the goal
155   of which is to eventually be able to distinguish unambiguously
156   between locking-related failures and other types, noting that we
157   cannot currently do so because createSyncAccessHandle() does not
158   define its exceptions in the required level of detail.
159*/
160class GetSyncHandleError extends Error {
161  constructor(errorObject, ...msg){
162    super();
163    this.error = errorObject;
164    this.message = [
165      ...msg, ': Original exception ['+errorObject.name+']:',
166      errorObject.message
167    ].join(' ');
168    this.name = 'GetSyncHandleError';
169  }
170};
171
172/**
173   Returns the sync access handle associated with the given file
174   handle object (which must be a valid handle object, as created by
175   xOpen()), lazily opening it if needed.
176
177   In order to help alleviate cross-tab contention for a dabase,
178   if an exception is thrown while acquiring the handle, this routine
179   will wait briefly and try again, up to 3 times. If acquisition
180   still fails at that point it will give up and propagate the
181   exception.
182*/
183const getSyncHandle = async (fh)=>{
184  if(!fh.syncHandle){
185    const t = performance.now();
186    log("Acquiring sync handle for",fh.filenameAbs);
187    const maxTries = 4, msBase = 300;
188    let i = 1, ms = msBase;
189    for(; true; ms = msBase * ++i){
190      try {
191        //if(i<3) toss("Just testing getSyncHandle() wait-and-retry.");
192        //TODO? A config option which tells it to throw here
193        //randomly every now and then, for testing purposes.
194        fh.syncHandle = await fh.fileHandle.createSyncAccessHandle();
195        break;
196      }catch(e){
197        if(i === maxTries){
198          throw new GetSyncHandleError(
199            e, "Error getting sync handle.",maxTries,
200            "attempts failed.",fh.filenameAbs
201          );
202        }
203        warn("Error getting sync handle. Waiting",ms,
204              "ms and trying again.",fh.filenameAbs,e);
205        Atomics.wait(state.sabOPView, state.opIds.retry, 0, ms);
206      }
207    }
208    log("Got sync handle for",fh.filenameAbs,'in',performance.now() - t,'ms');
209    if(!fh.xLock){
210      __autoLocks.add(fh.fid);
211      log("Auto-locked",fh.fid,fh.filenameAbs);
212    }
213  }
214  return fh.syncHandle;
215};
216
217/**
218   If the given file-holding object has a sync handle attached to it,
219   that handle is remove and asynchronously closed. Though it may
220   sound sensible to continue work as soon as the close() returns
221   (noting that it's asynchronous), doing so can cause operations
222   performed soon afterwards, e.g. a call to getSyncHandle() to fail
223   because they may happen out of order from the close(). OPFS does
224   not guaranty that the actual order of operations is retained in
225   such cases. i.e.  always "await" on the result of this function.
226*/
227const closeSyncHandle = async (fh)=>{
228  if(fh.syncHandle){
229    log("Closing sync handle for",fh.filenameAbs);
230    const h = fh.syncHandle;
231    delete fh.syncHandle;
232    delete fh.xLock;
233    __autoLocks.delete(fh.fid);
234    return h.close();
235  }
236};
237
238/**
239   A proxy for closeSyncHandle() which is guaranteed to not throw.
240
241   This function is part of a lock/unlock step in functions which
242   require a sync access handle but may be called without xLock()
243   having been called first. Such calls need to release that
244   handle to avoid locking the file for all of time. This is an
245   _attempt_ at reducing cross-tab contention but it may prove
246   to be more of a problem than a solution and may need to be
247   removed.
248*/
249const closeSyncHandleNoThrow = async (fh)=>{
250  try{await closeSyncHandle(fh)}
251  catch(e){
252    warn("closeSyncHandleNoThrow() ignoring:",e,fh);
253  }
254};
255
256/**
257   Stores the given value at state.sabOPView[state.opIds.rc] and then
258   Atomics.notify()'s it.
259*/
260const storeAndNotify = (opName, value)=>{
261  log(opName+"() => notify(",value,")");
262  Atomics.store(state.sabOPView, state.opIds.rc, value);
263  Atomics.notify(state.sabOPView, state.opIds.rc);
264};
265
266/**
267   Throws if fh is a file-holding object which is flagged as read-only.
268*/
269const affirmNotRO = function(opName,fh){
270  if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs);
271};
272const affirmLocked = function(opName,fh){
273  //if(!fh.syncHandle) toss(opName+"(): File does not have a lock: "+fh.filenameAbs);
274  /**
275     Currently a no-op, as speedtest1 triggers xRead() without a
276     lock (that seems like a bug but it's currently uninvestigated).
277     This means, however, that some OPFS VFS routines may trigger
278     acquisition of a lock but never let it go until xUnlock() is
279     called (which it likely won't be if xLock() was not called).
280  */
281};
282
283/**
284   We track 2 different timers: the "metrics" timer records how much
285   time we spend performing work. The "wait" timer records how much
286   time we spend waiting on the underlying OPFS timer. See the calls
287   to mTimeStart(), mTimeEnd(), wTimeStart(), and wTimeEnd()
288   throughout this file to see how they're used.
289*/
290const __mTimer = Object.create(null);
291__mTimer.op = undefined;
292__mTimer.start = undefined;
293const mTimeStart = (op)=>{
294  __mTimer.start = performance.now();
295  __mTimer.op = op;
296  //metrics[op] || toss("Maintenance required: missing metrics for",op);
297  ++metrics[op].count;
298};
299const mTimeEnd = ()=>(
300  metrics[__mTimer.op].time += performance.now() - __mTimer.start
301);
302const __wTimer = Object.create(null);
303__wTimer.op = undefined;
304__wTimer.start = undefined;
305const wTimeStart = (op)=>{
306  __wTimer.start = performance.now();
307  __wTimer.op = op;
308  //metrics[op] || toss("Maintenance required: missing metrics for",op);
309};
310const wTimeEnd = ()=>(
311  metrics[__wTimer.op].wait += performance.now() - __wTimer.start
312);
313
314/**
315   Gets set to true by the 'opfs-async-shutdown' command to quit the
316   wait loop. This is only intended for debugging purposes: we cannot
317   inspect this file's state while the tight waitLoop() is running and
318   need a way to stop that loop for introspection purposes.
319*/
320let flagAsyncShutdown = false;
321
322
323/**
324   Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods
325   methods, as well as helpers like mkdir(). Maintenance reminder:
326   members are in alphabetical order to simplify finding them.
327*/
328const vfsAsyncImpls = {
329  'opfs-async-metrics': async ()=>{
330    mTimeStart('opfs-async-metrics');
331    metrics.dump();
332    storeAndNotify('opfs-async-metrics', 0);
333    mTimeEnd();
334  },
335  'opfs-async-shutdown': async ()=>{
336    flagAsyncShutdown = true;
337    storeAndNotify('opfs-async-shutdown', 0);
338  },
339  mkdir: async (dirname)=>{
340    mTimeStart('mkdir');
341    let rc = 0;
342    wTimeStart('mkdir');
343    try {
344        await getDirForFilename(dirname+"/filepart", true);
345    }catch(e){
346      state.s11n.storeException(2,e);
347      rc = state.sq3Codes.SQLITE_IOERR;
348    }finally{
349      wTimeEnd();
350    }
351    storeAndNotify('mkdir', rc);
352    mTimeEnd();
353  },
354  xAccess: async (filename)=>{
355    mTimeStart('xAccess');
356    /* OPFS cannot support the full range of xAccess() queries sqlite3
357       calls for. We can essentially just tell if the file is
358       accessible, but if it is it's automatically writable (unless
359       it's locked, which we cannot(?) know without trying to open
360       it). OPFS does not have the notion of read-only.
361
362       The return semantics of this function differ from sqlite3's
363       xAccess semantics because we are limited in what we can
364       communicate back to our synchronous communication partner: 0 =
365       accessible, non-0 means not accessible.
366    */
367    let rc = 0;
368    wTimeStart('xAccess');
369    try{
370      const [dh, fn] = await getDirForFilename(filename);
371      await dh.getFileHandle(fn);
372    }catch(e){
373      state.s11n.storeException(2,e);
374      rc = state.sq3Codes.SQLITE_IOERR;
375    }finally{
376      wTimeEnd();
377    }
378    storeAndNotify('xAccess', rc);
379    mTimeEnd();
380  },
381  xClose: async function(fid/*sqlite3_file pointer*/){
382    const opName = 'xClose';
383    mTimeStart(opName);
384    __autoLocks.delete(fid);
385    const fh = __openFiles[fid];
386    let rc = 0;
387    wTimeStart(opName);
388    if(fh){
389      delete __openFiles[fid];
390      await closeSyncHandle(fh);
391      if(fh.deleteOnClose){
392        try{ await fh.dirHandle.removeEntry(fh.filenamePart) }
393        catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) }
394      }
395    }else{
396      state.s11n.serialize();
397      rc = state.sq3Codes.SQLITE_NOTFOUND;
398    }
399    wTimeEnd();
400    storeAndNotify(opName, rc);
401    mTimeEnd();
402  },
403  xDelete: async function(...args){
404    mTimeStart('xDelete');
405    const rc = await vfsAsyncImpls.xDeleteNoWait(...args);
406    storeAndNotify('xDelete', rc);
407    mTimeEnd();
408  },
409  xDeleteNoWait: async function(filename, syncDir = 0, recursive = false){
410    /* The syncDir flag is, for purposes of the VFS API's semantics,
411       ignored here. However, if it has the value 0x1234 then: after
412       deleting the given file, recursively try to delete any empty
413       directories left behind in its wake (ignoring any errors and
414       stopping at the first failure).
415
416       That said: we don't know for sure that removeEntry() fails if
417       the dir is not empty because the API is not documented. It has,
418       however, a "recursive" flag which defaults to false, so
419       presumably it will fail if the dir is not empty and that flag
420       is false.
421    */
422    let rc = 0;
423    wTimeStart('xDelete');
424    try {
425      while(filename){
426        const [hDir, filenamePart] = await getDirForFilename(filename, false);
427        if(!filenamePart) break;
428        await hDir.removeEntry(filenamePart, {recursive});
429        if(0x1234 !== syncDir) break;
430        recursive = false;
431        filename = getResolvedPath(filename, true);
432        filename.pop();
433        filename = filename.join('/');
434      }
435    }catch(e){
436      state.s11n.storeException(2,e);
437      rc = state.sq3Codes.SQLITE_IOERR_DELETE;
438    }
439    wTimeEnd();
440    return rc;
441  },
442  xFileSize: async function(fid/*sqlite3_file pointer*/){
443    mTimeStart('xFileSize');
444    const fh = __openFiles[fid];
445    let rc;
446    wTimeStart('xFileSize');
447    try{
448      affirmLocked('xFileSize',fh);
449      rc = await (await getSyncHandle(fh)).getSize();
450      state.s11n.serialize(Number(rc));
451      rc = 0;
452    }catch(e){
453      state.s11n.storeException(2,e);
454      rc = state.sq3Codes.SQLITE_IOERR;
455    }
456    wTimeEnd();
457    storeAndNotify('xFileSize', rc);
458    mTimeEnd();
459  },
460  xLock: async function(fid/*sqlite3_file pointer*/,
461                        lockType/*SQLITE_LOCK_...*/){
462    mTimeStart('xLock');
463    const fh = __openFiles[fid];
464    let rc = 0;
465    const oldLockType = fh.xLock;
466    fh.xLock = lockType;
467    if( !fh.syncHandle ){
468      wTimeStart('xLock');
469      try {
470        await getSyncHandle(fh);
471        __autoLocks.delete(fid);
472      }catch(e){
473        state.s11n.storeException(1,e);
474        rc = state.sq3Codes.SQLITE_IOERR_LOCK;
475        fh.xLock = oldLockType;
476      }
477      wTimeEnd();
478    }
479    storeAndNotify('xLock',rc);
480    mTimeEnd();
481  },
482  xOpen: async function(fid/*sqlite3_file pointer*/, filename,
483                        flags/*SQLITE_OPEN_...*/){
484    const opName = 'xOpen';
485    mTimeStart(opName);
486    const deleteOnClose = (state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags);
487    const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags);
488    wTimeStart('xOpen');
489    try{
490      let hDir, filenamePart;
491      try {
492        [hDir, filenamePart] = await getDirForFilename(filename, !!create);
493      }catch(e){
494        state.s11n.storeException(1,e);
495        storeAndNotify(opName, state.sq3Codes.SQLITE_NOTFOUND);
496        mTimeEnd();
497        wTimeEnd();
498        return;
499      }
500      const hFile = await hDir.getFileHandle(filenamePart, {create});
501      /**
502         wa-sqlite, at this point, grabs a SyncAccessHandle and
503         assigns it to the syncHandle prop of the file state
504         object, but only for certain cases and it's unclear why it
505         places that limitation on it.
506      */
507      wTimeEnd();
508      __openFiles[fid] = Object.assign(Object.create(null),{
509        fid: fid,
510        filenameAbs: filename,
511        filenamePart: filenamePart,
512        dirHandle: hDir,
513        fileHandle: hFile,
514        sabView: state.sabFileBufView,
515        readOnly: create
516          ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags),
517        deleteOnClose: deleteOnClose
518      });
519      storeAndNotify(opName, 0);
520    }catch(e){
521      wTimeEnd();
522      error(opName,e);
523      state.s11n.storeException(1,e);
524      storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
525    }
526    mTimeEnd();
527  },
528  xRead: async function(fid/*sqlite3_file pointer*/,n,offset64){
529    mTimeStart('xRead');
530    let rc = 0, nRead;
531    const fh = __openFiles[fid];
532    try{
533      affirmLocked('xRead',fh);
534      wTimeStart('xRead');
535      nRead = (await getSyncHandle(fh)).read(
536        fh.sabView.subarray(0, n),
537        {at: Number(offset64)}
538      );
539      wTimeEnd();
540      if(nRead < n){/* Zero-fill remaining bytes */
541        fh.sabView.fill(0, nRead, n);
542        rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
543      }
544    }catch(e){
545      if(undefined===nRead) wTimeEnd();
546      error("xRead() failed",e,fh);
547      state.s11n.storeException(1,e);
548      rc = state.sq3Codes.SQLITE_IOERR_READ;
549    }
550    storeAndNotify('xRead',rc);
551    mTimeEnd();
552  },
553  xSync: async function(fid/*sqlite3_file pointer*/,flags/*ignored*/){
554    mTimeStart('xSync');
555    const fh = __openFiles[fid];
556    let rc = 0;
557    if(!fh.readOnly && fh.syncHandle){
558      try {
559        wTimeStart('xSync');
560        await fh.syncHandle.flush();
561      }catch(e){
562        state.s11n.storeException(2,e);
563        rc = state.sq3Codes.SQLITE_IOERR_FSYNC;
564      }
565      wTimeEnd();
566    }
567    storeAndNotify('xSync',rc);
568    mTimeEnd();
569  },
570  xTruncate: async function(fid/*sqlite3_file pointer*/,size){
571    mTimeStart('xTruncate');
572    let rc = 0;
573    const fh = __openFiles[fid];
574    wTimeStart('xTruncate');
575    try{
576      affirmLocked('xTruncate',fh);
577      affirmNotRO('xTruncate', fh);
578      await (await getSyncHandle(fh)).truncate(size);
579    }catch(e){
580      error("xTruncate():",e,fh);
581      state.s11n.storeException(2,e);
582      rc = state.sq3Codes.SQLITE_IOERR_TRUNCATE;
583    }
584    wTimeEnd();
585    storeAndNotify('xTruncate',rc);
586    mTimeEnd();
587  },
588  xUnlock: async function(fid/*sqlite3_file pointer*/,
589                          lockType/*SQLITE_LOCK_...*/){
590    mTimeStart('xUnlock');
591    let rc = 0;
592    const fh = __openFiles[fid];
593    if( state.sq3Codes.SQLITE_LOCK_NONE===lockType
594        && fh.syncHandle ){
595      wTimeStart('xUnlock');
596      try { await closeSyncHandle(fh) }
597      catch(e){
598        state.s11n.storeException(1,e);
599        rc = state.sq3Codes.SQLITE_IOERR_UNLOCK;
600      }
601      wTimeEnd();
602    }
603    storeAndNotify('xUnlock',rc);
604    mTimeEnd();
605  },
606  xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){
607    mTimeStart('xWrite');
608    let rc;
609    const fh = __openFiles[fid];
610    wTimeStart('xWrite');
611    try{
612      affirmLocked('xWrite',fh);
613      affirmNotRO('xWrite', fh);
614      rc = (
615        n === (await getSyncHandle(fh))
616          .write(fh.sabView.subarray(0, n),
617                 {at: Number(offset64)})
618      ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
619    }catch(e){
620      error("xWrite():",e,fh);
621      state.s11n.storeException(1,e);
622      rc = state.sq3Codes.SQLITE_IOERR_WRITE;
623    }
624    wTimeEnd();
625    storeAndNotify('xWrite',rc);
626    mTimeEnd();
627  }
628}/*vfsAsyncImpls*/;
629
630const initS11n = ()=>{
631  /**
632     ACHTUNG: this code is 100% duplicated in the other half of this
633     proxy! The documentation is maintained in the "synchronous half".
634  */
635  if(state.s11n) return state.s11n;
636  const textDecoder = new TextDecoder(),
637  textEncoder = new TextEncoder('utf-8'),
638  viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize),
639  viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
640  state.s11n = Object.create(null);
641  const TypeIds = Object.create(null);
642  TypeIds.number  = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' };
643  TypeIds.bigint  = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' };
644  TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' };
645  TypeIds.string =  { id: 4 };
646  const getTypeId = (v)=>(
647    TypeIds[typeof v]
648      || toss("Maintenance required: this value type cannot be serialized.",v)
649  );
650  const getTypeIdById = (tid)=>{
651    switch(tid){
652      case TypeIds.number.id: return TypeIds.number;
653      case TypeIds.bigint.id: return TypeIds.bigint;
654      case TypeIds.boolean.id: return TypeIds.boolean;
655      case TypeIds.string.id: return TypeIds.string;
656      default: toss("Invalid type ID:",tid);
657    }
658  };
659  state.s11n.deserialize = function(clear=false){
660    ++metrics.s11n.deserialize.count;
661    const t = performance.now();
662    const argc = viewU8[0];
663    const rc = argc ? [] : null;
664    if(argc){
665      const typeIds = [];
666      let offset = 1, i, n, v;
667      for(i = 0; i < argc; ++i, ++offset){
668        typeIds.push(getTypeIdById(viewU8[offset]));
669      }
670      for(i = 0; i < argc; ++i){
671        const t = typeIds[i];
672        if(t.getter){
673          v = viewDV[t.getter](offset, state.littleEndian);
674          offset += t.size;
675        }else{/*String*/
676          n = viewDV.getInt32(offset, state.littleEndian);
677          offset += 4;
678          v = textDecoder.decode(viewU8.slice(offset, offset+n));
679          offset += n;
680        }
681        rc.push(v);
682      }
683    }
684    if(clear) viewU8[0] = 0;
685    //log("deserialize:",argc, rc);
686    metrics.s11n.deserialize.time += performance.now() - t;
687    return rc;
688  };
689  state.s11n.serialize = function(...args){
690    const t = performance.now();
691    ++metrics.s11n.serialize.count;
692    if(args.length){
693      //log("serialize():",args);
694      const typeIds = [];
695      let i = 0, offset = 1;
696      viewU8[0] = args.length & 0xff /* header = # of args */;
697      for(; i < args.length; ++i, ++offset){
698        /* Write the TypeIds.id value into the next args.length
699           bytes. */
700        typeIds.push(getTypeId(args[i]));
701        viewU8[offset] = typeIds[i].id;
702      }
703      for(i = 0; i < args.length; ++i) {
704        /* Deserialize the following bytes based on their
705           corresponding TypeIds.id from the header. */
706        const t = typeIds[i];
707        if(t.setter){
708          viewDV[t.setter](offset, args[i], state.littleEndian);
709          offset += t.size;
710        }else{/*String*/
711          const s = textEncoder.encode(args[i]);
712          viewDV.setInt32(offset, s.byteLength, state.littleEndian);
713          offset += 4;
714          viewU8.set(s, offset);
715          offset += s.byteLength;
716        }
717      }
718      //log("serialize() result:",viewU8.slice(0,offset));
719    }else{
720      viewU8[0] = 0;
721    }
722    metrics.s11n.serialize.time += performance.now() - t;
723  };
724
725  state.s11n.storeException = state.asyncS11nExceptions
726    ? ((priority,e)=>{
727      if(priority<=state.asyncS11nExceptions){
728        state.s11n.serialize([e.name,': ',e.message].join(""));
729      }
730    })
731    : ()=>{};
732
733  return state.s11n;
734}/*initS11n()*/;
735
736const waitLoop = async function f(){
737  const opHandlers = Object.create(null);
738  for(let k of Object.keys(state.opIds)){
739    const vi = vfsAsyncImpls[k];
740    if(!vi) continue;
741    const o = Object.create(null);
742    opHandlers[state.opIds[k]] = o;
743    o.key = k;
744    o.f = vi;
745  }
746  /**
747     waitTime is how long (ms) to wait for each Atomics.wait().
748     We need to wake up periodically to give the thread a chance
749     to do other things.
750  */
751  const waitTime = 500;
752  while(!flagAsyncShutdown){
753    try {
754      if('timed-out'===Atomics.wait(
755        state.sabOPView, state.opIds.whichOp, 0, waitTime
756      )){
757        if(__autoLocks.size){
758          /* Release all auto-locks. */
759          for(const fid of __autoLocks){
760            const fh = __openFiles[fid];
761            await closeSyncHandleNoThrow(fh);
762            log("Auto-unlocked",fid,fh.filenameAbs);
763          }
764        }
765        continue;
766      }
767      const opId = Atomics.load(state.sabOPView, state.opIds.whichOp);
768      Atomics.store(state.sabOPView, state.opIds.whichOp, 0);
769      const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId);
770      const args = state.s11n.deserialize(
771        true /* clear s11n to keep the caller from confusing this with
772                an exception string written by the upcoming
773                operation */
774      ) || [];
775      //warn("waitLoop() whichOp =",opId, hnd, args);
776      if(hnd.f) await hnd.f(...args);
777      else error("Missing callback for opId",opId);
778    }catch(e){
779      error('in waitLoop():',e);
780    }
781  }
782};
783
784navigator.storage.getDirectory().then(function(d){
785  const wMsg = (type)=>postMessage({type});
786  state.rootDir = d;
787  self.onmessage = function({data}){
788    switch(data.type){
789        case 'opfs-async-init':{
790          /* Receive shared state from synchronous partner */
791          const opt = data.args;
792          state.littleEndian = opt.littleEndian;
793          state.asyncS11nExceptions = opt.asyncS11nExceptions;
794          state.verbose = opt.verbose ?? 2;
795          state.fileBufferSize = opt.fileBufferSize;
796          state.sabS11nOffset = opt.sabS11nOffset;
797          state.sabS11nSize = opt.sabS11nSize;
798          state.sabOP = opt.sabOP;
799          state.sabOPView = new Int32Array(state.sabOP);
800          state.sabIO = opt.sabIO;
801          state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
802          state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
803          state.opIds = opt.opIds;
804          state.sq3Codes = opt.sq3Codes;
805          Object.keys(vfsAsyncImpls).forEach((k)=>{
806            if(!Number.isFinite(state.opIds[k])){
807              toss("Maintenance required: missing state.opIds[",k,"]");
808            }
809          });
810          initS11n();
811          metrics.reset();
812          log("init state",state);
813          wMsg('opfs-async-inited');
814          waitLoop();
815          break;
816        }
817        case 'opfs-async-restart':
818          if(flagAsyncShutdown){
819            warn("Restarting after opfs-async-shutdown. Might or might not work.");
820            flagAsyncShutdown = false;
821            waitLoop();
822          }
823          break;
824        case 'opfs-async-metrics':
825          metrics.dump();
826          break;
827    }
828  };
829  wMsg('opfs-async-loaded');
830}).catch((e)=>error("error initializing OPFS asyncer:",e));
831