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