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