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        recursive = false;
381        filename = getResolvedPath(filename, true);
382        filename.pop();
383        filename = filename.join('/');
384      }
385    }catch(e){
386      state.s11n.storeException(2,e);
387      rc = state.sq3Codes.SQLITE_IOERR_DELETE;
388    }
389    wTimeEnd();
390    return rc;
391  },
392  xFileSize: async function(fid/*sqlite3_file pointer*/){
393    mTimeStart('xFileSize');
394    const fh = __openFiles[fid];
395    let rc;
396    wTimeStart('xFileSize');
397    try{
398      rc = await (await getSyncHandle(fh)).getSize();
399      state.s11n.serialize(Number(rc));
400      rc = 0;
401    }catch(e){
402      state.s11n.storeException(2,e);
403      rc = state.sq3Codes.SQLITE_IOERR;
404    }
405    wTimeEnd();
406    storeAndNotify('xFileSize', rc);
407    mTimeEnd();
408  },
409  xLock: async function(fid/*sqlite3_file pointer*/,
410                        lockType/*SQLITE_LOCK_...*/){
411    mTimeStart('xLock');
412    const fh = __openFiles[fid];
413    let rc = 0;
414    if( !fh.syncHandle ){
415      wTimeStart('xLock');
416      try { await getSyncHandle(fh) }
417      catch(e){
418        state.s11n.storeException(1,e);
419        rc = state.sq3Codes.SQLITE_IOERR_LOCK;
420      }
421      wTimeEnd();
422    }
423    storeAndNotify('xLock',rc);
424    mTimeEnd();
425  },
426  xOpen: async function(fid/*sqlite3_file pointer*/, filename,
427                        flags/*SQLITE_OPEN_...*/){
428    const opName = 'xOpen';
429    mTimeStart(opName);
430    const deleteOnClose = (state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags);
431    const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags);
432    wTimeStart('xOpen');
433    try{
434      let hDir, filenamePart;
435      try {
436        [hDir, filenamePart] = await getDirForFilename(filename, !!create);
437      }catch(e){
438        state.s11n.storeException(1,e);
439        storeAndNotify(opName, state.sq3Codes.SQLITE_NOTFOUND);
440        mTimeEnd();
441        wTimeEnd();
442        return;
443      }
444      const hFile = await hDir.getFileHandle(filenamePart, {create});
445      /**
446         wa-sqlite, at this point, grabs a SyncAccessHandle and
447         assigns it to the syncHandle prop of the file state
448         object, but only for certain cases and it's unclear why it
449         places that limitation on it.
450      */
451      wTimeEnd();
452      __openFiles[fid] = Object.assign(Object.create(null),{
453        filenameAbs: filename,
454        filenamePart: filenamePart,
455        dirHandle: hDir,
456        fileHandle: hFile,
457        sabView: state.sabFileBufView,
458        readOnly: create
459          ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags),
460        deleteOnClose: deleteOnClose
461      });
462      storeAndNotify(opName, 0);
463    }catch(e){
464      wTimeEnd();
465      error(opName,e);
466      state.s11n.storeException(1,e);
467      storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
468    }
469    mTimeEnd();
470  },
471  xRead: async function(fid/*sqlite3_file pointer*/,n,offset64){
472    mTimeStart('xRead');
473    let rc = 0, nRead;
474    const fh = __openFiles[fid];
475    try{
476      wTimeStart('xRead');
477      nRead = (await getSyncHandle(fh)).read(
478        fh.sabView.subarray(0, n),
479        {at: Number(offset64)}
480      );
481      wTimeEnd();
482      if(nRead < n){/* Zero-fill remaining bytes */
483        fh.sabView.fill(0, nRead, n);
484        rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
485      }
486    }catch(e){
487      if(undefined===nRead) wTimeEnd();
488      error("xRead() failed",e,fh);
489      state.s11n.storeException(1,e);
490      rc = state.sq3Codes.SQLITE_IOERR_READ;
491    }
492    storeAndNotify('xRead',rc);
493    mTimeEnd();
494  },
495  xSync: async function(fid/*sqlite3_file pointer*/,flags/*ignored*/){
496    mTimeStart('xSync');
497    const fh = __openFiles[fid];
498    let rc = 0;
499    if(!fh.readOnly && fh.syncHandle){
500      try {
501        wTimeStart('xSync');
502        await fh.syncHandle.flush();
503      }catch(e){
504        state.s11n.storeException(2,e);
505        rc = state.sq3Codes.SQLITE_IOERR_FSYNC;
506      }
507      wTimeEnd();
508    }
509    storeAndNotify('xSync',rc);
510    mTimeEnd();
511  },
512  xTruncate: async function(fid/*sqlite3_file pointer*/,size){
513    mTimeStart('xTruncate');
514    let rc = 0;
515    const fh = __openFiles[fid];
516    wTimeStart('xTruncate');
517    try{
518      affirmNotRO('xTruncate', fh);
519      await (await getSyncHandle(fh)).truncate(size);
520    }catch(e){
521      error("xTruncate():",e,fh);
522      state.s11n.storeException(2,e);
523      rc = state.sq3Codes.SQLITE_IOERR_TRUNCATE;
524    }
525    wTimeEnd();
526    storeAndNotify('xTruncate',rc);
527    mTimeEnd();
528  },
529  xUnlock: async function(fid/*sqlite3_file pointer*/,
530                          lockType/*SQLITE_LOCK_...*/){
531    mTimeStart('xUnlock');
532    let rc = 0;
533    const fh = __openFiles[fid];
534    if( state.sq3Codes.SQLITE_LOCK_NONE===lockType
535        && fh.syncHandle ){
536      wTimeStart('xUnlock');
537      try { await closeSyncHandle(fh) }
538      catch(e){
539        state.s11n.storeException(1,e);
540        rc = state.sq3Codes.SQLITE_IOERR_UNLOCK;
541      }
542      wTimeEnd();
543    }
544    storeAndNotify('xUnlock',rc);
545    mTimeEnd();
546  },
547  xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){
548    mTimeStart('xWrite');
549    let rc;
550    wTimeStart('xWrite');
551    try{
552      const fh = __openFiles[fid];
553      affirmNotRO('xWrite', fh);
554      rc = (
555        n === (await getSyncHandle(fh))
556          .write(fh.sabView.subarray(0, n),
557                 {at: Number(offset64)})
558      ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
559    }catch(e){
560      error("xWrite():",e,fh);
561      state.s11n.storeException(1,e);
562      rc = state.sq3Codes.SQLITE_IOERR_WRITE;
563    }
564    wTimeEnd();
565    storeAndNotify('xWrite',rc);
566    mTimeEnd();
567  }
568}/*vfsAsyncImpls*/;
569
570const initS11n = ()=>{
571  /**
572     ACHTUNG: this code is 100% duplicated in the other half of this
573     proxy! The documentation is maintained in the "synchronous half".
574  */
575  if(state.s11n) return state.s11n;
576  const textDecoder = new TextDecoder(),
577  textEncoder = new TextEncoder('utf-8'),
578  viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize),
579  viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
580  state.s11n = Object.create(null);
581  const TypeIds = Object.create(null);
582  TypeIds.number  = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' };
583  TypeIds.bigint  = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' };
584  TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' };
585  TypeIds.string =  { id: 4 };
586  const getTypeId = (v)=>(
587    TypeIds[typeof v]
588      || toss("Maintenance required: this value type cannot be serialized.",v)
589  );
590  const getTypeIdById = (tid)=>{
591    switch(tid){
592      case TypeIds.number.id: return TypeIds.number;
593      case TypeIds.bigint.id: return TypeIds.bigint;
594      case TypeIds.boolean.id: return TypeIds.boolean;
595      case TypeIds.string.id: return TypeIds.string;
596      default: toss("Invalid type ID:",tid);
597    }
598  };
599  state.s11n.deserialize = function(){
600    ++metrics.s11n.deserialize.count;
601    const t = performance.now();
602    const argc = viewU8[0];
603    const rc = argc ? [] : null;
604    if(argc){
605      const typeIds = [];
606      let offset = 1, i, n, v;
607      for(i = 0; i < argc; ++i, ++offset){
608        typeIds.push(getTypeIdById(viewU8[offset]));
609      }
610      for(i = 0; i < argc; ++i){
611        const t = typeIds[i];
612        if(t.getter){
613          v = viewDV[t.getter](offset, state.littleEndian);
614          offset += t.size;
615        }else{/*String*/
616          n = viewDV.getInt32(offset, state.littleEndian);
617          offset += 4;
618          v = textDecoder.decode(viewU8.slice(offset, offset+n));
619          offset += n;
620        }
621        rc.push(v);
622      }
623    }
624    //log("deserialize:",argc, rc);
625    metrics.s11n.deserialize.time += performance.now() - t;
626    return rc;
627  };
628  state.s11n.serialize = function(...args){
629    const t = performance.now();
630    ++metrics.s11n.serialize.count;
631    if(args.length){
632      //log("serialize():",args);
633      const typeIds = [];
634      let i = 0, offset = 1;
635      viewU8[0] = args.length & 0xff /* header = # of args */;
636      for(; i < args.length; ++i, ++offset){
637        /* Write the TypeIds.id value into the next args.length
638           bytes. */
639        typeIds.push(getTypeId(args[i]));
640        viewU8[offset] = typeIds[i].id;
641      }
642      for(i = 0; i < args.length; ++i) {
643        /* Deserialize the following bytes based on their
644           corresponding TypeIds.id from the header. */
645        const t = typeIds[i];
646        if(t.setter){
647          viewDV[t.setter](offset, args[i], state.littleEndian);
648          offset += t.size;
649        }else{/*String*/
650          const s = textEncoder.encode(args[i]);
651          viewDV.setInt32(offset, s.byteLength, state.littleEndian);
652          offset += 4;
653          viewU8.set(s, offset);
654          offset += s.byteLength;
655        }
656      }
657      //log("serialize() result:",viewU8.slice(0,offset));
658    }else{
659      viewU8[0] = 0;
660    }
661    metrics.s11n.serialize.time += performance.now() - t;
662  };
663
664  state.s11n.storeException = state.asyncS11nExceptions
665    ? ((priority,e)=>{
666      if(priority<=state.asyncS11nExceptions){
667        state.s11n.serialize([e.name,': ',e.message].join(''));
668      }
669    })
670    : ()=>{};
671
672  return state.s11n;
673}/*initS11n()*/;
674
675const waitLoop = async function f(){
676  const opHandlers = Object.create(null);
677  for(let k of Object.keys(state.opIds)){
678    const vi = vfsAsyncImpls[k];
679    if(!vi) continue;
680    const o = Object.create(null);
681    opHandlers[state.opIds[k]] = o;
682    o.key = k;
683    o.f = vi;
684  }
685  /**
686     waitTime is how long (ms) to wait for each Atomics.wait().
687     We need to wake up periodically to give the thread a chance
688     to do other things.
689  */
690  const waitTime = 1000;
691  while(!flagAsyncShutdown){
692    try {
693      if('timed-out'===Atomics.wait(
694        state.sabOPView, state.opIds.whichOp, 0, waitTime
695      )){
696        continue;
697      }
698      const opId = Atomics.load(state.sabOPView, state.opIds.whichOp);
699      Atomics.store(state.sabOPView, state.opIds.whichOp, 0);
700      const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId);
701      const args = state.s11n.deserialize() || [];
702      state.s11n.serialize(/* clear s11n to keep the caller from
703                              confusing this with an exception string
704                              written by the upcoming operation */);
705      //warn("waitLoop() whichOp =",opId, hnd, args);
706      if(hnd.f) await hnd.f(...args);
707      else error("Missing callback for opId",opId);
708    }catch(e){
709      error('in waitLoop():',e);
710    }
711  }
712};
713
714navigator.storage.getDirectory().then(function(d){
715  const wMsg = (type)=>postMessage({type});
716  state.rootDir = d;
717  self.onmessage = function({data}){
718    switch(data.type){
719        case 'opfs-async-init':{
720          /* Receive shared state from synchronous partner */
721          const opt = data.args;
722          state.littleEndian = opt.littleEndian;
723          state.asyncS11nExceptions = opt.asyncS11nExceptions;
724          state.verbose = opt.verbose ?? 2;
725          state.fileBufferSize = opt.fileBufferSize;
726          state.sabS11nOffset = opt.sabS11nOffset;
727          state.sabS11nSize = opt.sabS11nSize;
728          state.sabOP = opt.sabOP;
729          state.sabOPView = new Int32Array(state.sabOP);
730          state.sabIO = opt.sabIO;
731          state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
732          state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
733          state.opIds = opt.opIds;
734          state.sq3Codes = opt.sq3Codes;
735          Object.keys(vfsAsyncImpls).forEach((k)=>{
736            if(!Number.isFinite(state.opIds[k])){
737              toss("Maintenance required: missing state.opIds[",k,"]");
738            }
739          });
740          initS11n();
741          metrics.reset();
742          log("init state",state);
743          wMsg('opfs-async-inited');
744          waitLoop();
745          break;
746        }
747        case 'opfs-async-restart':
748          if(flagAsyncShutdown){
749            warn("Restarting after opfs-async-shutdown. Might or might not work.");
750            flagAsyncShutdown = false;
751            waitLoop();
752          }
753          break;
754        case 'opfs-async-metrics':
755          metrics.dump();
756          break;
757    }
758  };
759  wMsg('opfs-async-loaded');
760}).catch((e)=>error("error initializing OPFS asyncer:",e));
761