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};
233
234/**
235   We track 2 different timers: the "metrics" timer records how much
236   time we spend performing work. The "wait" timer records how much
237   time we spend waiting on the underlying OPFS timer. See the calls
238   to mTimeStart(), mTimeEnd(), wTimeStart(), and wTimeEnd()
239   throughout this file to see how they're used.
240*/
241const __mTimer = Object.create(null);
242__mTimer.op = undefined;
243__mTimer.start = undefined;
244const mTimeStart = (op)=>{
245  __mTimer.start = performance.now();
246  __mTimer.op = op;
247  //metrics[op] || toss("Maintenance required: missing metrics for",op);
248  ++metrics[op].count;
249};
250const mTimeEnd = ()=>(
251  metrics[__mTimer.op].time += performance.now() - __mTimer.start
252);
253const __wTimer = Object.create(null);
254__wTimer.op = undefined;
255__wTimer.start = undefined;
256const wTimeStart = (op)=>{
257  __wTimer.start = performance.now();
258  __wTimer.op = op;
259  //metrics[op] || toss("Maintenance required: missing metrics for",op);
260};
261const wTimeEnd = ()=>(
262  metrics[__wTimer.op].wait += performance.now() - __wTimer.start
263);
264
265/**
266   Gets set to true by the 'opfs-async-shutdown' command to quit the
267   wait loop. This is only intended for debugging purposes: we cannot
268   inspect this file's state while the tight waitLoop() is running and
269   need a way to stop that loop for introspection purposes.
270*/
271let flagAsyncShutdown = false;
272
273
274/**
275   Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods
276   methods, as well as helpers like mkdir(). Maintenance reminder:
277   members are in alphabetical order to simplify finding them.
278*/
279const vfsAsyncImpls = {
280  'opfs-async-metrics': async ()=>{
281    mTimeStart('opfs-async-metrics');
282    metrics.dump();
283    storeAndNotify('opfs-async-metrics', 0);
284    mTimeEnd();
285  },
286  'opfs-async-shutdown': async ()=>{
287    flagAsyncShutdown = true;
288    storeAndNotify('opfs-async-shutdown', 0);
289  },
290  mkdir: async (dirname)=>{
291    mTimeStart('mkdir');
292    let rc = 0;
293    wTimeStart('mkdir');
294    try {
295        await getDirForFilename(dirname+"/filepart", true);
296    }catch(e){
297      state.s11n.storeException(2,e);
298      rc = state.sq3Codes.SQLITE_IOERR;
299    }finally{
300      wTimeEnd();
301    }
302    storeAndNotify('mkdir', rc);
303    mTimeEnd();
304  },
305  xAccess: async (filename)=>{
306    mTimeStart('xAccess');
307    /* OPFS cannot support the full range of xAccess() queries sqlite3
308       calls for. We can essentially just tell if the file is
309       accessible, but if it is it's automatically writable (unless
310       it's locked, which we cannot(?) know without trying to open
311       it). OPFS does not have the notion of read-only.
312
313       The return semantics of this function differ from sqlite3's
314       xAccess semantics because we are limited in what we can
315       communicate back to our synchronous communication partner: 0 =
316       accessible, non-0 means not accessible.
317    */
318    let rc = 0;
319    wTimeStart('xAccess');
320    try{
321      const [dh, fn] = await getDirForFilename(filename);
322      await dh.getFileHandle(fn);
323    }catch(e){
324      state.s11n.storeException(2,e);
325      rc = state.sq3Codes.SQLITE_IOERR;
326    }finally{
327      wTimeEnd();
328    }
329    storeAndNotify('xAccess', rc);
330    mTimeEnd();
331  },
332  xClose: async function(fid/*sqlite3_file pointer*/){
333    const opName = 'xClose';
334    mTimeStart(opName);
335    const fh = __openFiles[fid];
336    let rc = 0;
337    wTimeStart('xClose');
338    if(fh){
339      delete __openFiles[fid];
340      await closeSyncHandle(fh);
341      if(fh.deleteOnClose){
342        try{ await fh.dirHandle.removeEntry(fh.filenamePart) }
343        catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) }
344      }
345    }else{
346      state.s11n.serialize();
347      rc = state.sq3Codes.SQLITE_NOTFOUND;
348    }
349    wTimeEnd();
350    storeAndNotify(opName, rc);
351    mTimeEnd();
352  },
353  xDelete: async function(...args){
354    mTimeStart('xDelete');
355    const rc = await vfsAsyncImpls.xDeleteNoWait(...args);
356    storeAndNotify('xDelete', rc);
357    mTimeEnd();
358  },
359  xDeleteNoWait: async function(filename, syncDir = 0, recursive = false){
360    /* The syncDir flag is, for purposes of the VFS API's semantics,
361       ignored here. However, if it has the value 0x1234 then: after
362       deleting the given file, recursively try to delete any empty
363       directories left behind in its wake (ignoring any errors and
364       stopping at the first failure).
365
366       That said: we don't know for sure that removeEntry() fails if
367       the dir is not empty because the API is not documented. It has,
368       however, a "recursive" flag which defaults to false, so
369       presumably it will fail if the dir is not empty and that flag
370       is false.
371    */
372    let rc = 0;
373    wTimeStart('xDelete');
374    try {
375      while(filename){
376        const [hDir, filenamePart] = await getDirForFilename(filename, false);
377        if(!filenamePart) break;
378        await hDir.removeEntry(filenamePart, {recursive});
379        if(0x1234 !== syncDir) break;
380        filename = getResolvedPath(filename, true);
381        filename.pop();
382        filename = filename.join('/');
383      }
384    }catch(e){
385      state.s11n.storeException(2,e);
386      rc = state.sq3Codes.SQLITE_IOERR_DELETE;
387    }
388    wTimeEnd();
389    return rc;
390  },
391  xFileSize: async function(fid/*sqlite3_file pointer*/){
392    mTimeStart('xFileSize');
393    const fh = __openFiles[fid];
394    let rc;
395    wTimeStart('xFileSize');
396    try{
397      rc = await (await getSyncHandle(fh)).getSize();
398      state.s11n.serialize(Number(rc));
399      rc = 0;
400    }catch(e){
401      state.s11n.storeException(2,e);
402      rc = state.sq3Codes.SQLITE_IOERR;
403    }
404    wTimeEnd();
405    storeAndNotify('xFileSize', rc);
406    mTimeEnd();
407  },
408  xLock: async function(fid/*sqlite3_file pointer*/,
409                        lockType/*SQLITE_LOCK_...*/){
410    mTimeStart('xLock');
411    const fh = __openFiles[fid];
412    let rc = 0;
413    if( !fh.syncHandle ){
414      wTimeStart('xLock');
415      try { await getSyncHandle(fh) }
416      catch(e){
417        state.s11n.storeException(1,e);
418        rc = state.sq3Codes.SQLITE_IOERR_LOCK;
419      }
420      wTimeEnd();
421    }
422    storeAndNotify('xLock',rc);
423    mTimeEnd();
424  },
425  xOpen: async function(fid/*sqlite3_file pointer*/, filename,
426                        flags/*SQLITE_OPEN_...*/){
427    const opName = 'xOpen';
428    mTimeStart(opName);
429    const deleteOnClose = (state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags);
430    const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags);
431    wTimeStart('xOpen');
432    try{
433      let hDir, filenamePart;
434      try {
435        [hDir, filenamePart] = await getDirForFilename(filename, !!create);
436      }catch(e){
437        storeAndNotify(opName, state.sql3Codes.SQLITE_NOTFOUND);
438        mTimeEnd();
439        wTimeEnd();
440        return;
441      }
442      const hFile = await hDir.getFileHandle(filenamePart, {create});
443      /**
444         wa-sqlite, at this point, grabs a SyncAccessHandle and
445         assigns it to the syncHandle prop of the file state
446         object, but only for certain cases and it's unclear why it
447         places that limitation on it.
448      */
449      wTimeEnd();
450      __openFiles[fid] = Object.assign(Object.create(null),{
451        filenameAbs: filename,
452        filenamePart: filenamePart,
453        dirHandle: hDir,
454        fileHandle: hFile,
455        sabView: state.sabFileBufView,
456        readOnly: create
457          ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags),
458        deleteOnClose: deleteOnClose
459      });
460      storeAndNotify(opName, 0);
461    }catch(e){
462      wTimeEnd();
463      error(opName,e);
464      state.s11n.storeException(1,e);
465      storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
466    }
467    mTimeEnd();
468  },
469  xRead: async function(fid/*sqlite3_file pointer*/,n,offset64){
470    mTimeStart('xRead');
471    let rc = 0, nRead;
472    const fh = __openFiles[fid];
473    try{
474      wTimeStart('xRead');
475      nRead = (await getSyncHandle(fh)).read(
476        fh.sabView.subarray(0, n),
477        {at: Number(offset64)}
478      );
479      wTimeEnd();
480      if(nRead < n){/* Zero-fill remaining bytes */
481        fh.sabView.fill(0, nRead, n);
482        rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
483      }
484    }catch(e){
485      if(undefined===nRead) wTimeEnd();
486      error("xRead() failed",e,fh);
487      state.s11n.storeException(1,e);
488      rc = state.sq3Codes.SQLITE_IOERR_READ;
489    }
490    storeAndNotify('xRead',rc);
491    mTimeEnd();
492  },
493  xSync: async function(fid/*sqlite3_file pointer*/,flags/*ignored*/){
494    mTimeStart('xSync');
495    const fh = __openFiles[fid];
496    let rc = 0;
497    if(!fh.readOnly && fh.syncHandle){
498      try {
499        wTimeStart('xSync');
500        await fh.syncHandle.flush();
501      }catch(e){
502        state.s11n.storeException(2,e);
503        rc = state.sq3Codes.SQLITE_IOERR_FSYNC;
504      }
505      wTimeEnd();
506    }
507    storeAndNotify('xSync',rc);
508    mTimeEnd();
509  },
510  xTruncate: async function(fid/*sqlite3_file pointer*/,size){
511    mTimeStart('xTruncate');
512    let rc = 0;
513    const fh = __openFiles[fid];
514    wTimeStart('xTruncate');
515    try{
516      affirmNotRO('xTruncate', fh);
517      await (await getSyncHandle(fh)).truncate(size);
518    }catch(e){
519      error("xTruncate():",e,fh);
520      state.s11n.storeException(2,e);
521      rc = state.sq3Codes.SQLITE_IOERR_TRUNCATE;
522    }
523    wTimeEnd();
524    storeAndNotify('xTruncate',rc);
525    mTimeEnd();
526  },
527  xUnlock: async function(fid/*sqlite3_file pointer*/,
528                          lockType/*SQLITE_LOCK_...*/){
529    mTimeStart('xUnlock');
530    let rc = 0;
531    const fh = __openFiles[fid];
532    if( state.sq3Codes.SQLITE_LOCK_NONE===lockType
533        && fh.syncHandle ){
534      wTimeStart('xUnlock');
535      try { await closeSyncHandle(fh) }
536      catch(e){
537        state.s11n.storeException(1,e);
538        rc = state.sq3Codes.SQLITE_IOERR_UNLOCK;
539      }
540      wTimeEnd();
541    }
542    storeAndNotify('xUnlock',rc);
543    mTimeEnd();
544  },
545  xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){
546    mTimeStart('xWrite');
547    let rc;
548    wTimeStart('xWrite');
549    try{
550      const fh = __openFiles[fid];
551      affirmNotRO('xWrite', fh);
552      rc = (
553        n === (await getSyncHandle(fh))
554          .write(fh.sabView.subarray(0, n),
555                 {at: Number(offset64)})
556      ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
557    }catch(e){
558      error("xWrite():",e,fh);
559      state.s11n.storeException(1,e);
560      rc = state.sq3Codes.SQLITE_IOERR_WRITE;
561    }
562    wTimeEnd();
563    storeAndNotify('xWrite',rc);
564    mTimeEnd();
565  }
566}/*vfsAsyncImpls*/;
567
568const initS11n = ()=>{
569  /**
570     ACHTUNG: this code is 100% duplicated in the other half of this
571     proxy! The documentation is maintained in the "synchronous half".
572  */
573  if(state.s11n) return state.s11n;
574  const textDecoder = new TextDecoder(),
575  textEncoder = new TextEncoder('utf-8'),
576  viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize),
577  viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
578  state.s11n = Object.create(null);
579  const TypeIds = Object.create(null);
580  TypeIds.number  = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' };
581  TypeIds.bigint  = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' };
582  TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' };
583  TypeIds.string =  { id: 4 };
584  const getTypeId = (v)=>(
585    TypeIds[typeof v]
586      || toss("Maintenance required: this value type cannot be serialized.",v)
587  );
588  const getTypeIdById = (tid)=>{
589    switch(tid){
590      case TypeIds.number.id: return TypeIds.number;
591      case TypeIds.bigint.id: return TypeIds.bigint;
592      case TypeIds.boolean.id: return TypeIds.boolean;
593      case TypeIds.string.id: return TypeIds.string;
594      default: toss("Invalid type ID:",tid);
595    }
596  };
597  state.s11n.deserialize = function(){
598    ++metrics.s11n.deserialize.count;
599    const t = performance.now();
600    const argc = viewU8[0];
601    const rc = argc ? [] : null;
602    if(argc){
603      const typeIds = [];
604      let offset = 1, i, n, v;
605      for(i = 0; i < argc; ++i, ++offset){
606        typeIds.push(getTypeIdById(viewU8[offset]));
607      }
608      for(i = 0; i < argc; ++i){
609        const t = typeIds[i];
610        if(t.getter){
611          v = viewDV[t.getter](offset, state.littleEndian);
612          offset += t.size;
613        }else{/*String*/
614          n = viewDV.getInt32(offset, state.littleEndian);
615          offset += 4;
616          v = textDecoder.decode(viewU8.slice(offset, offset+n));
617          offset += n;
618        }
619        rc.push(v);
620      }
621    }
622    //log("deserialize:",argc, rc);
623    metrics.s11n.deserialize.time += performance.now() - t;
624    return rc;
625  };
626  state.s11n.serialize = function(...args){
627    const t = performance.now();
628    ++metrics.s11n.serialize.count;
629    if(args.length){
630      //log("serialize():",args);
631      const typeIds = [];
632      let i = 0, offset = 1;
633      viewU8[0] = args.length & 0xff /* header = # of args */;
634      for(; i < args.length; ++i, ++offset){
635        /* Write the TypeIds.id value into the next args.length
636           bytes. */
637        typeIds.push(getTypeId(args[i]));
638        viewU8[offset] = typeIds[i].id;
639      }
640      for(i = 0; i < args.length; ++i) {
641        /* Deserialize the following bytes based on their
642           corresponding TypeIds.id from the header. */
643        const t = typeIds[i];
644        if(t.setter){
645          viewDV[t.setter](offset, args[i], state.littleEndian);
646          offset += t.size;
647        }else{/*String*/
648          const s = textEncoder.encode(args[i]);
649          viewDV.setInt32(offset, s.byteLength, state.littleEndian);
650          offset += 4;
651          viewU8.set(s, offset);
652          offset += s.byteLength;
653        }
654      }
655      //log("serialize() result:",viewU8.slice(0,offset));
656    }else{
657      viewU8[0] = 0;
658    }
659    metrics.s11n.serialize.time += performance.now() - t;
660  };
661
662  state.s11n.storeException = state.asyncS11nExceptions
663    ? ((priority,e)=>{
664      if(priority<=state.asyncS11nExceptions){
665        state.s11n.serialize([e.name,': ',e.message].join(''));
666      }
667    })
668    : ()=>{};
669
670  return state.s11n;
671}/*initS11n()*/;
672
673const waitLoop = async function f(){
674  const opHandlers = Object.create(null);
675  for(let k of Object.keys(state.opIds)){
676    const vi = vfsAsyncImpls[k];
677    if(!vi) continue;
678    const o = Object.create(null);
679    opHandlers[state.opIds[k]] = o;
680    o.key = k;
681    o.f = vi;
682  }
683  /**
684     waitTime is how long (ms) to wait for each Atomics.wait().
685     We need to wake up periodically to give the thread a chance
686     to do other things.
687  */
688  const waitTime = 1000;
689  while(!flagAsyncShutdown){
690    try {
691      if('timed-out'===Atomics.wait(
692        state.sabOPView, state.opIds.whichOp, 0, waitTime
693      )){
694        continue;
695      }
696      const opId = Atomics.load(state.sabOPView, state.opIds.whichOp);
697      Atomics.store(state.sabOPView, state.opIds.whichOp, 0);
698      const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId);
699      const args = state.s11n.deserialize() || [];
700      state.s11n.serialize(/* clear s11n to keep the caller from
701                              confusing this with an exception string
702                              written by the upcoming operation */);
703      //warn("waitLoop() whichOp =",opId, hnd, args);
704      if(hnd.f) await hnd.f(...args);
705      else error("Missing callback for opId",opId);
706    }catch(e){
707      error('in waitLoop():',e);
708    }
709  }
710};
711
712navigator.storage.getDirectory().then(function(d){
713  const wMsg = (type)=>postMessage({type});
714  state.rootDir = d;
715  self.onmessage = function({data}){
716    switch(data.type){
717        case 'opfs-async-init':{
718          /* Receive shared state from synchronous partner */
719          const opt = data.args;
720          state.littleEndian = opt.littleEndian;
721          state.asyncS11nExceptions = opt.asyncS11nExceptions;
722          state.verbose = opt.verbose ?? 2;
723          state.fileBufferSize = opt.fileBufferSize;
724          state.sabS11nOffset = opt.sabS11nOffset;
725          state.sabS11nSize = opt.sabS11nSize;
726          state.sabOP = opt.sabOP;
727          state.sabOPView = new Int32Array(state.sabOP);
728          state.sabIO = opt.sabIO;
729          state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
730          state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
731          state.opIds = opt.opIds;
732          state.sq3Codes = opt.sq3Codes;
733          Object.keys(vfsAsyncImpls).forEach((k)=>{
734            if(!Number.isFinite(state.opIds[k])){
735              toss("Maintenance required: missing state.opIds[",k,"]");
736            }
737          });
738          initS11n();
739          metrics.reset();
740          log("init state",state);
741          wMsg('opfs-async-inited');
742          waitLoop();
743          break;
744        }
745        case 'opfs-async-restart':
746          if(flagAsyncShutdown){
747            warn("Restarting after opfs-async-shutdown. Might or might not work.");
748            flagAsyncShutdown = false;
749            waitLoop();
750          }
751          break;
752        case 'opfs-async-metrics':
753          metrics.dump();
754          break;
755    }
756  };
757  wMsg('opfs-async-loaded');
758}).catch((e)=>error("error initializing OPFS asyncer:",e));
759