1cd0df83cSstephan/* 2cd0df83cSstephan 2022-09-16 3cd0df83cSstephan 4cd0df83cSstephan The author disclaims copyright to this source code. In place of a 5cd0df83cSstephan legal notice, here is a blessing: 6cd0df83cSstephan 7cd0df83cSstephan * May you do good and not evil. 8cd0df83cSstephan * May you find forgiveness for yourself and forgive others. 9cd0df83cSstephan * May you share freely, never taking more than you give. 10cd0df83cSstephan 11cd0df83cSstephan *********************************************************************** 12cd0df83cSstephan 13f861b36bSstephan A Worker which manages asynchronous OPFS handles on behalf of a 14cd0df83cSstephan synchronous API which controls it via a combination of Worker 15cd0df83cSstephan messages, SharedArrayBuffer, and Atomics. It is the asynchronous 16cd0df83cSstephan counterpart of the API defined in sqlite3-api-opfs.js. 17cd0df83cSstephan 18cd0df83cSstephan Highly indebted to: 19cd0df83cSstephan 20cd0df83cSstephan https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/OriginPrivateFileSystemVFS.js 21cd0df83cSstephan 22cd0df83cSstephan for demonstrating how to use the OPFS APIs. 23cd0df83cSstephan 24cd0df83cSstephan This file is to be loaded as a Worker. It does not have any direct 25cd0df83cSstephan access to the sqlite3 JS/WASM bits, so any bits which it needs (most 26cd0df83cSstephan notably SQLITE_xxx integer codes) have to be imported into it via an 27cd0df83cSstephan initialization process. 28f861b36bSstephan 29f861b36bSstephan This file represents an implementation detail of a larger piece of 30f861b36bSstephan code, and not a public interface. Its details may change at any time 31f861b36bSstephan and are not intended to be used by any client-level code. 32cd0df83cSstephan*/ 33f861b36bSstephan"use strict"; 34cd0df83cSstephanconst toss = function(...args){throw new Error(args.join(' '))}; 35cd0df83cSstephanif(self.window === self){ 36cd0df83cSstephan toss("This code cannot run from the main thread.", 37cd0df83cSstephan "Load it as a Worker from a separate Worker."); 38cd0df83cSstephan}else if(!navigator.storage.getDirectory){ 39cd0df83cSstephan toss("This API requires navigator.storage.getDirectory."); 40cd0df83cSstephan} 41cd0df83cSstephan 42cd0df83cSstephan/** 43cd0df83cSstephan Will hold state copied to this object from the syncronous side of 44cd0df83cSstephan this API. 45cd0df83cSstephan*/ 46cd0df83cSstephanconst state = Object.create(null); 47*da264159Sstephan 48cd0df83cSstephan/** 49cd0df83cSstephan verbose: 50cd0df83cSstephan 51cd0df83cSstephan 0 = no logging output 52cd0df83cSstephan 1 = only errors 53cd0df83cSstephan 2 = warnings and errors 54cd0df83cSstephan 3 = debug, warnings, and errors 55cd0df83cSstephan*/ 56cd0df83cSstephanstate.verbose = 2; 57cd0df83cSstephan 58cd0df83cSstephanconst loggers = { 59cd0df83cSstephan 0:console.error.bind(console), 60cd0df83cSstephan 1:console.warn.bind(console), 61cd0df83cSstephan 2:console.log.bind(console) 62cd0df83cSstephan}; 63cd0df83cSstephanconst logImpl = (level,...args)=>{ 64cd0df83cSstephan if(state.verbose>level) loggers[level]("OPFS asyncer:",...args); 65cd0df83cSstephan}; 66cd0df83cSstephanconst log = (...args)=>logImpl(2, ...args); 67cd0df83cSstephanconst warn = (...args)=>logImpl(1, ...args); 68cd0df83cSstephanconst error = (...args)=>logImpl(0, ...args); 69cd0df83cSstephanconst metrics = Object.create(null); 70cd0df83cSstephanmetrics.reset = ()=>{ 71cd0df83cSstephan let k; 72cd0df83cSstephan const r = (m)=>(m.count = m.time = m.wait = 0); 73cd0df83cSstephan for(k in state.opIds){ 74cd0df83cSstephan r(metrics[k] = Object.create(null)); 75cd0df83cSstephan } 76cd0df83cSstephan let s = metrics.s11n = Object.create(null); 77cd0df83cSstephan s = s.serialize = Object.create(null); 78cd0df83cSstephan s.count = s.time = 0; 79cd0df83cSstephan s = metrics.s11n.deserialize = Object.create(null); 80cd0df83cSstephan s.count = s.time = 0; 81cd0df83cSstephan}; 82cd0df83cSstephanmetrics.dump = ()=>{ 83cd0df83cSstephan let k, n = 0, t = 0, w = 0; 84cd0df83cSstephan for(k in state.opIds){ 85cd0df83cSstephan const m = metrics[k]; 86cd0df83cSstephan n += m.count; 87cd0df83cSstephan t += m.time; 88cd0df83cSstephan w += m.wait; 89cd0df83cSstephan m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0; 90cd0df83cSstephan } 91cd0df83cSstephan console.log(self.location.href, 92cd0df83cSstephan "metrics for",self.location.href,":\n", 93cd0df83cSstephan metrics, 94cd0df83cSstephan "\nTotal of",n,"op(s) for",t,"ms", 95cd0df83cSstephan "approx",w,"ms spent waiting on OPFS APIs."); 96cd0df83cSstephan console.log("Serialization metrics:",metrics.s11n); 97cd0df83cSstephan}; 98cd0df83cSstephan 99cd0df83cSstephan/** 100*da264159Sstephan __openFiles is a map of sqlite3_file pointers (integers) to 101*da264159Sstephan metadata related to a given OPFS file handles. The pointers are, in 102*da264159Sstephan this side of the interface, opaque file handle IDs provided by the 103*da264159Sstephan synchronous part of this constellation. Each value is an object 104*da264159Sstephan with a structure demonstrated in the xOpen() impl. 105cd0df83cSstephan*/ 106cd0df83cSstephanconst __openFiles = Object.create(null); 107*da264159Sstephan/** 108*da264159Sstephan __autoLocks is a Set of sqlite3_file pointers (integers) which were 109*da264159Sstephan "auto-locked". i.e. those for which we obtained a sync access 110*da264159Sstephan handle without an explicit xLock() call. Such locks will be 111*da264159Sstephan released during db connection idle time, whereas a sync access 112*da264159Sstephan handle obtained via xLock(), or subsequently xLock()'d after 113*da264159Sstephan auto-acquisition, will not be released until xUnlock() is called. 114*da264159Sstephan 115*da264159Sstephan Maintenance reminder: if we relinquish auto-locks at the end of the 116*da264159Sstephan operation which acquires them, we pay a massive performance 117*da264159Sstephan penalty: speedtest1 benchmarks take up to 4x as long. By delaying 118*da264159Sstephan the lock release until idle time, the hit is negligible. 119*da264159Sstephan*/ 120*da264159Sstephanconst __autoLocks = new Set(); 121cd0df83cSstephan 122cd0df83cSstephan/** 123cd0df83cSstephan Expects an OPFS file path. It gets resolved, such that ".." 124cd0df83cSstephan components are properly expanded, and returned. If the 2nd arg is 125cd0df83cSstephan true, the result is returned as an array of path elements, else an 126cd0df83cSstephan absolute path string is returned. 127cd0df83cSstephan*/ 128cd0df83cSstephanconst getResolvedPath = function(filename,splitIt){ 129cd0df83cSstephan const p = new URL( 130cd0df83cSstephan filename, 'file://irrelevant' 131cd0df83cSstephan ).pathname; 132cd0df83cSstephan return splitIt ? p.split('/').filter((v)=>!!v) : p; 133cd0df83cSstephan}; 134cd0df83cSstephan 135cd0df83cSstephan/** 136cd0df83cSstephan Takes the absolute path to a filesystem element. Returns an array 137cd0df83cSstephan of [handleOfContainingDir, filename]. If the 2nd argument is truthy 138cd0df83cSstephan then each directory element leading to the file is created along 139cd0df83cSstephan the way. Throws if any creation or resolution fails. 140cd0df83cSstephan*/ 141cd0df83cSstephanconst getDirForFilename = async function f(absFilename, createDirs = false){ 142cd0df83cSstephan const path = getResolvedPath(absFilename, true); 143cd0df83cSstephan const filename = path.pop(); 144cd0df83cSstephan let dh = state.rootDir; 145cd0df83cSstephan for(const dirName of path){ 146cd0df83cSstephan if(dirName){ 147cd0df83cSstephan dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs}); 148cd0df83cSstephan } 149cd0df83cSstephan } 150cd0df83cSstephan return [dh, filename]; 151cd0df83cSstephan}; 152cd0df83cSstephan 153cd0df83cSstephan/** 15443b442a6Sstephan An error class specifically for use with getSyncHandle(), the goal 15543b442a6Sstephan of which is to eventually be able to distinguish unambiguously 15643b442a6Sstephan between locking-related failures and other types, noting that we 15743b442a6Sstephan cannot currently do so because createSyncAccessHandle() does not 15843b442a6Sstephan define its exceptions in the required level of detail. 15943b442a6Sstephan*/ 16043b442a6Sstephanclass GetSyncHandleError extends Error { 16143b442a6Sstephan constructor(errorObject, ...msg){ 16243b442a6Sstephan super(); 16343b442a6Sstephan this.error = errorObject; 16443b442a6Sstephan this.message = [ 16543b442a6Sstephan ...msg, ': Original exception ['+errorObject.name+']:', 16643b442a6Sstephan errorObject.message 16743b442a6Sstephan ].join(' '); 16843b442a6Sstephan this.name = 'GetSyncHandleError'; 16943b442a6Sstephan } 17043b442a6Sstephan}; 17143b442a6Sstephan 17243b442a6Sstephan/** 173cd0df83cSstephan Returns the sync access handle associated with the given file 174f861b36bSstephan handle object (which must be a valid handle object, as created by 175f861b36bSstephan xOpen()), lazily opening it if needed. 176cd0df83cSstephan 177cd0df83cSstephan In order to help alleviate cross-tab contention for a dabase, 178cd0df83cSstephan if an exception is thrown while acquiring the handle, this routine 179cd0df83cSstephan will wait briefly and try again, up to 3 times. If acquisition 180cd0df83cSstephan still fails at that point it will give up and propagate the 181cd0df83cSstephan exception. 182cd0df83cSstephan*/ 183cd0df83cSstephanconst getSyncHandle = async (fh)=>{ 184cd0df83cSstephan if(!fh.syncHandle){ 185cd0df83cSstephan const t = performance.now(); 186cd0df83cSstephan log("Acquiring sync handle for",fh.filenameAbs); 1879163ef1fSstephan const maxTries = 4, msBase = 300; 1889163ef1fSstephan let i = 1, ms = msBase; 1899163ef1fSstephan for(; true; ms = msBase * ++i){ 190cd0df83cSstephan try { 19143b442a6Sstephan //if(i<3) toss("Just testing getSyncHandle() wait-and-retry."); 192cd0df83cSstephan //TODO? A config option which tells it to throw here 193cd0df83cSstephan //randomly every now and then, for testing purposes. 194cd0df83cSstephan fh.syncHandle = await fh.fileHandle.createSyncAccessHandle(); 195cd0df83cSstephan break; 196cd0df83cSstephan }catch(e){ 197cd0df83cSstephan if(i === maxTries){ 19843b442a6Sstephan throw new GetSyncHandleError( 19943b442a6Sstephan e, "Error getting sync handle.",maxTries, 20043b442a6Sstephan "attempts failed.",fh.filenameAbs 20143b442a6Sstephan ); 202cd0df83cSstephan } 203cd0df83cSstephan warn("Error getting sync handle. Waiting",ms, 204cd0df83cSstephan "ms and trying again.",fh.filenameAbs,e); 205f861b36bSstephan Atomics.wait(state.sabOPView, state.opIds.retry, 0, ms); 206cd0df83cSstephan } 207cd0df83cSstephan } 208cd0df83cSstephan log("Got sync handle for",fh.filenameAbs,'in',performance.now() - t,'ms'); 209*da264159Sstephan if(!fh.xLock){ 210*da264159Sstephan __autoLocks.add(fh.fid); 211*da264159Sstephan log("Auto-locked",fh.fid,fh.filenameAbs); 212*da264159Sstephan } 213cd0df83cSstephan } 214cd0df83cSstephan return fh.syncHandle; 215cd0df83cSstephan}; 216cd0df83cSstephan 217cd0df83cSstephan/** 218cd0df83cSstephan If the given file-holding object has a sync handle attached to it, 219cd0df83cSstephan that handle is remove and asynchronously closed. Though it may 220cd0df83cSstephan sound sensible to continue work as soon as the close() returns 221cd0df83cSstephan (noting that it's asynchronous), doing so can cause operations 222cd0df83cSstephan performed soon afterwards, e.g. a call to getSyncHandle() to fail 223cd0df83cSstephan because they may happen out of order from the close(). OPFS does 224cd0df83cSstephan not guaranty that the actual order of operations is retained in 225cd0df83cSstephan such cases. i.e. always "await" on the result of this function. 226cd0df83cSstephan*/ 227cd0df83cSstephanconst closeSyncHandle = async (fh)=>{ 228cd0df83cSstephan if(fh.syncHandle){ 229cd0df83cSstephan log("Closing sync handle for",fh.filenameAbs); 230cd0df83cSstephan const h = fh.syncHandle; 231cd0df83cSstephan delete fh.syncHandle; 232*da264159Sstephan delete fh.xLock; 233*da264159Sstephan __autoLocks.delete(fh.fid); 234cd0df83cSstephan return h.close(); 235cd0df83cSstephan } 236cd0df83cSstephan}; 237cd0df83cSstephan 238cd0df83cSstephan/** 239aafa022fSstephan A proxy for closeSyncHandle() which is guaranteed to not throw. 240aafa022fSstephan 241aafa022fSstephan This function is part of a lock/unlock step in functions which 242aafa022fSstephan require a sync access handle but may be called without xLock() 243aafa022fSstephan having been called first. Such calls need to release that 244aafa022fSstephan handle to avoid locking the file for all of time. This is an 245aafa022fSstephan _attempt_ at reducing cross-tab contention but it may prove 246aafa022fSstephan to be more of a problem than a solution and may need to be 247aafa022fSstephan removed. 248aafa022fSstephan*/ 249aafa022fSstephanconst closeSyncHandleNoThrow = async (fh)=>{ 250aafa022fSstephan try{await closeSyncHandle(fh)} 251aafa022fSstephan catch(e){ 252aafa022fSstephan warn("closeSyncHandleNoThrow() ignoring:",e,fh); 253aafa022fSstephan } 254aafa022fSstephan}; 255aafa022fSstephan 256aafa022fSstephan/** 257cd0df83cSstephan Stores the given value at state.sabOPView[state.opIds.rc] and then 258cd0df83cSstephan Atomics.notify()'s it. 259cd0df83cSstephan*/ 260cd0df83cSstephanconst storeAndNotify = (opName, value)=>{ 26143b442a6Sstephan log(opName+"() => notify(",value,")"); 262cd0df83cSstephan Atomics.store(state.sabOPView, state.opIds.rc, value); 263cd0df83cSstephan Atomics.notify(state.sabOPView, state.opIds.rc); 264cd0df83cSstephan}; 265cd0df83cSstephan 266cd0df83cSstephan/** 267cd0df83cSstephan Throws if fh is a file-holding object which is flagged as read-only. 268cd0df83cSstephan*/ 269cd0df83cSstephanconst affirmNotRO = function(opName,fh){ 270cd0df83cSstephan if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs); 271cd0df83cSstephan}; 272f45c3370Sstephanconst affirmLocked = function(opName,fh){ 273f45c3370Sstephan //if(!fh.syncHandle) toss(opName+"(): File does not have a lock: "+fh.filenameAbs); 274f45c3370Sstephan /** 275f45c3370Sstephan Currently a no-op, as speedtest1 triggers xRead() without a 276f45c3370Sstephan lock (that seems like a bug but it's currently uninvestigated). 277f45c3370Sstephan This means, however, that some OPFS VFS routines may trigger 278f45c3370Sstephan acquisition of a lock but never let it go until xUnlock() is 279f45c3370Sstephan called (which it likely won't be if xLock() was not called). 280f45c3370Sstephan */ 281f45c3370Sstephan}; 282cd0df83cSstephan 283cd0df83cSstephan/** 284cd0df83cSstephan We track 2 different timers: the "metrics" timer records how much 285cd0df83cSstephan time we spend performing work. The "wait" timer records how much 286cd0df83cSstephan time we spend waiting on the underlying OPFS timer. See the calls 287cd0df83cSstephan to mTimeStart(), mTimeEnd(), wTimeStart(), and wTimeEnd() 288cd0df83cSstephan throughout this file to see how they're used. 289cd0df83cSstephan*/ 290cd0df83cSstephanconst __mTimer = Object.create(null); 291cd0df83cSstephan__mTimer.op = undefined; 292cd0df83cSstephan__mTimer.start = undefined; 293cd0df83cSstephanconst mTimeStart = (op)=>{ 294cd0df83cSstephan __mTimer.start = performance.now(); 295cd0df83cSstephan __mTimer.op = op; 296cd0df83cSstephan //metrics[op] || toss("Maintenance required: missing metrics for",op); 297cd0df83cSstephan ++metrics[op].count; 298cd0df83cSstephan}; 299cd0df83cSstephanconst mTimeEnd = ()=>( 300cd0df83cSstephan metrics[__mTimer.op].time += performance.now() - __mTimer.start 301cd0df83cSstephan); 302cd0df83cSstephanconst __wTimer = Object.create(null); 303cd0df83cSstephan__wTimer.op = undefined; 304cd0df83cSstephan__wTimer.start = undefined; 305cd0df83cSstephanconst wTimeStart = (op)=>{ 306cd0df83cSstephan __wTimer.start = performance.now(); 307cd0df83cSstephan __wTimer.op = op; 308cd0df83cSstephan //metrics[op] || toss("Maintenance required: missing metrics for",op); 309cd0df83cSstephan}; 310cd0df83cSstephanconst wTimeEnd = ()=>( 311cd0df83cSstephan metrics[__wTimer.op].wait += performance.now() - __wTimer.start 312cd0df83cSstephan); 313cd0df83cSstephan 314cd0df83cSstephan/** 315cd0df83cSstephan Gets set to true by the 'opfs-async-shutdown' command to quit the 316cd0df83cSstephan wait loop. This is only intended for debugging purposes: we cannot 317cd0df83cSstephan inspect this file's state while the tight waitLoop() is running and 318cd0df83cSstephan need a way to stop that loop for introspection purposes. 319cd0df83cSstephan*/ 320cd0df83cSstephanlet flagAsyncShutdown = false; 321cd0df83cSstephan 322cd0df83cSstephan 323cd0df83cSstephan/** 324cd0df83cSstephan Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods 325cd0df83cSstephan methods, as well as helpers like mkdir(). Maintenance reminder: 326cd0df83cSstephan members are in alphabetical order to simplify finding them. 327cd0df83cSstephan*/ 328cd0df83cSstephanconst vfsAsyncImpls = { 329cd0df83cSstephan 'opfs-async-metrics': async ()=>{ 330cd0df83cSstephan mTimeStart('opfs-async-metrics'); 331cd0df83cSstephan metrics.dump(); 332cd0df83cSstephan storeAndNotify('opfs-async-metrics', 0); 333cd0df83cSstephan mTimeEnd(); 334cd0df83cSstephan }, 335cd0df83cSstephan 'opfs-async-shutdown': async ()=>{ 336cd0df83cSstephan flagAsyncShutdown = true; 337cd0df83cSstephan storeAndNotify('opfs-async-shutdown', 0); 338cd0df83cSstephan }, 339cd0df83cSstephan mkdir: async (dirname)=>{ 340cd0df83cSstephan mTimeStart('mkdir'); 341cd0df83cSstephan let rc = 0; 342cd0df83cSstephan wTimeStart('mkdir'); 343cd0df83cSstephan try { 344cd0df83cSstephan await getDirForFilename(dirname+"/filepart", true); 345cd0df83cSstephan }catch(e){ 346cd0df83cSstephan state.s11n.storeException(2,e); 347cd0df83cSstephan rc = state.sq3Codes.SQLITE_IOERR; 348cd0df83cSstephan }finally{ 349cd0df83cSstephan wTimeEnd(); 350cd0df83cSstephan } 351cd0df83cSstephan storeAndNotify('mkdir', rc); 352cd0df83cSstephan mTimeEnd(); 353cd0df83cSstephan }, 354cd0df83cSstephan xAccess: async (filename)=>{ 355cd0df83cSstephan mTimeStart('xAccess'); 356cd0df83cSstephan /* OPFS cannot support the full range of xAccess() queries sqlite3 357cd0df83cSstephan calls for. We can essentially just tell if the file is 358cd0df83cSstephan accessible, but if it is it's automatically writable (unless 359cd0df83cSstephan it's locked, which we cannot(?) know without trying to open 360cd0df83cSstephan it). OPFS does not have the notion of read-only. 361cd0df83cSstephan 362cd0df83cSstephan The return semantics of this function differ from sqlite3's 363cd0df83cSstephan xAccess semantics because we are limited in what we can 364cd0df83cSstephan communicate back to our synchronous communication partner: 0 = 365cd0df83cSstephan accessible, non-0 means not accessible. 366cd0df83cSstephan */ 367cd0df83cSstephan let rc = 0; 368cd0df83cSstephan wTimeStart('xAccess'); 369cd0df83cSstephan try{ 370cd0df83cSstephan const [dh, fn] = await getDirForFilename(filename); 371cd0df83cSstephan await dh.getFileHandle(fn); 372cd0df83cSstephan }catch(e){ 373cd0df83cSstephan state.s11n.storeException(2,e); 374cd0df83cSstephan rc = state.sq3Codes.SQLITE_IOERR; 375cd0df83cSstephan }finally{ 376cd0df83cSstephan wTimeEnd(); 377cd0df83cSstephan } 378cd0df83cSstephan storeAndNotify('xAccess', rc); 379cd0df83cSstephan mTimeEnd(); 380cd0df83cSstephan }, 381f861b36bSstephan xClose: async function(fid/*sqlite3_file pointer*/){ 382cd0df83cSstephan const opName = 'xClose'; 383cd0df83cSstephan mTimeStart(opName); 384*da264159Sstephan __autoLocks.delete(fid); 385cd0df83cSstephan const fh = __openFiles[fid]; 386cd0df83cSstephan let rc = 0; 387*da264159Sstephan wTimeStart(opName); 388cd0df83cSstephan if(fh){ 389cd0df83cSstephan delete __openFiles[fid]; 390cd0df83cSstephan await closeSyncHandle(fh); 391cd0df83cSstephan if(fh.deleteOnClose){ 392cd0df83cSstephan try{ await fh.dirHandle.removeEntry(fh.filenamePart) } 393cd0df83cSstephan catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) } 394cd0df83cSstephan } 395cd0df83cSstephan }else{ 396cd0df83cSstephan state.s11n.serialize(); 397cd0df83cSstephan rc = state.sq3Codes.SQLITE_NOTFOUND; 398cd0df83cSstephan } 399cd0df83cSstephan wTimeEnd(); 400cd0df83cSstephan storeAndNotify(opName, rc); 401cd0df83cSstephan mTimeEnd(); 402cd0df83cSstephan }, 403cd0df83cSstephan xDelete: async function(...args){ 404cd0df83cSstephan mTimeStart('xDelete'); 405cd0df83cSstephan const rc = await vfsAsyncImpls.xDeleteNoWait(...args); 406cd0df83cSstephan storeAndNotify('xDelete', rc); 407cd0df83cSstephan mTimeEnd(); 408cd0df83cSstephan }, 409cd0df83cSstephan xDeleteNoWait: async function(filename, syncDir = 0, recursive = false){ 410cd0df83cSstephan /* The syncDir flag is, for purposes of the VFS API's semantics, 411cd0df83cSstephan ignored here. However, if it has the value 0x1234 then: after 412cd0df83cSstephan deleting the given file, recursively try to delete any empty 413cd0df83cSstephan directories left behind in its wake (ignoring any errors and 414cd0df83cSstephan stopping at the first failure). 415cd0df83cSstephan 416cd0df83cSstephan That said: we don't know for sure that removeEntry() fails if 417cd0df83cSstephan the dir is not empty because the API is not documented. It has, 418cd0df83cSstephan however, a "recursive" flag which defaults to false, so 419cd0df83cSstephan presumably it will fail if the dir is not empty and that flag 420cd0df83cSstephan is false. 421cd0df83cSstephan */ 422cd0df83cSstephan let rc = 0; 423cd0df83cSstephan wTimeStart('xDelete'); 424cd0df83cSstephan try { 425cd0df83cSstephan while(filename){ 426cd0df83cSstephan const [hDir, filenamePart] = await getDirForFilename(filename, false); 427cd0df83cSstephan if(!filenamePart) break; 428cd0df83cSstephan await hDir.removeEntry(filenamePart, {recursive}); 429cd0df83cSstephan if(0x1234 !== syncDir) break; 43049048b14Sstephan recursive = false; 431cd0df83cSstephan filename = getResolvedPath(filename, true); 432cd0df83cSstephan filename.pop(); 433cd0df83cSstephan filename = filename.join('/'); 434cd0df83cSstephan } 435cd0df83cSstephan }catch(e){ 436cd0df83cSstephan state.s11n.storeException(2,e); 437cd0df83cSstephan rc = state.sq3Codes.SQLITE_IOERR_DELETE; 438cd0df83cSstephan } 439cd0df83cSstephan wTimeEnd(); 440cd0df83cSstephan return rc; 441cd0df83cSstephan }, 442f861b36bSstephan xFileSize: async function(fid/*sqlite3_file pointer*/){ 443cd0df83cSstephan mTimeStart('xFileSize'); 444cd0df83cSstephan const fh = __openFiles[fid]; 44543b442a6Sstephan let rc; 446cd0df83cSstephan wTimeStart('xFileSize'); 447cd0df83cSstephan try{ 448f45c3370Sstephan affirmLocked('xFileSize',fh); 44943b442a6Sstephan rc = await (await getSyncHandle(fh)).getSize(); 45043b442a6Sstephan state.s11n.serialize(Number(rc)); 45143b442a6Sstephan rc = 0; 452cd0df83cSstephan }catch(e){ 453cd0df83cSstephan state.s11n.storeException(2,e); 45443b442a6Sstephan rc = state.sq3Codes.SQLITE_IOERR; 455cd0df83cSstephan } 456cd0df83cSstephan wTimeEnd(); 45743b442a6Sstephan storeAndNotify('xFileSize', rc); 458cd0df83cSstephan mTimeEnd(); 459cd0df83cSstephan }, 460f861b36bSstephan xLock: async function(fid/*sqlite3_file pointer*/, 461f861b36bSstephan lockType/*SQLITE_LOCK_...*/){ 462cd0df83cSstephan mTimeStart('xLock'); 463cd0df83cSstephan const fh = __openFiles[fid]; 464cd0df83cSstephan let rc = 0; 465*da264159Sstephan const oldLockType = fh.xLock; 466*da264159Sstephan fh.xLock = lockType; 467cd0df83cSstephan if( !fh.syncHandle ){ 468cd0df83cSstephan wTimeStart('xLock'); 469*da264159Sstephan try { 470*da264159Sstephan await getSyncHandle(fh); 471*da264159Sstephan __autoLocks.delete(fid); 472*da264159Sstephan }catch(e){ 473cd0df83cSstephan state.s11n.storeException(1,e); 47443b442a6Sstephan rc = state.sq3Codes.SQLITE_IOERR_LOCK; 475*da264159Sstephan fh.xLock = oldLockType; 476cd0df83cSstephan } 477cd0df83cSstephan wTimeEnd(); 478cd0df83cSstephan } 479cd0df83cSstephan storeAndNotify('xLock',rc); 480cd0df83cSstephan mTimeEnd(); 481cd0df83cSstephan }, 482f861b36bSstephan xOpen: async function(fid/*sqlite3_file pointer*/, filename, 483f861b36bSstephan flags/*SQLITE_OPEN_...*/){ 484cd0df83cSstephan const opName = 'xOpen'; 485cd0df83cSstephan mTimeStart(opName); 486cd0df83cSstephan const deleteOnClose = (state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags); 487cd0df83cSstephan const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags); 488cd0df83cSstephan wTimeStart('xOpen'); 489cd0df83cSstephan try{ 490cd0df83cSstephan let hDir, filenamePart; 491cd0df83cSstephan try { 492cd0df83cSstephan [hDir, filenamePart] = await getDirForFilename(filename, !!create); 493cd0df83cSstephan }catch(e){ 494c18c8bf9Sstephan state.s11n.storeException(1,e); 495c18c8bf9Sstephan storeAndNotify(opName, state.sq3Codes.SQLITE_NOTFOUND); 496cd0df83cSstephan mTimeEnd(); 497cd0df83cSstephan wTimeEnd(); 498cd0df83cSstephan return; 499cd0df83cSstephan } 500cd0df83cSstephan const hFile = await hDir.getFileHandle(filenamePart, {create}); 501cd0df83cSstephan /** 502cd0df83cSstephan wa-sqlite, at this point, grabs a SyncAccessHandle and 503cd0df83cSstephan assigns it to the syncHandle prop of the file state 504cd0df83cSstephan object, but only for certain cases and it's unclear why it 505cd0df83cSstephan places that limitation on it. 506cd0df83cSstephan */ 507cd0df83cSstephan wTimeEnd(); 508cd0df83cSstephan __openFiles[fid] = Object.assign(Object.create(null),{ 509*da264159Sstephan fid: fid, 510cd0df83cSstephan filenameAbs: filename, 511cd0df83cSstephan filenamePart: filenamePart, 512cd0df83cSstephan dirHandle: hDir, 513cd0df83cSstephan fileHandle: hFile, 514cd0df83cSstephan sabView: state.sabFileBufView, 515cd0df83cSstephan readOnly: create 516cd0df83cSstephan ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags), 517cd0df83cSstephan deleteOnClose: deleteOnClose 518cd0df83cSstephan }); 519cd0df83cSstephan storeAndNotify(opName, 0); 520cd0df83cSstephan }catch(e){ 521cd0df83cSstephan wTimeEnd(); 522cd0df83cSstephan error(opName,e); 523cd0df83cSstephan state.s11n.storeException(1,e); 524cd0df83cSstephan storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR); 525cd0df83cSstephan } 526cd0df83cSstephan mTimeEnd(); 527cd0df83cSstephan }, 528f861b36bSstephan xRead: async function(fid/*sqlite3_file pointer*/,n,offset64){ 529cd0df83cSstephan mTimeStart('xRead'); 530cd0df83cSstephan let rc = 0, nRead; 531cd0df83cSstephan const fh = __openFiles[fid]; 532cd0df83cSstephan try{ 533f45c3370Sstephan affirmLocked('xRead',fh); 534cd0df83cSstephan wTimeStart('xRead'); 535cd0df83cSstephan nRead = (await getSyncHandle(fh)).read( 536cd0df83cSstephan fh.sabView.subarray(0, n), 537f861b36bSstephan {at: Number(offset64)} 538cd0df83cSstephan ); 539cd0df83cSstephan wTimeEnd(); 540cd0df83cSstephan if(nRead < n){/* Zero-fill remaining bytes */ 541cd0df83cSstephan fh.sabView.fill(0, nRead, n); 542cd0df83cSstephan rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ; 543cd0df83cSstephan } 544cd0df83cSstephan }catch(e){ 545cd0df83cSstephan if(undefined===nRead) wTimeEnd(); 546cd0df83cSstephan error("xRead() failed",e,fh); 547cd0df83cSstephan state.s11n.storeException(1,e); 548cd0df83cSstephan rc = state.sq3Codes.SQLITE_IOERR_READ; 549cd0df83cSstephan } 550cd0df83cSstephan storeAndNotify('xRead',rc); 551cd0df83cSstephan mTimeEnd(); 552cd0df83cSstephan }, 553f861b36bSstephan xSync: async function(fid/*sqlite3_file pointer*/,flags/*ignored*/){ 554cd0df83cSstephan mTimeStart('xSync'); 555cd0df83cSstephan const fh = __openFiles[fid]; 556cd0df83cSstephan let rc = 0; 557cd0df83cSstephan if(!fh.readOnly && fh.syncHandle){ 558cd0df83cSstephan try { 559cd0df83cSstephan wTimeStart('xSync'); 560cd0df83cSstephan await fh.syncHandle.flush(); 561cd0df83cSstephan }catch(e){ 562cd0df83cSstephan state.s11n.storeException(2,e); 56343b442a6Sstephan rc = state.sq3Codes.SQLITE_IOERR_FSYNC; 564cd0df83cSstephan } 565cd0df83cSstephan wTimeEnd(); 566cd0df83cSstephan } 567cd0df83cSstephan storeAndNotify('xSync',rc); 568cd0df83cSstephan mTimeEnd(); 569cd0df83cSstephan }, 570f861b36bSstephan xTruncate: async function(fid/*sqlite3_file pointer*/,size){ 571cd0df83cSstephan mTimeStart('xTruncate'); 572cd0df83cSstephan let rc = 0; 573cd0df83cSstephan const fh = __openFiles[fid]; 574cd0df83cSstephan wTimeStart('xTruncate'); 575cd0df83cSstephan try{ 576f45c3370Sstephan affirmLocked('xTruncate',fh); 577cd0df83cSstephan affirmNotRO('xTruncate', fh); 578cd0df83cSstephan await (await getSyncHandle(fh)).truncate(size); 579cd0df83cSstephan }catch(e){ 580cd0df83cSstephan error("xTruncate():",e,fh); 581cd0df83cSstephan state.s11n.storeException(2,e); 582cd0df83cSstephan rc = state.sq3Codes.SQLITE_IOERR_TRUNCATE; 583cd0df83cSstephan } 584cd0df83cSstephan wTimeEnd(); 585cd0df83cSstephan storeAndNotify('xTruncate',rc); 586cd0df83cSstephan mTimeEnd(); 587cd0df83cSstephan }, 588f861b36bSstephan xUnlock: async function(fid/*sqlite3_file pointer*/, 589f861b36bSstephan lockType/*SQLITE_LOCK_...*/){ 590cd0df83cSstephan mTimeStart('xUnlock'); 591cd0df83cSstephan let rc = 0; 592cd0df83cSstephan const fh = __openFiles[fid]; 593cd0df83cSstephan if( state.sq3Codes.SQLITE_LOCK_NONE===lockType 594cd0df83cSstephan && fh.syncHandle ){ 595cd0df83cSstephan wTimeStart('xUnlock'); 596cd0df83cSstephan try { await closeSyncHandle(fh) } 597cd0df83cSstephan catch(e){ 598cd0df83cSstephan state.s11n.storeException(1,e); 59943b442a6Sstephan rc = state.sq3Codes.SQLITE_IOERR_UNLOCK; 600cd0df83cSstephan } 601cd0df83cSstephan wTimeEnd(); 602cd0df83cSstephan } 603cd0df83cSstephan storeAndNotify('xUnlock',rc); 604cd0df83cSstephan mTimeEnd(); 605cd0df83cSstephan }, 606f861b36bSstephan xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){ 607cd0df83cSstephan mTimeStart('xWrite'); 608cd0df83cSstephan let rc; 609f45c3370Sstephan const fh = __openFiles[fid]; 610cd0df83cSstephan wTimeStart('xWrite'); 611cd0df83cSstephan try{ 612f45c3370Sstephan affirmLocked('xWrite',fh); 613cd0df83cSstephan affirmNotRO('xWrite', fh); 614cd0df83cSstephan rc = ( 615cd0df83cSstephan n === (await getSyncHandle(fh)) 616cd0df83cSstephan .write(fh.sabView.subarray(0, n), 617f861b36bSstephan {at: Number(offset64)}) 618cd0df83cSstephan ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE; 619cd0df83cSstephan }catch(e){ 620cd0df83cSstephan error("xWrite():",e,fh); 621cd0df83cSstephan state.s11n.storeException(1,e); 622cd0df83cSstephan rc = state.sq3Codes.SQLITE_IOERR_WRITE; 623cd0df83cSstephan } 624cd0df83cSstephan wTimeEnd(); 625cd0df83cSstephan storeAndNotify('xWrite',rc); 626cd0df83cSstephan mTimeEnd(); 627cd0df83cSstephan } 628cd0df83cSstephan}/*vfsAsyncImpls*/; 629cd0df83cSstephan 630cd0df83cSstephanconst initS11n = ()=>{ 631cd0df83cSstephan /** 632cd0df83cSstephan ACHTUNG: this code is 100% duplicated in the other half of this 633cd0df83cSstephan proxy! The documentation is maintained in the "synchronous half". 634cd0df83cSstephan */ 635cd0df83cSstephan if(state.s11n) return state.s11n; 636cd0df83cSstephan const textDecoder = new TextDecoder(), 637cd0df83cSstephan textEncoder = new TextEncoder('utf-8'), 638cd0df83cSstephan viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize), 639cd0df83cSstephan viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize); 640cd0df83cSstephan state.s11n = Object.create(null); 641cd0df83cSstephan const TypeIds = Object.create(null); 642cd0df83cSstephan TypeIds.number = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' }; 643cd0df83cSstephan TypeIds.bigint = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' }; 644cd0df83cSstephan TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' }; 645cd0df83cSstephan TypeIds.string = { id: 4 }; 646cd0df83cSstephan const getTypeId = (v)=>( 647cd0df83cSstephan TypeIds[typeof v] 648cd0df83cSstephan || toss("Maintenance required: this value type cannot be serialized.",v) 649cd0df83cSstephan ); 650cd0df83cSstephan const getTypeIdById = (tid)=>{ 651cd0df83cSstephan switch(tid){ 652cd0df83cSstephan case TypeIds.number.id: return TypeIds.number; 653cd0df83cSstephan case TypeIds.bigint.id: return TypeIds.bigint; 654cd0df83cSstephan case TypeIds.boolean.id: return TypeIds.boolean; 655cd0df83cSstephan case TypeIds.string.id: return TypeIds.string; 656cd0df83cSstephan default: toss("Invalid type ID:",tid); 657cd0df83cSstephan } 658cd0df83cSstephan }; 659*da264159Sstephan state.s11n.deserialize = function(clear=false){ 660cd0df83cSstephan ++metrics.s11n.deserialize.count; 661cd0df83cSstephan const t = performance.now(); 662cd0df83cSstephan const argc = viewU8[0]; 663cd0df83cSstephan const rc = argc ? [] : null; 664cd0df83cSstephan if(argc){ 665cd0df83cSstephan const typeIds = []; 666cd0df83cSstephan let offset = 1, i, n, v; 667cd0df83cSstephan for(i = 0; i < argc; ++i, ++offset){ 668cd0df83cSstephan typeIds.push(getTypeIdById(viewU8[offset])); 669cd0df83cSstephan } 670cd0df83cSstephan for(i = 0; i < argc; ++i){ 671cd0df83cSstephan const t = typeIds[i]; 672cd0df83cSstephan if(t.getter){ 673cd0df83cSstephan v = viewDV[t.getter](offset, state.littleEndian); 674cd0df83cSstephan offset += t.size; 675cd0df83cSstephan }else{/*String*/ 676cd0df83cSstephan n = viewDV.getInt32(offset, state.littleEndian); 677cd0df83cSstephan offset += 4; 678cd0df83cSstephan v = textDecoder.decode(viewU8.slice(offset, offset+n)); 679cd0df83cSstephan offset += n; 680cd0df83cSstephan } 681cd0df83cSstephan rc.push(v); 682cd0df83cSstephan } 683cd0df83cSstephan } 684*da264159Sstephan if(clear) viewU8[0] = 0; 685cd0df83cSstephan //log("deserialize:",argc, rc); 686cd0df83cSstephan metrics.s11n.deserialize.time += performance.now() - t; 687cd0df83cSstephan return rc; 688cd0df83cSstephan }; 689cd0df83cSstephan state.s11n.serialize = function(...args){ 690cd0df83cSstephan const t = performance.now(); 691cd0df83cSstephan ++metrics.s11n.serialize.count; 692cd0df83cSstephan if(args.length){ 693cd0df83cSstephan //log("serialize():",args); 694cd0df83cSstephan const typeIds = []; 695cd0df83cSstephan let i = 0, offset = 1; 696cd0df83cSstephan viewU8[0] = args.length & 0xff /* header = # of args */; 697cd0df83cSstephan for(; i < args.length; ++i, ++offset){ 698cd0df83cSstephan /* Write the TypeIds.id value into the next args.length 699cd0df83cSstephan bytes. */ 700cd0df83cSstephan typeIds.push(getTypeId(args[i])); 701cd0df83cSstephan viewU8[offset] = typeIds[i].id; 702cd0df83cSstephan } 703cd0df83cSstephan for(i = 0; i < args.length; ++i) { 704cd0df83cSstephan /* Deserialize the following bytes based on their 705cd0df83cSstephan corresponding TypeIds.id from the header. */ 706cd0df83cSstephan const t = typeIds[i]; 707cd0df83cSstephan if(t.setter){ 708cd0df83cSstephan viewDV[t.setter](offset, args[i], state.littleEndian); 709cd0df83cSstephan offset += t.size; 710cd0df83cSstephan }else{/*String*/ 711cd0df83cSstephan const s = textEncoder.encode(args[i]); 712cd0df83cSstephan viewDV.setInt32(offset, s.byteLength, state.littleEndian); 713cd0df83cSstephan offset += 4; 714cd0df83cSstephan viewU8.set(s, offset); 715cd0df83cSstephan offset += s.byteLength; 716cd0df83cSstephan } 717cd0df83cSstephan } 718cd0df83cSstephan //log("serialize() result:",viewU8.slice(0,offset)); 719cd0df83cSstephan }else{ 720cd0df83cSstephan viewU8[0] = 0; 721cd0df83cSstephan } 722cd0df83cSstephan metrics.s11n.serialize.time += performance.now() - t; 723cd0df83cSstephan }; 724cd0df83cSstephan 725cd0df83cSstephan state.s11n.storeException = state.asyncS11nExceptions 726cd0df83cSstephan ? ((priority,e)=>{ 727cd0df83cSstephan if(priority<=state.asyncS11nExceptions){ 7284df2ab57Sstephan state.s11n.serialize([e.name,': ',e.message].join("")); 729cd0df83cSstephan } 730cd0df83cSstephan }) 731cd0df83cSstephan : ()=>{}; 732cd0df83cSstephan 733cd0df83cSstephan return state.s11n; 734cd0df83cSstephan}/*initS11n()*/; 735cd0df83cSstephan 736cd0df83cSstephanconst waitLoop = async function f(){ 737cd0df83cSstephan const opHandlers = Object.create(null); 738cd0df83cSstephan for(let k of Object.keys(state.opIds)){ 739cd0df83cSstephan const vi = vfsAsyncImpls[k]; 740cd0df83cSstephan if(!vi) continue; 741cd0df83cSstephan const o = Object.create(null); 742cd0df83cSstephan opHandlers[state.opIds[k]] = o; 743cd0df83cSstephan o.key = k; 744cd0df83cSstephan o.f = vi; 745cd0df83cSstephan } 746cd0df83cSstephan /** 747cd0df83cSstephan waitTime is how long (ms) to wait for each Atomics.wait(). 748cd0df83cSstephan We need to wake up periodically to give the thread a chance 749cd0df83cSstephan to do other things. 750cd0df83cSstephan */ 751*da264159Sstephan const waitTime = 500; 752cd0df83cSstephan while(!flagAsyncShutdown){ 753cd0df83cSstephan try { 754cd0df83cSstephan if('timed-out'===Atomics.wait( 755cd0df83cSstephan state.sabOPView, state.opIds.whichOp, 0, waitTime 756cd0df83cSstephan )){ 757*da264159Sstephan if(__autoLocks.size){ 758*da264159Sstephan /* Release all auto-locks. */ 759*da264159Sstephan for(const fid of __autoLocks){ 760*da264159Sstephan const fh = __openFiles[fid]; 761*da264159Sstephan await closeSyncHandleNoThrow(fh); 762*da264159Sstephan log("Auto-unlocked",fid,fh.filenameAbs); 763*da264159Sstephan } 764*da264159Sstephan } 765cd0df83cSstephan continue; 766cd0df83cSstephan } 767cd0df83cSstephan const opId = Atomics.load(state.sabOPView, state.opIds.whichOp); 768cd0df83cSstephan Atomics.store(state.sabOPView, state.opIds.whichOp, 0); 769cd0df83cSstephan const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId); 770*da264159Sstephan const args = state.s11n.deserialize( 771*da264159Sstephan true /* clear s11n to keep the caller from confusing this with 772*da264159Sstephan an exception string written by the upcoming 773*da264159Sstephan operation */ 774*da264159Sstephan ) || []; 775cd0df83cSstephan //warn("waitLoop() whichOp =",opId, hnd, args); 776cd0df83cSstephan if(hnd.f) await hnd.f(...args); 777cd0df83cSstephan else error("Missing callback for opId",opId); 778cd0df83cSstephan }catch(e){ 779cd0df83cSstephan error('in waitLoop():',e); 780cd0df83cSstephan } 781cd0df83cSstephan } 782cd0df83cSstephan}; 783cd0df83cSstephan 784cd0df83cSstephannavigator.storage.getDirectory().then(function(d){ 785cd0df83cSstephan const wMsg = (type)=>postMessage({type}); 786cd0df83cSstephan state.rootDir = d; 787cd0df83cSstephan self.onmessage = function({data}){ 788cd0df83cSstephan switch(data.type){ 789cd0df83cSstephan case 'opfs-async-init':{ 790cd0df83cSstephan /* Receive shared state from synchronous partner */ 791cd0df83cSstephan const opt = data.args; 792cd0df83cSstephan state.littleEndian = opt.littleEndian; 793cd0df83cSstephan state.asyncS11nExceptions = opt.asyncS11nExceptions; 794cd0df83cSstephan state.verbose = opt.verbose ?? 2; 795cd0df83cSstephan state.fileBufferSize = opt.fileBufferSize; 796cd0df83cSstephan state.sabS11nOffset = opt.sabS11nOffset; 797cd0df83cSstephan state.sabS11nSize = opt.sabS11nSize; 798cd0df83cSstephan state.sabOP = opt.sabOP; 799cd0df83cSstephan state.sabOPView = new Int32Array(state.sabOP); 800cd0df83cSstephan state.sabIO = opt.sabIO; 801cd0df83cSstephan state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize); 802cd0df83cSstephan state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize); 803cd0df83cSstephan state.opIds = opt.opIds; 804cd0df83cSstephan state.sq3Codes = opt.sq3Codes; 805cd0df83cSstephan Object.keys(vfsAsyncImpls).forEach((k)=>{ 806cd0df83cSstephan if(!Number.isFinite(state.opIds[k])){ 807cd0df83cSstephan toss("Maintenance required: missing state.opIds[",k,"]"); 808cd0df83cSstephan } 809cd0df83cSstephan }); 810cd0df83cSstephan initS11n(); 811cd0df83cSstephan metrics.reset(); 812cd0df83cSstephan log("init state",state); 813cd0df83cSstephan wMsg('opfs-async-inited'); 814cd0df83cSstephan waitLoop(); 815cd0df83cSstephan break; 816cd0df83cSstephan } 817cd0df83cSstephan case 'opfs-async-restart': 818cd0df83cSstephan if(flagAsyncShutdown){ 819cd0df83cSstephan warn("Restarting after opfs-async-shutdown. Might or might not work."); 820cd0df83cSstephan flagAsyncShutdown = false; 821cd0df83cSstephan waitLoop(); 822cd0df83cSstephan } 823cd0df83cSstephan break; 824cd0df83cSstephan case 'opfs-async-metrics': 825cd0df83cSstephan metrics.dump(); 826cd0df83cSstephan break; 827cd0df83cSstephan } 828cd0df83cSstephan }; 829cd0df83cSstephan wMsg('opfs-async-loaded'); 830cd0df83cSstephan}).catch((e)=>error("error initializing OPFS asyncer:",e)); 831