Snapshot of upstream SQLite 3.41.0
[sqlcipher.git] / ext / wasm / api / sqlite3-opfs-async-proxy.js
blob1456ae08d2f5231e89e926d2b02d6bc98a090fe6
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 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".
49 "use strict";
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.");
60 /**
61 Will hold state copied to this object from the syncronous side of
62 this API.
64 const state = Object.create(null);
66 /**
67 verbose:
69 0 = no logging output
70 1 = only errors
71 2 = warnings and errors
72 3 = debug, warnings, and errors
74 state.verbose = 1;
76 const loggers = {
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);
88 metrics.reset = ()=>{
89 let k;
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);
96 s.count = s.time = 0;
97 s = metrics.s11n.deserialize = Object.create(null);
98 s.count = s.time = 0;
100 metrics.dump = ()=>{
101 let k, n = 0, t = 0, w = 0;
102 for(k in state.opIds){
103 const m = metrics[k];
104 n += m.count;
105 t += m.time;
106 w += m.wait;
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",
111 metrics,
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){
147 const p = new URL(
148 filename, 'file://irrelevant'
149 ).pathname;
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){
164 if(dirName){
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)=>{
182 if(fh.syncHandle){
183 log("Closing sync handle for",fh.filenameAbs);
184 const h = fh.syncHandle;
185 delete fh.syncHandle;
186 delete fh.xLock;
187 __implicitLocks.delete(fh.fid);
188 return h.close();
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
201 removed.
203 const closeSyncHandleNoThrow = async (fh)=>{
204 try{await closeSyncHandle(fh)}
205 catch(e){
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
252 layer.
254 class GetSyncHandleError extends Error {
255 constructor(errorObject, ...msg){
256 super([
257 ...msg, ': '+errorObject.name+':',
258 errorObject.message
259 ].join(' '), {
260 cause: errorObject
262 this.name = 'GetSyncHandleError';
265 GetSyncHandleError.convertRc = (e,rc)=>{
266 if(1){
267 return (
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')))
274 ) ? (
275 /*console.warn("SQLITE_BUSY",e),*/
276 state.sq3Codes.SQLITE_BUSY
277 ) : rc;
278 }else{
279 return rc;
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
292 an I/O error.
294 const getSyncHandle = async (fh,opName)=>{
295 if(!fh.syncHandle){
296 const t = performance.now();
297 log("Acquiring sync handle for",fh.filenameAbs);
298 const maxTries = 6,
299 msBase = state.asyncIdleWaitTime * 2;
300 let i = 1, ms = msBase;
301 for(; true; ms = msBase * ++i){
302 try {
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();
307 break;
308 }catch(e){
309 if(i === maxTries){
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');
322 if(!fh.xLock){
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();
359 __mTimer.op = op;
360 //metrics[op] || toss("Maintenance required: missing metrics for",op);
361 ++metrics[op].count;
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();
371 __wTimer.op = op;
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');
394 metrics.dump();
395 storeAndNotify('opfs-async-metrics', 0);
396 mTimeEnd();
398 'opfs-async-shutdown': async ()=>{
399 flagAsyncShutdown = true;
400 storeAndNotify('opfs-async-shutdown', 0);
402 mkdir: async (dirname)=>{
403 mTimeStart('mkdir');
404 let rc = 0;
405 wTimeStart('mkdir');
406 try {
407 await getDirForFilename(dirname+"/filepart", true);
408 }catch(e){
409 state.s11n.storeException(2,e);
410 rc = state.sq3Codes.SQLITE_IOERR;
411 }finally{
412 wTimeEnd();
414 storeAndNotify('mkdir', rc);
415 mTimeEnd();
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.
430 let rc = 0;
431 wTimeStart('xAccess');
432 try{
433 const [dh, fn] = await getDirForFilename(filename);
434 await dh.getFileHandle(fn);
435 }catch(e){
436 state.s11n.storeException(2,e);
437 rc = state.sq3Codes.SQLITE_IOERR;
438 }finally{
439 wTimeEnd();
441 storeAndNotify('xAccess', rc);
442 mTimeEnd();
444 xClose: async function(fid/*sqlite3_file pointer*/){
445 const opName = 'xClose';
446 mTimeStart(opName);
447 __implicitLocks.delete(fid);
448 const fh = __openFiles[fid];
449 let rc = 0;
450 wTimeStart(opName);
451 if(fh){
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) }
458 }else{
459 state.s11n.serialize();
460 rc = state.sq3Codes.SQLITE_NOTFOUND;
462 wTimeEnd();
463 storeAndNotify(opName, rc);
464 mTimeEnd();
466 xDelete: async function(...args){
467 mTimeStart('xDelete');
468 const rc = await vfsAsyncImpls.xDeleteNoWait(...args);
469 storeAndNotify('xDelete', rc);
470 mTimeEnd();
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
483 is false.
485 let rc = 0;
486 wTimeStart('xDelete');
487 try {
488 while(filename){
489 const [hDir, filenamePart] = await getDirForFilename(filename, false);
490 if(!filenamePart) break;
491 await hDir.removeEntry(filenamePart, {recursive});
492 if(0x1234 !== syncDir) break;
493 recursive = false;
494 filename = getResolvedPath(filename, true);
495 filename.pop();
496 filename = filename.join('/');
498 }catch(e){
499 state.s11n.storeException(2,e);
500 rc = state.sq3Codes.SQLITE_IOERR_DELETE;
502 wTimeEnd();
503 return rc;
505 xFileSize: async function(fid/*sqlite3_file pointer*/){
506 mTimeStart('xFileSize');
507 const fh = __openFiles[fid];
508 let rc = 0;
509 wTimeStart('xFileSize');
510 try{
511 const sz = await (await getSyncHandle(fh,'xFileSize')).getSize();
512 state.s11n.serialize(Number(sz));
513 }catch(e){
514 state.s11n.storeException(1,e);
515 rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR);
517 await releaseImplicitLock(fh);
518 wTimeEnd();
519 storeAndNotify('xFileSize', rc);
520 mTimeEnd();
522 xLock: async function(fid/*sqlite3_file pointer*/,
523 lockType/*SQLITE_LOCK_...*/){
524 mTimeStart('xLock');
525 const fh = __openFiles[fid];
526 let rc = 0;
527 const oldLockType = fh.xLock;
528 fh.xLock = lockType;
529 if( !fh.syncHandle ){
530 wTimeStart('xLock');
531 try {
532 await getSyncHandle(fh,'xLock');
533 __implicitLocks.delete(fid);
534 }catch(e){
535 state.s11n.storeException(1,e);
536 rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_LOCK);
537 fh.xLock = oldLockType;
539 wTimeEnd();
541 storeAndNotify('xLock',rc);
542 mTimeEnd();
544 xOpen: async function(fid/*sqlite3_file pointer*/, filename,
545 flags/*SQLITE_OPEN_...*/,
546 opfsFlags/*OPFS_...*/){
547 const opName = 'xOpen';
548 mTimeStart(opName);
549 const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags);
550 wTimeStart('xOpen');
551 try{
552 let hDir, filenamePart;
553 try {
554 [hDir, filenamePart] = await getDirForFilename(filename, !!create);
555 }catch(e){
556 state.s11n.storeException(1,e);
557 storeAndNotify(opName, state.sq3Codes.SQLITE_NOTFOUND);
558 mTimeEnd();
559 wTimeEnd();
560 return;
562 const hFile = await hDir.getFileHandle(filenamePart, {create});
563 wTimeEnd();
564 const fh = Object.assign(Object.create(null),{
565 fid: fid,
566 filenameAbs: filename,
567 filenamePart: filenamePart,
568 dirHandle: hDir,
569 fileHandle: hFile,
570 sabView: state.sabFileBufView,
571 readOnly: create
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
581 mode. */
582 && (0===(flags & state.sq3Codes.SQLITE_OPEN_MAIN_DB))){
583 /* sqlite does not lock these files, so go ahead and grab an OPFS
584 lock. */
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
588 if needed. */;
589 await getSyncHandle(fh,'xOpen');
591 __openFiles[fid] = fh;
592 storeAndNotify(opName, 0);
593 }catch(e){
594 wTimeEnd();
595 error(opName,e);
596 state.s11n.storeException(1,e);
597 storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
599 mTimeEnd();
601 xRead: async function(fid/*sqlite3_file pointer*/,n,offset64){
602 mTimeStart('xRead');
603 let rc = 0, nRead;
604 const fh = __openFiles[fid];
605 try{
606 wTimeStart('xRead');
607 nRead = (await getSyncHandle(fh,'xRead')).read(
608 fh.sabView.subarray(0, n),
609 {at: Number(offset64)}
611 wTimeEnd();
612 if(nRead < n){/* Zero-fill remaining bytes */
613 fh.sabView.fill(0, nRead, n);
614 rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
616 }catch(e){
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);
624 mTimeEnd();
626 xSync: async function(fid/*sqlite3_file pointer*/,flags/*ignored*/){
627 mTimeStart('xSync');
628 const fh = __openFiles[fid];
629 let rc = 0;
630 if(!fh.readOnly && fh.syncHandle){
631 try {
632 wTimeStart('xSync');
633 await fh.syncHandle.flush();
634 }catch(e){
635 state.s11n.storeException(2,e);
636 rc = state.sq3Codes.SQLITE_IOERR_FSYNC;
638 wTimeEnd();
640 storeAndNotify('xSync',rc);
641 mTimeEnd();
643 xTruncate: async function(fid/*sqlite3_file pointer*/,size){
644 mTimeStart('xTruncate');
645 let rc = 0;
646 const fh = __openFiles[fid];
647 wTimeStart('xTruncate');
648 try{
649 affirmNotRO('xTruncate', fh);
650 await (await getSyncHandle(fh,'xTruncate')).truncate(size);
651 }catch(e){
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);
657 wTimeEnd();
658 storeAndNotify('xTruncate',rc);
659 mTimeEnd();
661 xUnlock: async function(fid/*sqlite3_file pointer*/,
662 lockType/*SQLITE_LOCK_...*/){
663 mTimeStart('xUnlock');
664 let rc = 0;
665 const fh = __openFiles[fid];
666 if( state.sq3Codes.SQLITE_LOCK_NONE===lockType
667 && fh.syncHandle ){
668 wTimeStart('xUnlock');
669 try { await closeSyncHandle(fh) }
670 catch(e){
671 state.s11n.storeException(1,e);
672 rc = state.sq3Codes.SQLITE_IOERR_UNLOCK;
674 wTimeEnd();
676 storeAndNotify('xUnlock',rc);
677 mTimeEnd();
679 xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){
680 mTimeStart('xWrite');
681 let rc;
682 const fh = __openFiles[fid];
683 wTimeStart('xWrite');
684 try{
685 affirmNotRO('xWrite', fh);
686 rc = (
687 n === (await getSyncHandle(fh,'xWrite'))
688 .write(fh.sabView.subarray(0, n),
689 {at: Number(offset64)})
690 ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
691 }catch(e){
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);
697 wTimeEnd();
698 storeAndNotify('xWrite',rc);
699 mTimeEnd();
701 }/*vfsAsyncImpls*/;
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)=>(
720 TypeIds[typeof v]
721 || toss("Maintenance required: this value type cannot be serialized.",v)
723 const getTypeIdById = (tid)=>{
724 switch(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;
737 if(argc){
738 const typeIds = [];
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];
745 if(t.getter){
746 v = viewDV[t.getter](offset, state.littleEndian);
747 offset += t.size;
748 }else{/*String*/
749 n = viewDV.getInt32(offset, state.littleEndian);
750 offset += 4;
751 v = textDecoder.decode(viewU8.slice(offset, offset+n));
752 offset += n;
754 rc.push(v);
757 if(clear) viewU8[0] = 0;
758 //log("deserialize:",argc, rc);
759 metrics.s11n.deserialize.time += performance.now() - t;
760 return rc;
762 state.s11n.serialize = function(...args){
763 const t = performance.now();
764 ++metrics.s11n.serialize.count;
765 if(args.length){
766 //log("serialize():",args);
767 const typeIds = [];
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
772 bytes. */
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];
780 if(t.setter){
781 viewDV[t.setter](offset, args[i], state.littleEndian);
782 offset += t.size;
783 }else{/*String*/
784 const s = textEncoder.encode(args[i]);
785 viewDV.setInt32(offset, s.byteLength, state.littleEndian);
786 offset += 4;
787 viewU8.set(s, offset);
788 offset += s.byteLength;
791 //log("serialize() result:",viewU8.slice(0,offset));
792 }else{
793 viewU8[0] = 0;
795 metrics.s11n.serialize.time += performance.now() - t;
798 state.s11n.storeException = state.asyncS11nExceptions
799 ? ((priority,e)=>{
800 if(priority<=state.asyncS11nExceptions){
801 state.s11n.serialize([e.name,': ',e.message].join(""));
804 : ()=>{};
806 return state.s11n;
807 }/*initS11n()*/;
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];
813 if(!vi) continue;
814 const o = Object.create(null);
815 opHandlers[state.opIds[k]] = o;
816 o.key = k;
817 o.f = vi;
819 while(!flagAsyncShutdown){
820 try {
821 if('timed-out'===Atomics.wait(
822 state.sabOPView, state.opIds.whichOp, 0, state.asyncIdleWaitTime
824 await releaseImplicitLocks();
825 continue;
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
833 operation */
834 ) || [];
835 //warn("waitLoop() whichOp =",opId, hnd, args);
836 if(hnd.f) await hnd.f(...args);
837 else error("Missing callback for opId",opId);
838 }catch(e){
839 error('in waitLoop():',e);
844 navigator.storage.getDirectory().then(function(d){
845 state.rootDir = d;
846 self.onmessage = function({data}){
847 switch(data.type){
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,"]");
861 initS11n();
862 metrics.reset();
863 log("init state",state);
864 wPost('opfs-async-inited');
865 waitLoop();
866 break;
868 case 'opfs-async-restart':
869 if(flagAsyncShutdown){
870 warn("Restarting after opfs-async-shutdown. Might or might not work.");
871 flagAsyncShutdown = false;
872 waitLoop();
874 break;
875 case 'opfs-async-metrics':
876 metrics.dump();
877 break;
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.");
895 }else{
896 installAsyncProxy(self);