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 We cannot change to the sync forms at this point without breaking
39 clients who use Chrome v104-ish or higher. truncate(), getSize(),
40 flush(), and close() are now (as of v108) synchronous. Calling them
41 with an "await", as we have to for the async forms, is still legal
42 with the sync forms but is superfluous. Calling the async forms with
43 theFunc().then(...) is not compatible with the change to
44 synchronous, but we do do not use those APIs that way. i.e. we don't
45 _need_ to change anything for this, but at some point (after Chrome
46 versions (approximately) 104-107 are extinct) should change our
47 usage of those methods to remove the "await".
50 const wPost
= (type
,...args
)=>postMessage({type
, payload
:args
});
51 const installAsyncProxy = function(self
){
52 const toss = function(...args
){throw new Error(args
.join(' '))};
53 if(self
.window
=== self
){
54 toss("This code cannot run from the main thread.",
55 "Load it as a Worker from a separate Worker.");
56 }else if(!navigator
.storage
.getDirectory
){
57 toss("This API requires navigator.storage.getDirectory.");
61 Will hold state copied to this object from the syncronous side of
64 const state
= Object
.create(null);
71 2 = warnings and errors
72 3 = debug, warnings, and errors
77 0:console
.error
.bind(console
),
78 1:console
.warn
.bind(console
),
79 2:console
.log
.bind(console
)
81 const logImpl
= (level
,...args
)=>{
82 if(state
.verbose
>level
) loggers
[level
]("OPFS asyncer:",...args
);
84 const log
= (...args
)=>logImpl(2, ...args
);
85 const warn
= (...args
)=>logImpl(1, ...args
);
86 const error
= (...args
)=>logImpl(0, ...args
);
87 const metrics
= Object
.create(null);
90 const r
= (m
)=>(m
.count
= m
.time
= m
.wait
= 0);
91 for(k
in state
.opIds
){
92 r(metrics
[k
] = Object
.create(null));
94 let s
= metrics
.s11n
= Object
.create(null);
95 s
= s
.serialize
= Object
.create(null);
97 s
= metrics
.s11n
.deserialize
= Object
.create(null);
101 let k
, n
= 0, t
= 0, w
= 0;
102 for(k
in state
.opIds
){
103 const m
= metrics
[k
];
107 m
.avgTime
= (m
.count
&& m
.time
) ? (m
.time
/ m
.count
) : 0;
109 console
.log(self
.location
.href
,
110 "metrics for",self
.location
.href
,":\n",
112 "\nTotal of",n
,"op(s) for",t
,"ms",
113 "approx",w
,"ms spent waiting on OPFS APIs.");
114 console
.log("Serialization metrics:",metrics
.s11n
);
118 __openFiles is a map of sqlite3_file pointers (integers) to
119 metadata related to a given OPFS file handles. The pointers are, in
120 this side of the interface, opaque file handle IDs provided by the
121 synchronous part of this constellation. Each value is an object
122 with a structure demonstrated in the xOpen() impl.
124 const __openFiles
= Object
.create(null);
126 __implicitLocks is a Set of sqlite3_file pointers (integers) which were
127 "auto-locked". i.e. those for which we obtained a sync access
128 handle without an explicit xLock() call. Such locks will be
129 released during db connection idle time, whereas a sync access
130 handle obtained via xLock(), or subsequently xLock()'d after
131 auto-acquisition, will not be released until xUnlock() is called.
133 Maintenance reminder: if we relinquish auto-locks at the end of the
134 operation which acquires them, we pay a massive performance
135 penalty: speedtest1 benchmarks take up to 4x as long. By delaying
136 the lock release until idle time, the hit is negligible.
138 const __implicitLocks
= new Set();
141 Expects an OPFS file path. It gets resolved, such that ".."
142 components are properly expanded, and returned. If the 2nd arg is
143 true, the result is returned as an array of path elements, else an
144 absolute path string is returned.
146 const getResolvedPath = function(filename
,splitIt
){
148 filename
, 'file://irrelevant'
150 return splitIt
? p
.split('/').filter((v
)=>!!v
) : p
;
154 Takes the absolute path to a filesystem element. Returns an array
155 of [handleOfContainingDir, filename]. If the 2nd argument is truthy
156 then each directory element leading to the file is created along
157 the way. Throws if any creation or resolution fails.
159 const getDirForFilename
= async
function f(absFilename
, createDirs
= false){
160 const path
= getResolvedPath(absFilename
, true);
161 const filename
= path
.pop();
162 let dh
= state
.rootDir
;
163 for(const dirName
of path
){
165 dh
= await dh
.getDirectoryHandle(dirName
, {create
: !!createDirs
});
168 return [dh
, filename
];
172 If the given file-holding object has a sync handle attached to it,
173 that handle is remove and asynchronously closed. Though it may
174 sound sensible to continue work as soon as the close() returns
175 (noting that it's asynchronous), doing so can cause operations
176 performed soon afterwards, e.g. a call to getSyncHandle() to fail
177 because they may happen out of order from the close(). OPFS does
178 not guaranty that the actual order of operations is retained in
179 such cases. i.e. always "await" on the result of this function.
181 const closeSyncHandle
= async (fh
)=>{
183 log("Closing sync handle for",fh
.filenameAbs
);
184 const h
= fh
.syncHandle
;
185 delete fh
.syncHandle
;
187 __implicitLocks
.delete(fh
.fid
);
193 A proxy for closeSyncHandle() which is guaranteed to not throw.
195 This function is part of a lock/unlock step in functions which
196 require a sync access handle but may be called without xLock()
197 having been called first. Such calls need to release that
198 handle to avoid locking the file for all of time. This is an
199 _attempt_ at reducing cross-tab contention but it may prove
200 to be more of a problem than a solution and may need to be
203 const closeSyncHandleNoThrow
= async (fh
)=>{
204 try{await
closeSyncHandle(fh
)}
206 warn("closeSyncHandleNoThrow() ignoring:",e
,fh
);
210 /* Release all auto-locks. */
211 const releaseImplicitLocks
= async ()=>{
212 if(__implicitLocks
.size
){
213 /* Release all auto-locks. */
214 for(const fid
of __implicitLocks
){
215 const fh
= __openFiles
[fid
];
216 await
closeSyncHandleNoThrow(fh
);
217 log("Auto-unlocked",fid
,fh
.filenameAbs
);
223 An experiment in improving concurrency by freeing up implicit locks
224 sooner. This is known to impact performance dramatically but it has
225 also shown to improve concurrency considerably.
227 If fh.releaseImplicitLocks is truthy and fh is in __implicitLocks,
228 this routine returns closeSyncHandleNoThrow(), else it is a no-op.
230 const releaseImplicitLock
= async (fh
)=>{
231 if(fh
.releaseImplicitLocks
&& __implicitLocks
.has(fh
.fid
)){
232 return closeSyncHandleNoThrow(fh
);
237 An error class specifically for use with getSyncHandle(), the goal
238 of which is to eventually be able to distinguish unambiguously
239 between locking-related failures and other types, noting that we
240 cannot currently do so because createSyncAccessHandle() does not
241 define its exceptions in the required level of detail.
243 2022-11-29: according to:
245 https://github.com/whatwg/fs/pull/21
247 NoModificationAllowedError will be the standard exception thrown
248 when acquisition of a sync access handle fails due to a locking
249 error. As of this writing, that error type is not visible in the
250 dev console in Chrome v109, nor is it documented in MDN, but an
251 error with that "name" property is being thrown from the OPFS
254 class GetSyncHandleError
extends Error
{
255 constructor(errorObject
, ...msg
){
257 ...msg
, ': '+errorObject
.name
+':',
262 this.name
= 'GetSyncHandleError';
265 GetSyncHandleError
.convertRc
= (e
,rc
)=>{
268 e
instanceof GetSyncHandleError
269 && ((e
.cause
.name
==='NoModificationAllowedError')
270 /* Inconsistent exception.name from Chrome/ium with the
271 same exception.message text: */
272 || (e
.cause
.name
==='DOMException'
273 && 0===e
.cause
.message
.indexOf('Access Handles cannot')))
275 /*console.warn("SQLITE_BUSY",e),*/
276 state
.sq3Codes
.SQLITE_BUSY
283 Returns the sync access handle associated with the given file
284 handle object (which must be a valid handle object, as created by
285 xOpen()), lazily opening it if needed.
287 In order to help alleviate cross-tab contention for a dabase, if
288 an exception is thrown while acquiring the handle, this routine
289 will wait briefly and try again, up to some fixed number of
290 times. If acquisition still fails at that point it will give up
291 and propagate the exception. Client-level code will see that as
294 const getSyncHandle
= async (fh
,opName
)=>{
296 const t
= performance
.now();
297 log("Acquiring sync handle for",fh
.filenameAbs
);
299 msBase
= state
.asyncIdleWaitTime
* 2;
300 let i
= 1, ms
= msBase
;
301 for(; true; ms
= msBase
* ++i
){
303 //if(i<3) toss("Just testing getSyncHandle() wait-and-retry.");
304 //TODO? A config option which tells it to throw here
305 //randomly every now and then, for testing purposes.
306 fh
.syncHandle
= await fh
.fileHandle
.createSyncAccessHandle();
310 throw new GetSyncHandleError(
311 e
, "Error getting sync handle for",opName
+"().",maxTries
,
312 "attempts failed.",fh
.filenameAbs
315 warn("Error getting sync handle for",opName
+"(). Waiting",ms
,
316 "ms and trying again.",fh
.filenameAbs
,e
);
317 Atomics
.wait(state
.sabOPView
, state
.opIds
.retry
, 0, ms
);
320 log("Got",opName
+"() sync handle for",fh
.filenameAbs
,
321 'in',performance
.now() - t
,'ms');
323 __implicitLocks
.add(fh
.fid
);
324 log("Acquired implicit lock for",opName
+"()",fh
.fid
,fh
.filenameAbs
);
327 return fh
.syncHandle
;
331 Stores the given value at state.sabOPView[state.opIds.rc] and then
332 Atomics.notify()'s it.
334 const storeAndNotify
= (opName
, value
)=>{
335 log(opName
+"() => notify(",value
,")");
336 Atomics
.store(state
.sabOPView
, state
.opIds
.rc
, value
);
337 Atomics
.notify(state
.sabOPView
, state
.opIds
.rc
);
341 Throws if fh is a file-holding object which is flagged as read-only.
343 const affirmNotRO = function(opName
,fh
){
344 if(fh
.readOnly
) toss(opName
+"(): File is read-only: "+fh
.filenameAbs
);
348 We track 2 different timers: the "metrics" timer records how much
349 time we spend performing work. The "wait" timer records how much
350 time we spend waiting on the underlying OPFS timer. See the calls
351 to mTimeStart(), mTimeEnd(), wTimeStart(), and wTimeEnd()
352 throughout this file to see how they're used.
354 const __mTimer
= Object
.create(null);
355 __mTimer
.op
= undefined;
356 __mTimer
.start
= undefined;
357 const mTimeStart
= (op
)=>{
358 __mTimer
.start
= performance
.now();
360 //metrics[op] || toss("Maintenance required: missing metrics for",op);
363 const mTimeEnd
= ()=>(
364 metrics
[__mTimer
.op
].time
+= performance
.now() - __mTimer
.start
366 const __wTimer
= Object
.create(null);
367 __wTimer
.op
= undefined;
368 __wTimer
.start
= undefined;
369 const wTimeStart
= (op
)=>{
370 __wTimer
.start
= performance
.now();
372 //metrics[op] || toss("Maintenance required: missing metrics for",op);
374 const wTimeEnd
= ()=>(
375 metrics
[__wTimer
.op
].wait
+= performance
.now() - __wTimer
.start
379 Gets set to true by the 'opfs-async-shutdown' command to quit the
380 wait loop. This is only intended for debugging purposes: we cannot
381 inspect this file's state while the tight waitLoop() is running and
382 need a way to stop that loop for introspection purposes.
384 let flagAsyncShutdown
= false;
387 Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods
388 methods, as well as helpers like mkdir(). Maintenance reminder:
389 members are in alphabetical order to simplify finding them.
391 const vfsAsyncImpls
= {
392 'opfs-async-metrics': async ()=>{
393 mTimeStart('opfs-async-metrics');
395 storeAndNotify('opfs-async-metrics', 0);
398 'opfs-async-shutdown': async ()=>{
399 flagAsyncShutdown
= true;
400 storeAndNotify('opfs-async-shutdown', 0);
402 mkdir
: async (dirname
)=>{
407 await
getDirForFilename(dirname
+"/filepart", true);
409 state
.s11n
.storeException(2,e
);
410 rc
= state
.sq3Codes
.SQLITE_IOERR
;
414 storeAndNotify('mkdir', rc
);
417 xAccess
: async (filename
)=>{
418 mTimeStart('xAccess');
419 /* OPFS cannot support the full range of xAccess() queries
420 sqlite3 calls for. We can essentially just tell if the file
421 is accessible, but if it is then it's automatically writable
422 (unless it's locked, which we cannot(?) know without trying
423 to open it). OPFS does not have the notion of read-only.
425 The return semantics of this function differ from sqlite3's
426 xAccess semantics because we are limited in what we can
427 communicate back to our synchronous communication partner: 0 =
428 accessible, non-0 means not accessible.
431 wTimeStart('xAccess');
433 const [dh
, fn
] = await
getDirForFilename(filename
);
434 await dh
.getFileHandle(fn
);
436 state
.s11n
.storeException(2,e
);
437 rc
= state
.sq3Codes
.SQLITE_IOERR
;
441 storeAndNotify('xAccess', rc
);
444 xClose
: async
function(fid
/*sqlite3_file pointer*/){
445 const opName
= 'xClose';
447 __implicitLocks
.delete(fid
);
448 const fh
= __openFiles
[fid
];
452 delete __openFiles
[fid
];
453 await
closeSyncHandle(fh
);
454 if(fh
.deleteOnClose
){
455 try{ await fh
.dirHandle
.removeEntry(fh
.filenamePart
) }
456 catch(e
){ warn("Ignoring dirHandle.removeEntry() failure of",fh
,e
) }
459 state
.s11n
.serialize();
460 rc
= state
.sq3Codes
.SQLITE_NOTFOUND
;
463 storeAndNotify(opName
, rc
);
466 xDelete
: async
function(...args
){
467 mTimeStart('xDelete');
468 const rc
= await vfsAsyncImpls
.xDeleteNoWait(...args
);
469 storeAndNotify('xDelete', rc
);
472 xDeleteNoWait
: async
function(filename
, syncDir
= 0, recursive
= false){
473 /* The syncDir flag is, for purposes of the VFS API's semantics,
474 ignored here. However, if it has the value 0x1234 then: after
475 deleting the given file, recursively try to delete any empty
476 directories left behind in its wake (ignoring any errors and
477 stopping at the first failure).
479 That said: we don't know for sure that removeEntry() fails if
480 the dir is not empty because the API is not documented. It has,
481 however, a "recursive" flag which defaults to false, so
482 presumably it will fail if the dir is not empty and that flag
486 wTimeStart('xDelete');
489 const [hDir
, filenamePart
] = await
getDirForFilename(filename
, false);
490 if(!filenamePart
) break;
491 await hDir
.removeEntry(filenamePart
, {recursive
});
492 if(0x1234 !== syncDir
) break;
494 filename
= getResolvedPath(filename
, true);
496 filename
= filename
.join('/');
499 state
.s11n
.storeException(2,e
);
500 rc
= state
.sq3Codes
.SQLITE_IOERR_DELETE
;
505 xFileSize
: async
function(fid
/*sqlite3_file pointer*/){
506 mTimeStart('xFileSize');
507 const fh
= __openFiles
[fid
];
509 wTimeStart('xFileSize');
511 const sz
= await (await
getSyncHandle(fh
,'xFileSize')).getSize();
512 state
.s11n
.serialize(Number(sz
));
514 state
.s11n
.storeException(1,e
);
515 rc
= GetSyncHandleError
.convertRc(e
,state
.sq3Codes
.SQLITE_IOERR
);
517 await
releaseImplicitLock(fh
);
519 storeAndNotify('xFileSize', rc
);
522 xLock
: async
function(fid
/*sqlite3_file pointer*/,
523 lockType
/*SQLITE_LOCK_...*/){
525 const fh
= __openFiles
[fid
];
527 const oldLockType
= fh
.xLock
;
529 if( !fh
.syncHandle
){
532 await
getSyncHandle(fh
,'xLock');
533 __implicitLocks
.delete(fid
);
535 state
.s11n
.storeException(1,e
);
536 rc
= GetSyncHandleError
.convertRc(e
,state
.sq3Codes
.SQLITE_IOERR_LOCK
);
537 fh
.xLock
= oldLockType
;
541 storeAndNotify('xLock',rc
);
544 xOpen
: async
function(fid
/*sqlite3_file pointer*/, filename
,
545 flags
/*SQLITE_OPEN_...*/,
546 opfsFlags
/*OPFS_...*/){
547 const opName
= 'xOpen';
549 const create
= (state
.sq3Codes
.SQLITE_OPEN_CREATE
& flags
);
552 let hDir
, filenamePart
;
554 [hDir
, filenamePart
] = await
getDirForFilename(filename
, !!create
);
556 state
.s11n
.storeException(1,e
);
557 storeAndNotify(opName
, state
.sq3Codes
.SQLITE_NOTFOUND
);
562 const hFile
= await hDir
.getFileHandle(filenamePart
, {create
});
564 const fh
= Object
.assign(Object
.create(null),{
566 filenameAbs
: filename
,
567 filenamePart
: filenamePart
,
570 sabView
: state
.sabFileBufView
,
572 ? false : (state
.sq3Codes
.SQLITE_OPEN_READONLY
& flags
),
573 deleteOnClose
: !!(state
.sq3Codes
.SQLITE_OPEN_DELETEONCLOSE
& flags
)
575 fh
.releaseImplicitLocks
=
576 (opfsFlags
& state
.opfsFlags
.OPFS_UNLOCK_ASAP
)
577 || state
.opfsFlags
.defaultUnlockAsap
;
578 if(0 /* this block is modelled after something wa-sqlite
579 does but it leads to immediate contention on journal files.
580 Update: this approach reportedly only works for DELETE journal
582 && (0===(flags
& state
.sq3Codes
.SQLITE_OPEN_MAIN_DB
))){
583 /* sqlite does not lock these files, so go ahead and grab an OPFS
585 fh
.xLock
= "xOpen"/* Truthy value to keep entry from getting
586 flagged as auto-locked. String value so
587 that we can easily distinguish is later
589 await
getSyncHandle(fh
,'xOpen');
591 __openFiles
[fid
] = fh
;
592 storeAndNotify(opName
, 0);
596 state
.s11n
.storeException(1,e
);
597 storeAndNotify(opName
, state
.sq3Codes
.SQLITE_IOERR
);
601 xRead
: async
function(fid
/*sqlite3_file pointer*/,n
,offset64
){
604 const fh
= __openFiles
[fid
];
607 nRead
= (await
getSyncHandle(fh
,'xRead')).read(
608 fh
.sabView
.subarray(0, n
),
609 {at
: Number(offset64
)}
612 if(nRead
< n
){/* Zero-fill remaining bytes */
613 fh
.sabView
.fill(0, nRead
, n
);
614 rc
= state
.sq3Codes
.SQLITE_IOERR_SHORT_READ
;
617 if(undefined===nRead
) wTimeEnd();
618 error("xRead() failed",e
,fh
);
619 state
.s11n
.storeException(1,e
);
620 rc
= GetSyncHandleError
.convertRc(e
,state
.sq3Codes
.SQLITE_IOERR_READ
);
622 await
releaseImplicitLock(fh
);
623 storeAndNotify('xRead',rc
);
626 xSync
: async
function(fid
/*sqlite3_file pointer*/,flags
/*ignored*/){
628 const fh
= __openFiles
[fid
];
630 if(!fh
.readOnly
&& fh
.syncHandle
){
633 await fh
.syncHandle
.flush();
635 state
.s11n
.storeException(2,e
);
636 rc
= state
.sq3Codes
.SQLITE_IOERR_FSYNC
;
640 storeAndNotify('xSync',rc
);
643 xTruncate
: async
function(fid
/*sqlite3_file pointer*/,size
){
644 mTimeStart('xTruncate');
646 const fh
= __openFiles
[fid
];
647 wTimeStart('xTruncate');
649 affirmNotRO('xTruncate', fh
);
650 await (await
getSyncHandle(fh
,'xTruncate')).truncate(size
);
652 error("xTruncate():",e
,fh
);
653 state
.s11n
.storeException(2,e
);
654 rc
= GetSyncHandleError
.convertRc(e
,state
.sq3Codes
.SQLITE_IOERR_TRUNCATE
);
656 await
releaseImplicitLock(fh
);
658 storeAndNotify('xTruncate',rc
);
661 xUnlock
: async
function(fid
/*sqlite3_file pointer*/,
662 lockType
/*SQLITE_LOCK_...*/){
663 mTimeStart('xUnlock');
665 const fh
= __openFiles
[fid
];
666 if( state
.sq3Codes
.SQLITE_LOCK_NONE
===lockType
668 wTimeStart('xUnlock');
669 try { await
closeSyncHandle(fh
) }
671 state
.s11n
.storeException(1,e
);
672 rc
= state
.sq3Codes
.SQLITE_IOERR_UNLOCK
;
676 storeAndNotify('xUnlock',rc
);
679 xWrite
: async
function(fid
/*sqlite3_file pointer*/,n
,offset64
){
680 mTimeStart('xWrite');
682 const fh
= __openFiles
[fid
];
683 wTimeStart('xWrite');
685 affirmNotRO('xWrite', fh
);
687 n
=== (await
getSyncHandle(fh
,'xWrite'))
688 .write(fh
.sabView
.subarray(0, n
),
689 {at
: Number(offset64
)})
690 ) ? 0 : state
.sq3Codes
.SQLITE_IOERR_WRITE
;
692 error("xWrite():",e
,fh
);
693 state
.s11n
.storeException(1,e
);
694 rc
= GetSyncHandleError
.convertRc(e
,state
.sq3Codes
.SQLITE_IOERR_WRITE
);
696 await
releaseImplicitLock(fh
);
698 storeAndNotify('xWrite',rc
);
703 const initS11n
= ()=>{
705 ACHTUNG: this code is 100% duplicated in the other half of this
706 proxy! The documentation is maintained in the "synchronous half".
708 if(state
.s11n
) return state
.s11n
;
709 const textDecoder
= new TextDecoder(),
710 textEncoder
= new TextEncoder('utf-8'),
711 viewU8
= new Uint8Array(state
.sabIO
, state
.sabS11nOffset
, state
.sabS11nSize
),
712 viewDV
= new DataView(state
.sabIO
, state
.sabS11nOffset
, state
.sabS11nSize
);
713 state
.s11n
= Object
.create(null);
714 const TypeIds
= Object
.create(null);
715 TypeIds
.number
= { id
: 1, size
: 8, getter
: 'getFloat64', setter
: 'setFloat64' };
716 TypeIds
.bigint
= { id
: 2, size
: 8, getter
: 'getBigInt64', setter
: 'setBigInt64' };
717 TypeIds
.boolean = { id
: 3, size
: 4, getter
: 'getInt32', setter
: 'setInt32' };
718 TypeIds
.string
= { id
: 4 };
719 const getTypeId
= (v
)=>(
721 || toss("Maintenance required: this value type cannot be serialized.",v
)
723 const getTypeIdById
= (tid
)=>{
725 case TypeIds
.number
.id
: return TypeIds
.number
;
726 case TypeIds
.bigint
.id
: return TypeIds
.bigint
;
727 case TypeIds
.boolean.id
: return TypeIds
.boolean;
728 case TypeIds
.string
.id
: return TypeIds
.string
;
729 default: toss("Invalid type ID:",tid
);
732 state
.s11n
.deserialize = function(clear
=false){
733 ++metrics
.s11n
.deserialize
.count
;
734 const t
= performance
.now();
735 const argc
= viewU8
[0];
736 const rc
= argc
? [] : null;
739 let offset
= 1, i
, n
, v
;
740 for(i
= 0; i
< argc
; ++i
, ++offset
){
741 typeIds
.push(getTypeIdById(viewU8
[offset
]));
743 for(i
= 0; i
< argc
; ++i
){
744 const t
= typeIds
[i
];
746 v
= viewDV
[t
.getter
](offset
, state
.littleEndian
);
749 n
= viewDV
.getInt32(offset
, state
.littleEndian
);
751 v
= textDecoder
.decode(viewU8
.slice(offset
, offset
+n
));
757 if(clear
) viewU8
[0] = 0;
758 //log("deserialize:",argc, rc);
759 metrics
.s11n
.deserialize
.time
+= performance
.now() - t
;
762 state
.s11n
.serialize = function(...args
){
763 const t
= performance
.now();
764 ++metrics
.s11n
.serialize
.count
;
766 //log("serialize():",args);
768 let i
= 0, offset
= 1;
769 viewU8
[0] = args
.length
& 0xff /* header = # of args */;
770 for(; i
< args
.length
; ++i
, ++offset
){
771 /* Write the TypeIds.id value into the next args.length
773 typeIds
.push(getTypeId(args
[i
]));
774 viewU8
[offset
] = typeIds
[i
].id
;
776 for(i
= 0; i
< args
.length
; ++i
) {
777 /* Deserialize the following bytes based on their
778 corresponding TypeIds.id from the header. */
779 const t
= typeIds
[i
];
781 viewDV
[t
.setter
](offset
, args
[i
], state
.littleEndian
);
784 const s
= textEncoder
.encode(args
[i
]);
785 viewDV
.setInt32(offset
, s
.byteLength
, state
.littleEndian
);
787 viewU8
.set(s
, offset
);
788 offset
+= s
.byteLength
;
791 //log("serialize() result:",viewU8.slice(0,offset));
795 metrics
.s11n
.serialize
.time
+= performance
.now() - t
;
798 state
.s11n
.storeException
= state
.asyncS11nExceptions
800 if(priority
<=state
.asyncS11nExceptions
){
801 state
.s11n
.serialize([e
.name
,': ',e
.message
].join(""));
809 const waitLoop
= async
function f(){
810 const opHandlers
= Object
.create(null);
811 for(let k
of Object
.keys(state
.opIds
)){
812 const vi
= vfsAsyncImpls
[k
];
814 const o
= Object
.create(null);
815 opHandlers
[state
.opIds
[k
]] = o
;
819 while(!flagAsyncShutdown
){
821 if('timed-out'===Atomics
.wait(
822 state
.sabOPView
, state
.opIds
.whichOp
, 0, state
.asyncIdleWaitTime
824 await
releaseImplicitLocks();
827 const opId
= Atomics
.load(state
.sabOPView
, state
.opIds
.whichOp
);
828 Atomics
.store(state
.sabOPView
, state
.opIds
.whichOp
, 0);
829 const hnd
= opHandlers
[opId
] ?? toss("No waitLoop handler for whichOp #",opId
);
830 const args
= state
.s11n
.deserialize(
831 true /* clear s11n to keep the caller from confusing this with
832 an exception string written by the upcoming
835 //warn("waitLoop() whichOp =",opId, hnd, args);
836 if(hnd
.f
) await hnd
.f(...args
);
837 else error("Missing callback for opId",opId
);
839 error('in waitLoop():',e
);
844 navigator
.storage
.getDirectory().then(function(d
){
846 self
.onmessage = function({data
}){
848 case 'opfs-async-init':{
849 /* Receive shared state from synchronous partner */
850 const opt
= data
.args
;
851 for(const k
in opt
) state
[k
] = opt
[k
];
852 state
.verbose
= opt
.verbose
?? 1;
853 state
.sabOPView
= new Int32Array(state
.sabOP
);
854 state
.sabFileBufView
= new Uint8Array(state
.sabIO
, 0, state
.fileBufferSize
);
855 state
.sabS11nView
= new Uint8Array(state
.sabIO
, state
.sabS11nOffset
, state
.sabS11nSize
);
856 Object
.keys(vfsAsyncImpls
).forEach((k
)=>{
857 if(!Number
.isFinite(state
.opIds
[k
])){
858 toss("Maintenance required: missing state.opIds[",k
,"]");
863 log("init state",state
);
864 wPost('opfs-async-inited');
868 case 'opfs-async-restart':
869 if(flagAsyncShutdown
){
870 warn("Restarting after opfs-async-shutdown. Might or might not work.");
871 flagAsyncShutdown
= false;
875 case 'opfs-async-metrics':
880 wPost('opfs-async-loaded');
881 }).catch((e
)=>error("error initializing OPFS asyncer:",e
));
882 }/*installAsyncProxy()*/;
883 if(!self
.SharedArrayBuffer
){
884 wPost('opfs-unavailable', "Missing SharedArrayBuffer API.",
885 "The server must emit the COOP/COEP response headers to enable that.");
886 }else if(!self
.Atomics
){
887 wPost('opfs-unavailable', "Missing Atomics API.",
888 "The server must emit the COOP/COEP response headers to enable that.");
889 }else if(!self
.FileSystemHandle
||
890 !self
.FileSystemDirectoryHandle
||
891 !self
.FileSystemFileHandle
||
892 !self
.FileSystemFileHandle
.prototype.createSyncAccessHandle
||
893 !navigator
.storage
.getDirectory
){
894 wPost('opfs-unavailable',"Missing required OPFS APIs.");
896 installAsyncProxy(self
);