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