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.
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
36 https://developer.chrome.com/blog/sync-methods-for-accesshandles/
38 Firefox v111 and Safari 16.4, both released in March 2023, also
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".
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.");
64 Will hold state copied to this object from the syncronous side of
67 const state = Object.create(null);
74 2 = warnings and errors
75 3 = debug, warnings, and errors
80 0:console.error.bind(console),
81 1:console.warn.bind(console),
82 2:console.log.bind(console)
84 const logImpl = (level,...args)=>{
85 if(state.verbose>level) loggers[level]("OPFS asyncer:",...args);
87 const log = (...args)=>logImpl(2, ...args);
88 const warn = (...args)=>logImpl(1, ...args);
89 const error = (...args)=>logImpl(0, ...args);
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.
98 const __openFiles = Object.create(null);
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.
112 const __implicitLocks = new Set();
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.
120 const getResolvedPath = function(filename,splitIt){
122 filename, 'file://irrelevant'
124 return splitIt ? p.split('/').filter((v)=>!!v) : p;
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.
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){
139 dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs});
142 return [dh, filename];
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.
155 const closeSyncHandle = async (fh)=>{
157 log("Closing sync handle for",fh.filenameAbs);
158 const h = fh.syncHandle;
159 delete fh.syncHandle;
161 __implicitLocks.delete(fh.fid);
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
177 const closeSyncHandleNoThrow = async (fh)=>{
178 try{await closeSyncHandle(fh)}
180 warn("closeSyncHandleNoThrow() ignoring:",e,fh);
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);
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.
204 const releaseImplicitLock = async (fh)=>{
205 if(fh.releaseImplicitLocks && __implicitLocks.has(fh.fid)){
206 return closeSyncHandleNoThrow(fh);
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
228 class GetSyncHandleError extends Error {
229 constructor(errorObject, ...msg){
231 ...msg, ': '+errorObject.name+':',
236 this.name = 'GetSyncHandleError';
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.
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 ){
255 Maintenance reminder: SQLITE_NOTFOUND, though it looks like
256 a good match, has different semantics than NotFoundError
257 and is not suitable here.
259 return state.sq3Codes.SQLITE_CANTOPEN;
261 }else if( 'NotFoundError'===e?.name ){
262 return state.sq3Codes.SQLITE_CANTOPEN;
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
279 const getSyncHandle = async (fh,opName)=>{
281 const t = performance.now();
282 log("Acquiring sync handle for",fh.filenameAbs);
284 msBase = state.asyncIdleWaitTime * 2;
285 let i = 1, ms = msBase;
286 for(; true; ms = msBase * ++i){
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();
295 throw new GetSyncHandleError(
296 e, "Error getting sync handle for",opName+"().",maxTries,
297 "attempts failed.",fh.filenameAbs
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);
305 log("Got",opName+"() sync handle for",fh.filenameAbs,
306 'in',performance.now() - t,'ms');
308 __implicitLocks.add(fh.fid);
309 log("Acquired implicit lock for",opName+"()",fh.fid,fh.filenameAbs);
312 return fh.syncHandle;
316 Stores the given value at state.sabOPView[state.opIds.rc] and then
317 Atomics.notify()'s it.
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);
326 Throws if fh is a file-holding object which is flagged as read-only.
328 const affirmNotRO = function(opName,fh){
329 if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs);
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.
338 let flagAsyncShutdown = false;
341 Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods
342 methods, as well as helpers like mkdir().
344 const vfsAsyncImpls = {
345 'opfs-async-shutdown': async ()=>{
346 flagAsyncShutdown = true;
347 storeAndNotify('opfs-async-shutdown', 0);
349 mkdir: async (dirname)=>{
352 await getDirForFilename(dirname+"/filepart", true);
354 state.s11n.storeException(2,e);
355 rc = state.sq3Codes.SQLITE_IOERR;
357 storeAndNotify('mkdir', rc);
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.
373 const [dh, fn] = await getDirForFilename(filename);
374 await dh.getFileHandle(fn);
376 state.s11n.storeException(2,e);
377 rc = state.sq3Codes.SQLITE_IOERR;
379 storeAndNotify('xAccess', rc);
381 xClose: async function(fid/*sqlite3_file pointer*/){
382 const opName = 'xClose';
383 __implicitLocks.delete(fid);
384 const fh = __openFiles[fid];
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) }
394 state.s11n.serialize();
395 rc = state.sq3Codes.SQLITE_NOTFOUND;
397 storeAndNotify(opName, rc);
399 xDelete: async function(...args){
400 const rc = await vfsAsyncImpls.xDeleteNoWait(...args);
401 storeAndNotify('xDelete', rc);
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
419 const [hDir, filenamePart] = await getDirForFilename(filename, false);
420 if(!filenamePart) break;
421 await hDir.removeEntry(filenamePart, {recursive});
422 if(0x1234 !== syncDir) break;
424 filename = getResolvedPath(filename, true);
426 filename = filename.join('/');
429 state.s11n.storeException(2,e);
430 rc = state.sq3Codes.SQLITE_IOERR_DELETE;
434 xFileSize: async function(fid/*sqlite3_file pointer*/){
435 const fh = __openFiles[fid];
438 const sz = await (await getSyncHandle(fh,'xFileSize')).getSize();
439 state.s11n.serialize(Number(sz));
441 state.s11n.storeException(1,e);
442 rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR);
444 await releaseImplicitLock(fh);
445 storeAndNotify('xFileSize', rc);
447 xLock: async function(fid/*sqlite3_file pointer*/,
448 lockType/*SQLITE_LOCK_...*/){
449 const fh = __openFiles[fid];
451 const oldLockType = fh.xLock;
453 if( !fh.syncHandle ){
455 await getSyncHandle(fh,'xLock');
456 __implicitLocks.delete(fid);
458 state.s11n.storeException(1,e);
459 rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_LOCK);
460 fh.xLock = oldLockType;
463 storeAndNotify('xLock',rc);
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);
471 let hDir, filenamePart;
473 [hDir, filenamePart] = await getDirForFilename(filename, !!create);
475 state.s11n.storeException(1,e);
476 storeAndNotify(opName, state.sq3Codes.SQLITE_NOTFOUND);
479 if( state.opfsFlags.OPFS_UNLINK_BEFORE_OPEN & opfsFlags ){
481 await hDir.removeEntry(filenamePart);
484 //warn("Ignoring failed Unlink of",filename,":",e);
487 const hFile = await hDir.getFileHandle(filenamePart, {create});
488 const fh = Object.assign(Object.create(null),{
490 filenameAbs: filename,
491 filenamePart: filenamePart,
494 sabView: state.sabFileBufView,
496 ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags),
497 deleteOnClose: !!(state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags)
499 fh.releaseImplicitLocks =
500 (opfsFlags & state.opfsFlags.OPFS_UNLOCK_ASAP)
501 || state.opfsFlags.defaultUnlockAsap;
502 __openFiles[fid] = fh;
503 storeAndNotify(opName, 0);
506 state.s11n.storeException(1,e);
507 storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
510 xRead: async function(fid/*sqlite3_file pointer*/,n,offset64){
512 const fh = __openFiles[fid];
514 nRead = (await getSyncHandle(fh,'xRead')).read(
515 fh.sabView.subarray(0, n),
516 {at: Number(offset64)}
518 if(nRead < n){/* Zero-fill remaining bytes */
519 fh.sabView.fill(0, nRead, n);
520 rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
523 error("xRead() failed",e,fh);
524 state.s11n.storeException(1,e);
525 rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_READ);
527 await releaseImplicitLock(fh);
528 storeAndNotify('xRead',rc);
530 xSync: async function(fid/*sqlite3_file pointer*/,flags/*ignored*/){
531 const fh = __openFiles[fid];
533 if(!fh.readOnly && fh.syncHandle){
535 await fh.syncHandle.flush();
537 state.s11n.storeException(2,e);
538 rc = state.sq3Codes.SQLITE_IOERR_FSYNC;
541 storeAndNotify('xSync',rc);
543 xTruncate: async function(fid/*sqlite3_file pointer*/,size){
545 const fh = __openFiles[fid];
547 affirmNotRO('xTruncate', fh);
548 await (await getSyncHandle(fh,'xTruncate')).truncate(size);
550 error("xTruncate():",e,fh);
551 state.s11n.storeException(2,e);
552 rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_TRUNCATE);
554 await releaseImplicitLock(fh);
555 storeAndNotify('xTruncate',rc);
557 xUnlock: async function(fid/*sqlite3_file pointer*/,
558 lockType/*SQLITE_LOCK_...*/){
560 const fh = __openFiles[fid];
561 if( state.sq3Codes.SQLITE_LOCK_NONE===lockType
563 try { await closeSyncHandle(fh) }
565 state.s11n.storeException(1,e);
566 rc = state.sq3Codes.SQLITE_IOERR_UNLOCK;
569 storeAndNotify('xUnlock',rc);
571 xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){
573 const fh = __openFiles[fid];
575 affirmNotRO('xWrite', fh);
577 n === (await getSyncHandle(fh,'xWrite'))
578 .write(fh.sabView.subarray(0, n),
579 {at: Number(offset64)})
580 ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
582 error("xWrite():",e,fh);
583 state.s11n.storeException(1,e);
584 rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_WRITE);
586 await releaseImplicitLock(fh);
587 storeAndNotify('xWrite',rc);
591 const initS11n = ()=>{
593 ACHTUNG: this code is 100% duplicated in the other half of this
594 proxy! The documentation is maintained in the "synchronous half".
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)=>(
609 || toss("Maintenance required: this value type cannot be serialized.",v)
611 const getTypeIdById = (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);
620 state.s11n.deserialize = function(clear=false){
621 const argc = viewU8[0];
622 const rc = argc ? [] : null;
625 let offset = 1, i, n, v;
626 for(i = 0; i < argc; ++i, ++offset){
627 typeIds.push(getTypeIdById(viewU8[offset]));
629 for(i = 0; i < argc; ++i){
630 const t = typeIds[i];
632 v = viewDV[t.getter](offset, state.littleEndian);
635 n = viewDV.getInt32(offset, state.littleEndian);
637 v = textDecoder.decode(viewU8.slice(offset, offset+n));
643 if(clear) viewU8[0] = 0;
644 //log("deserialize:",argc, rc);
647 state.s11n.serialize = function(...args){
649 //log("serialize():",args);
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
656 typeIds.push(getTypeId(args[i]));
657 viewU8[offset] = typeIds[i].id;
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];
664 viewDV[t.setter](offset, args[i], state.littleEndian);
667 const s = textEncoder.encode(args[i]);
668 viewDV.setInt32(offset, s.byteLength, state.littleEndian);
670 viewU8.set(s, offset);
671 offset += s.byteLength;
674 //log("serialize() result:",viewU8.slice(0,offset));
680 state.s11n.storeException = state.asyncS11nExceptions
682 if(priority<=state.asyncS11nExceptions){
683 state.s11n.serialize([e.name,': ',e.message].join(""));
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];
696 const o = Object.create(null);
697 opHandlers[state.opIds[k]] = o;
701 while(!flagAsyncShutdown){
703 if('not-equal'!==Atomics.wait(
704 state.sabOPView, state.opIds.whichOp, 0, state.asyncIdleWaitTime
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.
721 await releaseImplicitLocks();
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
732 //warn("waitLoop() whichOp =",opId, hnd, args);
733 if(hnd.f) await hnd.f(...args);
734 else error("Missing callback for opId",opId);
736 error('in waitLoop():',e);
741 navigator.storage.getDirectory().then(function(d){
743 globalThis.onmessage = function({data}){
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,"]");
759 log("init state",state);
760 wPost('opfs-async-inited');
764 case 'opfs-async-restart':
765 if(flagAsyncShutdown){
766 warn("Restarting after opfs-async-shutdown. Might or might not work.");
767 flagAsyncShutdown = false;
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.");