Fixes default log output to console for macOS
[sqlcipher.git] / ext / wasm / api / sqlite3-opfs-async-proxy.js
blob3e2b20ffb307ee06b42b789357090f5862e3f96f
1 /*
2   2022-09-16
4   The author disclaims copyright to this source code.  In place of a
5   legal notice, here is a blessing:
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.
11   ***********************************************************************
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-vfs-opfs.js.
18   Highly indebted to:
20   https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/OriginPrivateFileSystemVFS.js
22   for demonstrating how to use the OPFS APIs.
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.
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.
33   2022-11-27: Chrome v108 changes some async methods to synchronous, as
34   documented at:
36   https://developer.chrome.com/blog/sync-methods-for-accesshandles/
38   Firefox v111 and Safari 16.4, both released in March 2023, also
39   include this.
41   We cannot change to the sync forms at this point without breaking
42   clients who use Chrome v104-ish or higher. truncate(), getSize(),
43   flush(), and close() are now (as of v108) synchronous. Calling them
44   with an "await", as we have to for the async forms, is still legal
45   with the sync forms but is superfluous. Calling the async forms with
46   theFunc().then(...) is not compatible with the change to
47   synchronous, but we do do not use those APIs that way. i.e. we don't
48   _need_ to change anything for this, but at some point (after Chrome
49   versions (approximately) 104-107 are extinct) should change our
50   usage of those methods to remove the "await".
52 "use strict";
53 const wPost = (type,...args)=>postMessage({type, payload:args});
54 const installAsyncProxy = function(){
55   const toss = function(...args){throw new Error(args.join(' '))};
56   if(globalThis.window === globalThis){
57     toss("This code cannot run from the main thread.",
58          "Load it as a Worker from a separate Worker.");
59   }else if(!navigator?.storage?.getDirectory){
60     toss("This API requires navigator.storage.getDirectory.");
61   }
63   /**
64      Will hold state copied to this object from the syncronous side of
65      this API.
66   */
67   const state = Object.create(null);
69   /**
70      verbose:
72      0 = no logging output
73      1 = only errors
74      2 = warnings and errors
75      3 = debug, warnings, and errors
76   */
77   state.verbose = 1;
79   const loggers = {
80     0:console.error.bind(console),
81     1:console.warn.bind(console),
82     2:console.log.bind(console)
83   };
84   const logImpl = (level,...args)=>{
85     if(state.verbose>level) loggers[level]("OPFS asyncer:",...args);
86   };
87   const log =    (...args)=>logImpl(2, ...args);
88   const warn =   (...args)=>logImpl(1, ...args);
89   const error =  (...args)=>logImpl(0, ...args);
91   /**
92      __openFiles is a map of sqlite3_file pointers (integers) to
93      metadata related to a given OPFS file handles. The pointers are, in
94      this side of the interface, opaque file handle IDs provided by the
95      synchronous part of this constellation. Each value is an object
96      with a structure demonstrated in the xOpen() impl.
97   */
98   const __openFiles = Object.create(null);
99   /**
100      __implicitLocks is a Set of sqlite3_file pointers (integers) which were
101      "auto-locked".  i.e. those for which we obtained a sync access
102      handle without an explicit xLock() call. Such locks will be
103      released during db connection idle time, whereas a sync access
104      handle obtained via xLock(), or subsequently xLock()'d after
105      auto-acquisition, will not be released until xUnlock() is called.
107      Maintenance reminder: if we relinquish auto-locks at the end of the
108      operation which acquires them, we pay a massive performance
109      penalty: speedtest1 benchmarks take up to 4x as long. By delaying
110      the lock release until idle time, the hit is negligible.
111   */
112   const __implicitLocks = new Set();
114   /**
115      Expects an OPFS file path. It gets resolved, such that ".."
116      components are properly expanded, and returned. If the 2nd arg is
117      true, the result is returned as an array of path elements, else an
118      absolute path string is returned.
119   */
120   const getResolvedPath = function(filename,splitIt){
121     const p = new URL(
122       filename, 'file://irrelevant'
123     ).pathname;
124     return splitIt ? p.split('/').filter((v)=>!!v) : p;
125   };
127   /**
128      Takes the absolute path to a filesystem element. Returns an array
129      of [handleOfContainingDir, filename]. If the 2nd argument is truthy
130      then each directory element leading to the file is created along
131      the way. Throws if any creation or resolution fails.
132   */
133   const getDirForFilename = async function f(absFilename, createDirs = false){
134     const path = getResolvedPath(absFilename, true);
135     const filename = path.pop();
136     let dh = state.rootDir;
137     for(const dirName of path){
138       if(dirName){
139         dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs});
140       }
141     }
142     return [dh, filename];
143   };
145   /**
146      If the given file-holding object has a sync handle attached to it,
147      that handle is remove and asynchronously closed. Though it may
148      sound sensible to continue work as soon as the close() returns
149      (noting that it's asynchronous), doing so can cause operations
150      performed soon afterwards, e.g. a call to getSyncHandle() to fail
151      because they may happen out of order from the close(). OPFS does
152      not guaranty that the actual order of operations is retained in
153      such cases. i.e.  always "await" on the result of this function.
154   */
155   const closeSyncHandle = async (fh)=>{
156     if(fh.syncHandle){
157       log("Closing sync handle for",fh.filenameAbs);
158       const h = fh.syncHandle;
159       delete fh.syncHandle;
160       delete fh.xLock;
161       __implicitLocks.delete(fh.fid);
162       return h.close();
163     }
164   };
166   /**
167      A proxy for closeSyncHandle() which is guaranteed to not throw.
169      This function is part of a lock/unlock step in functions which
170      require a sync access handle but may be called without xLock()
171      having been called first. Such calls need to release that
172      handle to avoid locking the file for all of time. This is an
173      _attempt_ at reducing cross-tab contention but it may prove
174      to be more of a problem than a solution and may need to be
175      removed.
176   */
177   const closeSyncHandleNoThrow = async (fh)=>{
178     try{await closeSyncHandle(fh)}
179     catch(e){
180       warn("closeSyncHandleNoThrow() ignoring:",e,fh);
181     }
182   };
184   /* Release all auto-locks. */
185   const releaseImplicitLocks = async ()=>{
186     if(__implicitLocks.size){
187       /* Release all auto-locks. */
188       for(const fid of __implicitLocks){
189         const fh = __openFiles[fid];
190         await closeSyncHandleNoThrow(fh);
191         log("Auto-unlocked",fid,fh.filenameAbs);
192       }
193     }
194   };
196   /**
197      An experiment in improving concurrency by freeing up implicit locks
198      sooner. This is known to impact performance dramatically but it has
199      also shown to improve concurrency considerably.
201      If fh.releaseImplicitLocks is truthy and fh is in __implicitLocks,
202      this routine returns closeSyncHandleNoThrow(), else it is a no-op.
203   */
204   const releaseImplicitLock = async (fh)=>{
205     if(fh.releaseImplicitLocks && __implicitLocks.has(fh.fid)){
206       return closeSyncHandleNoThrow(fh);
207     }
208   };
210   /**
211      An error class specifically for use with getSyncHandle(), the goal
212      of which is to eventually be able to distinguish unambiguously
213      between locking-related failures and other types, noting that we
214      cannot currently do so because createSyncAccessHandle() does not
215      define its exceptions in the required level of detail.
217      2022-11-29: according to:
219      https://github.com/whatwg/fs/pull/21
221      NoModificationAllowedError will be the standard exception thrown
222      when acquisition of a sync access handle fails due to a locking
223      error. As of this writing, that error type is not visible in the
224      dev console in Chrome v109, nor is it documented in MDN, but an
225      error with that "name" property is being thrown from the OPFS
226      layer.
227   */
228   class GetSyncHandleError extends Error {
229     constructor(errorObject, ...msg){
230       super([
231         ...msg, ': '+errorObject.name+':',
232         errorObject.message
233       ].join(' '), {
234         cause: errorObject
235       });
236       this.name = 'GetSyncHandleError';
237     }
238   };
240   /**
241      Attempts to find a suitable SQLITE_xyz result code for Error
242      object e. Returns either such a translation or rc if if it does
243      not know how to translate the exception.
244   */
245   GetSyncHandleError.convertRc = (e,rc)=>{
246     if( e instanceof GetSyncHandleError ){
247       if( e.cause.name==='NoModificationAllowedError'
248         /* Inconsistent exception.name from Chrome/ium with the
249            same exception.message text: */
250           || (e.cause.name==='DOMException'
251               && 0===e.cause.message.indexOf('Access Handles cannot')) ){
252         return state.sq3Codes.SQLITE_BUSY;
253       }else if( 'NotFoundError'===e.cause.name ){
254         /**
255            Maintenance reminder: SQLITE_NOTFOUND, though it looks like
256            a good match, has different semantics than NotFoundError
257            and is not suitable here.
258         */
259         return state.sq3Codes.SQLITE_CANTOPEN;
260       }
261     }else if( 'NotFoundError'===e?.name ){
262       return state.sq3Codes.SQLITE_CANTOPEN;
263     }
264     return rc;
265   };
267   /**
268      Returns the sync access handle associated with the given file
269      handle object (which must be a valid handle object, as created by
270      xOpen()), lazily opening it if needed.
272      In order to help alleviate cross-tab contention for a dabase, if
273      an exception is thrown while acquiring the handle, this routine
274      will wait briefly and try again, up to some fixed number of
275      times. If acquisition still fails at that point it will give up
276      and propagate the exception. Client-level code will see that as
277      an I/O error.
278   */
279   const getSyncHandle = async (fh,opName)=>{
280     if(!fh.syncHandle){
281       const t = performance.now();
282       log("Acquiring sync handle for",fh.filenameAbs);
283       const maxTries = 6,
284             msBase = state.asyncIdleWaitTime * 2;
285       let i = 1, ms = msBase;
286       for(; true; ms = msBase * ++i){
287         try {
288           //if(i<3) toss("Just testing getSyncHandle() wait-and-retry.");
289           //TODO? A config option which tells it to throw here
290           //randomly every now and then, for testing purposes.
291           fh.syncHandle = await fh.fileHandle.createSyncAccessHandle();
292           break;
293         }catch(e){
294           if(i === maxTries){
295             throw new GetSyncHandleError(
296               e, "Error getting sync handle for",opName+"().",maxTries,
297               "attempts failed.",fh.filenameAbs
298             );
299           }
300           warn("Error getting sync handle for",opName+"(). Waiting",ms,
301                "ms and trying again.",fh.filenameAbs,e);
302           Atomics.wait(state.sabOPView, state.opIds.retry, 0, ms);
303         }
304       }
305       log("Got",opName+"() sync handle for",fh.filenameAbs,
306           'in',performance.now() - t,'ms');
307       if(!fh.xLock){
308         __implicitLocks.add(fh.fid);
309         log("Acquired implicit lock for",opName+"()",fh.fid,fh.filenameAbs);
310       }
311     }
312     return fh.syncHandle;
313   };
315   /**
316      Stores the given value at state.sabOPView[state.opIds.rc] and then
317      Atomics.notify()'s it.
318   */
319   const storeAndNotify = (opName, value)=>{
320     log(opName+"() => notify(",value,")");
321     Atomics.store(state.sabOPView, state.opIds.rc, value);
322     Atomics.notify(state.sabOPView, state.opIds.rc);
323   };
325   /**
326      Throws if fh is a file-holding object which is flagged as read-only.
327   */
328   const affirmNotRO = function(opName,fh){
329     if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs);
330   };
332   /**
333      Gets set to true by the 'opfs-async-shutdown' command to quit the
334      wait loop. This is only intended for debugging purposes: we cannot
335      inspect this file's state while the tight waitLoop() is running and
336      need a way to stop that loop for introspection purposes.
337   */
338   let flagAsyncShutdown = false;
340   /**
341      Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods
342      methods, as well as helpers like mkdir().
343   */
344   const vfsAsyncImpls = {
345     'opfs-async-shutdown': async ()=>{
346       flagAsyncShutdown = true;
347       storeAndNotify('opfs-async-shutdown', 0);
348     },
349     mkdir: async (dirname)=>{
350       let rc = 0;
351       try {
352         await getDirForFilename(dirname+"/filepart", true);
353       }catch(e){
354         state.s11n.storeException(2,e);
355         rc = state.sq3Codes.SQLITE_IOERR;
356       }
357       storeAndNotify('mkdir', rc);
358     },
359     xAccess: async (filename)=>{
360       /* OPFS cannot support the full range of xAccess() queries
361          sqlite3 calls for. We can essentially just tell if the file
362          is accessible, but if it is then it's automatically writable
363          (unless it's locked, which we cannot(?) know without trying
364          to open it). OPFS does not have the notion of read-only.
366          The return semantics of this function differ from sqlite3's
367          xAccess semantics because we are limited in what we can
368          communicate back to our synchronous communication partner: 0 =
369          accessible, non-0 means not accessible.
370       */
371       let rc = 0;
372       try{
373         const [dh, fn] = await getDirForFilename(filename);
374         await dh.getFileHandle(fn);
375       }catch(e){
376         state.s11n.storeException(2,e);
377         rc = state.sq3Codes.SQLITE_IOERR;
378       }
379       storeAndNotify('xAccess', rc);
380     },
381     xClose: async function(fid/*sqlite3_file pointer*/){
382       const opName = 'xClose';
383       __implicitLocks.delete(fid);
384       const fh = __openFiles[fid];
385       let rc = 0;
386       if(fh){
387         delete __openFiles[fid];
388         await closeSyncHandle(fh);
389         if(fh.deleteOnClose){
390           try{ await fh.dirHandle.removeEntry(fh.filenamePart) }
391           catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) }
392         }
393       }else{
394         state.s11n.serialize();
395         rc = state.sq3Codes.SQLITE_NOTFOUND;
396       }
397       storeAndNotify(opName, rc);
398     },
399     xDelete: async function(...args){
400       const rc = await vfsAsyncImpls.xDeleteNoWait(...args);
401       storeAndNotify('xDelete', rc);
402     },
403     xDeleteNoWait: async function(filename, syncDir = 0, recursive = false){
404       /* The syncDir flag is, for purposes of the VFS API's semantics,
405          ignored here. However, if it has the value 0x1234 then: after
406          deleting the given file, recursively try to delete any empty
407          directories left behind in its wake (ignoring any errors and
408          stopping at the first failure).
410          That said: we don't know for sure that removeEntry() fails if
411          the dir is not empty because the API is not documented. It has,
412          however, a "recursive" flag which defaults to false, so
413          presumably it will fail if the dir is not empty and that flag
414          is false.
415       */
416       let rc = 0;
417       try {
418         while(filename){
419           const [hDir, filenamePart] = await getDirForFilename(filename, false);
420           if(!filenamePart) break;
421           await hDir.removeEntry(filenamePart, {recursive});
422           if(0x1234 !== syncDir) break;
423           recursive = false;
424           filename = getResolvedPath(filename, true);
425           filename.pop();
426           filename = filename.join('/');
427         }
428       }catch(e){
429         state.s11n.storeException(2,e);
430         rc = state.sq3Codes.SQLITE_IOERR_DELETE;
431       }
432       return rc;
433     },
434     xFileSize: async function(fid/*sqlite3_file pointer*/){
435       const fh = __openFiles[fid];
436       let rc = 0;
437       try{
438         const sz = await (await getSyncHandle(fh,'xFileSize')).getSize();
439         state.s11n.serialize(Number(sz));
440       }catch(e){
441         state.s11n.storeException(1,e);
442         rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR);
443       }
444       await releaseImplicitLock(fh);
445       storeAndNotify('xFileSize', rc);
446     },
447     xLock: async function(fid/*sqlite3_file pointer*/,
448                           lockType/*SQLITE_LOCK_...*/){
449       const fh = __openFiles[fid];
450       let rc = 0;
451       const oldLockType = fh.xLock;
452       fh.xLock = lockType;
453       if( !fh.syncHandle ){
454         try {
455           await getSyncHandle(fh,'xLock');
456           __implicitLocks.delete(fid);
457         }catch(e){
458           state.s11n.storeException(1,e);
459           rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_LOCK);
460           fh.xLock = oldLockType;
461         }
462       }
463       storeAndNotify('xLock',rc);
464     },
465     xOpen: async function(fid/*sqlite3_file pointer*/, filename,
466                           flags/*SQLITE_OPEN_...*/,
467                           opfsFlags/*OPFS_...*/){
468       const opName = 'xOpen';
469       const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags);
470       try{
471         let hDir, filenamePart;
472         try {
473           [hDir, filenamePart] = await getDirForFilename(filename, !!create);
474         }catch(e){
475           state.s11n.storeException(1,e);
476           storeAndNotify(opName, state.sq3Codes.SQLITE_NOTFOUND);
477           return;
478         }
479         if( state.opfsFlags.OPFS_UNLINK_BEFORE_OPEN & opfsFlags ){
480           try{
481             await hDir.removeEntry(filenamePart);
482           }catch(e){
483             /* ignoring */
484             //warn("Ignoring failed Unlink of",filename,":",e);
485           }
486         }
487         const hFile = await hDir.getFileHandle(filenamePart, {create});
488         const fh = Object.assign(Object.create(null),{
489           fid: fid,
490           filenameAbs: filename,
491           filenamePart: filenamePart,
492           dirHandle: hDir,
493           fileHandle: hFile,
494           sabView: state.sabFileBufView,
495           readOnly: create
496             ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags),
497           deleteOnClose: !!(state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags)
498         });
499         fh.releaseImplicitLocks =
500           (opfsFlags & state.opfsFlags.OPFS_UNLOCK_ASAP)
501           || state.opfsFlags.defaultUnlockAsap;
502         __openFiles[fid] = fh;
503         storeAndNotify(opName, 0);
504       }catch(e){
505         error(opName,e);
506         state.s11n.storeException(1,e);
507         storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
508       }
509     },
510     xRead: async function(fid/*sqlite3_file pointer*/,n,offset64){
511       let rc = 0, nRead;
512       const fh = __openFiles[fid];
513       try{
514         nRead = (await getSyncHandle(fh,'xRead')).read(
515           fh.sabView.subarray(0, n),
516           {at: Number(offset64)}
517         );
518         if(nRead < n){/* Zero-fill remaining bytes */
519           fh.sabView.fill(0, nRead, n);
520           rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
521         }
522       }catch(e){
523         error("xRead() failed",e,fh);
524         state.s11n.storeException(1,e);
525         rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_READ);
526       }
527       await releaseImplicitLock(fh);
528       storeAndNotify('xRead',rc);
529     },
530     xSync: async function(fid/*sqlite3_file pointer*/,flags/*ignored*/){
531       const fh = __openFiles[fid];
532       let rc = 0;
533       if(!fh.readOnly && fh.syncHandle){
534         try {
535           await fh.syncHandle.flush();
536         }catch(e){
537           state.s11n.storeException(2,e);
538           rc = state.sq3Codes.SQLITE_IOERR_FSYNC;
539         }
540       }
541       storeAndNotify('xSync',rc);
542     },
543     xTruncate: async function(fid/*sqlite3_file pointer*/,size){
544       let rc = 0;
545       const fh = __openFiles[fid];
546       try{
547         affirmNotRO('xTruncate', fh);
548         await (await getSyncHandle(fh,'xTruncate')).truncate(size);
549       }catch(e){
550         error("xTruncate():",e,fh);
551         state.s11n.storeException(2,e);
552         rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_TRUNCATE);
553       }
554       await releaseImplicitLock(fh);
555       storeAndNotify('xTruncate',rc);
556     },
557     xUnlock: async function(fid/*sqlite3_file pointer*/,
558                             lockType/*SQLITE_LOCK_...*/){
559       let rc = 0;
560       const fh = __openFiles[fid];
561       if( state.sq3Codes.SQLITE_LOCK_NONE===lockType
562           && fh.syncHandle ){
563         try { await closeSyncHandle(fh) }
564         catch(e){
565           state.s11n.storeException(1,e);
566           rc = state.sq3Codes.SQLITE_IOERR_UNLOCK;
567         }
568       }
569       storeAndNotify('xUnlock',rc);
570     },
571     xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){
572       let rc;
573       const fh = __openFiles[fid];
574       try{
575         affirmNotRO('xWrite', fh);
576         rc = (
577           n === (await getSyncHandle(fh,'xWrite'))
578             .write(fh.sabView.subarray(0, n),
579                    {at: Number(offset64)})
580         ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
581       }catch(e){
582         error("xWrite():",e,fh);
583         state.s11n.storeException(1,e);
584         rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_WRITE);
585       }
586       await releaseImplicitLock(fh);
587       storeAndNotify('xWrite',rc);
588     }
589   }/*vfsAsyncImpls*/;
591   const initS11n = ()=>{
592     /**
593        ACHTUNG: this code is 100% duplicated in the other half of this
594        proxy! The documentation is maintained in the "synchronous half".
595     */
596     if(state.s11n) return state.s11n;
597     const textDecoder = new TextDecoder(),
598           textEncoder = new TextEncoder('utf-8'),
599           viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize),
600           viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
601     state.s11n = Object.create(null);
602     const TypeIds = Object.create(null);
603     TypeIds.number  = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' };
604     TypeIds.bigint  = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' };
605     TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' };
606     TypeIds.string =  { id: 4 };
607     const getTypeId = (v)=>(
608       TypeIds[typeof v]
609         || toss("Maintenance required: this value type cannot be serialized.",v)
610     );
611     const getTypeIdById = (tid)=>{
612       switch(tid){
613           case TypeIds.number.id: return TypeIds.number;
614           case TypeIds.bigint.id: return TypeIds.bigint;
615           case TypeIds.boolean.id: return TypeIds.boolean;
616           case TypeIds.string.id: return TypeIds.string;
617           default: toss("Invalid type ID:",tid);
618       }
619     };
620     state.s11n.deserialize = function(clear=false){
621       const argc = viewU8[0];
622       const rc = argc ? [] : null;
623       if(argc){
624         const typeIds = [];
625         let offset = 1, i, n, v;
626         for(i = 0; i < argc; ++i, ++offset){
627           typeIds.push(getTypeIdById(viewU8[offset]));
628         }
629         for(i = 0; i < argc; ++i){
630           const t = typeIds[i];
631           if(t.getter){
632             v = viewDV[t.getter](offset, state.littleEndian);
633             offset += t.size;
634           }else{/*String*/
635             n = viewDV.getInt32(offset, state.littleEndian);
636             offset += 4;
637             v = textDecoder.decode(viewU8.slice(offset, offset+n));
638             offset += n;
639           }
640           rc.push(v);
641         }
642       }
643       if(clear) viewU8[0] = 0;
644       //log("deserialize:",argc, rc);
645       return rc;
646     };
647     state.s11n.serialize = function(...args){
648       if(args.length){
649         //log("serialize():",args);
650         const typeIds = [];
651         let i = 0, offset = 1;
652         viewU8[0] = args.length & 0xff /* header = # of args */;
653         for(; i < args.length; ++i, ++offset){
654           /* Write the TypeIds.id value into the next args.length
655              bytes. */
656           typeIds.push(getTypeId(args[i]));
657           viewU8[offset] = typeIds[i].id;
658         }
659         for(i = 0; i < args.length; ++i) {
660           /* Deserialize the following bytes based on their
661              corresponding TypeIds.id from the header. */
662           const t = typeIds[i];
663           if(t.setter){
664             viewDV[t.setter](offset, args[i], state.littleEndian);
665             offset += t.size;
666           }else{/*String*/
667             const s = textEncoder.encode(args[i]);
668             viewDV.setInt32(offset, s.byteLength, state.littleEndian);
669             offset += 4;
670             viewU8.set(s, offset);
671             offset += s.byteLength;
672           }
673         }
674         //log("serialize() result:",viewU8.slice(0,offset));
675       }else{
676         viewU8[0] = 0;
677       }
678     };
680     state.s11n.storeException = state.asyncS11nExceptions
681       ? ((priority,e)=>{
682         if(priority<=state.asyncS11nExceptions){
683           state.s11n.serialize([e.name,': ',e.message].join(""));
684         }
685       })
686       : ()=>{};
688     return state.s11n;
689   }/*initS11n()*/;
691   const waitLoop = async function f(){
692     const opHandlers = Object.create(null);
693     for(let k of Object.keys(state.opIds)){
694       const vi = vfsAsyncImpls[k];
695       if(!vi) continue;
696       const o = Object.create(null);
697       opHandlers[state.opIds[k]] = o;
698       o.key = k;
699       o.f = vi;
700     }
701     while(!flagAsyncShutdown){
702       try {
703         if('not-equal'!==Atomics.wait(
704           state.sabOPView, state.opIds.whichOp, 0, state.asyncIdleWaitTime
705         )){
706           /* Maintenance note: we compare against 'not-equal' because
708              https://github.com/tomayac/sqlite-wasm/issues/12
710              is reporting that this occassionally, under high loads,
711              returns 'ok', which leads to the whichOp being 0 (which
712              isn't a valid operation ID and leads to an exception,
713              along with a corresponding ugly console log
714              message). Unfortunately, the conditions for that cannot
715              be reliably reproduced. The only place in our code which
716              writes a 0 to the state.opIds.whichOp SharedArrayBuffer
717              index is a few lines down from here, and that instance
718              is required in order for clear communication between
719              the sync half of this proxy and this half.
720           */
721           await releaseImplicitLocks();
722           continue;
723         }
724         const opId = Atomics.load(state.sabOPView, state.opIds.whichOp);
725         Atomics.store(state.sabOPView, state.opIds.whichOp, 0);
726         const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId);
727         const args = state.s11n.deserialize(
728           true /* clear s11n to keep the caller from confusing this with
729                   an exception string written by the upcoming
730                   operation */
731         ) || [];
732         //warn("waitLoop() whichOp =",opId, hnd, args);
733         if(hnd.f) await hnd.f(...args);
734         else error("Missing callback for opId",opId);
735       }catch(e){
736         error('in waitLoop():',e);
737       }
738     }
739   };
741   navigator.storage.getDirectory().then(function(d){
742     state.rootDir = d;
743     globalThis.onmessage = function({data}){
744       switch(data.type){
745           case 'opfs-async-init':{
746             /* Receive shared state from synchronous partner */
747             const opt = data.args;
748             for(const k in opt) state[k] = opt[k];
749             state.verbose = opt.verbose ?? 1;
750             state.sabOPView = new Int32Array(state.sabOP);
751             state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
752             state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
753             Object.keys(vfsAsyncImpls).forEach((k)=>{
754               if(!Number.isFinite(state.opIds[k])){
755                 toss("Maintenance required: missing state.opIds[",k,"]");
756               }
757             });
758             initS11n();
759             log("init state",state);
760             wPost('opfs-async-inited');
761             waitLoop();
762             break;
763           }
764           case 'opfs-async-restart':
765             if(flagAsyncShutdown){
766               warn("Restarting after opfs-async-shutdown. Might or might not work.");
767               flagAsyncShutdown = false;
768               waitLoop();
769             }
770             break;
771       }
772     };
773     wPost('opfs-async-loaded');
774   }).catch((e)=>error("error initializing OPFS asyncer:",e));
775 }/*installAsyncProxy()*/;
776 if(!globalThis.SharedArrayBuffer){
777   wPost('opfs-unavailable', "Missing SharedArrayBuffer API.",
778         "The server must emit the COOP/COEP response headers to enable that.");
779 }else if(!globalThis.Atomics){
780   wPost('opfs-unavailable', "Missing Atomics API.",
781         "The server must emit the COOP/COEP response headers to enable that.");
782 }else if(!globalThis.FileSystemHandle ||
783          !globalThis.FileSystemDirectoryHandle ||
784          !globalThis.FileSystemFileHandle ||
785          !globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle ||
786          !navigator?.storage?.getDirectory){
787   wPost('opfs-unavailable',"Missing required OPFS APIs.");
788 }else{
789   installAsyncProxy();