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