1/* 2 2022-09-16 3 4 The author disclaims copyright to this source code. In place of a 5 legal notice, here is a blessing: 6 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. 10 11 *********************************************************************** 12 13 An 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-api-opfs.js. 17 18 Highly indebted to: 19 20 https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/OriginPrivateFileSystemVFS.js 21 22 for demonstrating how to use the OPFS APIs. 23 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. 28*/ 29'use strict'; 30const toss = function(...args){throw new Error(args.join(' '))}; 31if(self.window === self){ 32 toss("This code cannot run from the main thread.", 33 "Load it as a Worker from a separate Worker."); 34}else if(!navigator.storage.getDirectory){ 35 toss("This API requires navigator.storage.getDirectory."); 36} 37 38/** 39 Will hold state copied to this object from the syncronous side of 40 this API. 41*/ 42const state = Object.create(null); 43/** 44 verbose: 45 46 0 = no logging output 47 1 = only errors 48 2 = warnings and errors 49 3 = debug, warnings, and errors 50*/ 51state.verbose = 2; 52 53const loggers = { 54 0:console.error.bind(console), 55 1:console.warn.bind(console), 56 2:console.log.bind(console) 57}; 58const logImpl = (level,...args)=>{ 59 if(state.verbose>level) loggers[level]("OPFS asyncer:",...args); 60}; 61const log = (...args)=>logImpl(2, ...args); 62const warn = (...args)=>logImpl(1, ...args); 63const error = (...args)=>logImpl(0, ...args); 64const metrics = Object.create(null); 65metrics.reset = ()=>{ 66 let k; 67 const r = (m)=>(m.count = m.time = m.wait = 0); 68 for(k in state.opIds){ 69 r(metrics[k] = Object.create(null)); 70 } 71 let s = metrics.s11n = Object.create(null); 72 s = s.serialize = Object.create(null); 73 s.count = s.time = 0; 74 s = metrics.s11n.deserialize = Object.create(null); 75 s.count = s.time = 0; 76}; 77metrics.dump = ()=>{ 78 let k, n = 0, t = 0, w = 0; 79 for(k in state.opIds){ 80 const m = metrics[k]; 81 n += m.count; 82 t += m.time; 83 w += m.wait; 84 m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0; 85 } 86 console.log(self.location.href, 87 "metrics for",self.location.href,":\n", 88 metrics, 89 "\nTotal of",n,"op(s) for",t,"ms", 90 "approx",w,"ms spent waiting on OPFS APIs."); 91 console.log("Serialization metrics:",metrics.s11n); 92}; 93 94/** 95 Map of sqlite3_file pointers (integers) to metadata related to a 96 given OPFS file handles. The pointers are, in this side of the 97 interface, opaque file handle IDs provided by the synchronous 98 part of this constellation. Each value is an object with a structure 99 demonstrated in the xOpen() impl. 100*/ 101const __openFiles = Object.create(null); 102 103/** 104 Expects an OPFS file path. It gets resolved, such that ".." 105 components are properly expanded, and returned. If the 2nd arg is 106 true, the result is returned as an array of path elements, else an 107 absolute path string is returned. 108*/ 109const getResolvedPath = function(filename,splitIt){ 110 const p = new URL( 111 filename, 'file://irrelevant' 112 ).pathname; 113 return splitIt ? p.split('/').filter((v)=>!!v) : p; 114}; 115 116/** 117 Takes the absolute path to a filesystem element. Returns an array 118 of [handleOfContainingDir, filename]. If the 2nd argument is truthy 119 then each directory element leading to the file is created along 120 the way. Throws if any creation or resolution fails. 121*/ 122const getDirForFilename = async function f(absFilename, createDirs = false){ 123 const path = getResolvedPath(absFilename, true); 124 const filename = path.pop(); 125 let dh = state.rootDir; 126 for(const dirName of path){ 127 if(dirName){ 128 dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs}); 129 } 130 } 131 return [dh, filename]; 132}; 133 134/** 135 Returns the sync access handle associated with the given file 136 handle object (which must be a valid handle object), lazily opening 137 it if needed. 138 139 In order to help alleviate cross-tab contention for a dabase, 140 if an exception is thrown while acquiring the handle, this routine 141 will wait briefly and try again, up to 3 times. If acquisition 142 still fails at that point it will give up and propagate the 143 exception. 144*/ 145const getSyncHandle = async (fh)=>{ 146 if(!fh.syncHandle){ 147 const t = performance.now(); 148 log("Acquiring sync handle for",fh.filenameAbs); 149 const maxTries = 3; 150 let i = 1, ms = 300; 151 for(; true; ms *= ++i){ 152 try { 153 //if(1===i) toss("Just testing."); 154 //TODO? A config option which tells it to throw here 155 //randomly every now and then, for testing purposes. 156 fh.syncHandle = await fh.fileHandle.createSyncAccessHandle(); 157 break; 158 }catch(e){ 159 if(i === maxTries){ 160 toss("Error getting sync handle.",maxTries, 161 "attempts failed. ",fh.filenameAbs, ":", e.message); 162 throw e; 163 } 164 warn("Error getting sync handle. Waiting",ms, 165 "ms and trying again.",fh.filenameAbs,e); 166 Atomics.wait(state.sabOPView, state.opIds.xSleep, 0, ms); 167 } 168 } 169 log("Got sync handle for",fh.filenameAbs,'in',performance.now() - t,'ms'); 170 } 171 return fh.syncHandle; 172}; 173 174/** 175 If the given file-holding object has a sync handle attached to it, 176 that handle is remove and asynchronously closed. Though it may 177 sound sensible to continue work as soon as the close() returns 178 (noting that it's asynchronous), doing so can cause operations 179 performed soon afterwards, e.g. a call to getSyncHandle() to fail 180 because they may happen out of order from the close(). OPFS does 181 not guaranty that the actual order of operations is retained in 182 such cases. i.e. always "await" on the result of this function. 183*/ 184const closeSyncHandle = async (fh)=>{ 185 if(fh.syncHandle){ 186 log("Closing sync handle for",fh.filenameAbs); 187 const h = fh.syncHandle; 188 delete fh.syncHandle; 189 return h.close(); 190 } 191}; 192 193/** 194 Stores the given value at state.sabOPView[state.opIds.rc] and then 195 Atomics.notify()'s it. 196*/ 197const storeAndNotify = (opName, value)=>{ 198 log(opName+"() => notify(",state.opIds.rc,",",value,")"); 199 Atomics.store(state.sabOPView, state.opIds.rc, value); 200 Atomics.notify(state.sabOPView, state.opIds.rc); 201}; 202 203/** 204 Throws if fh is a file-holding object which is flagged as read-only. 205*/ 206const affirmNotRO = function(opName,fh){ 207 if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs); 208}; 209 210/** 211 We track 2 different timers: the "metrics" timer records how much 212 time we spend performing work. The "wait" timer records how much 213 time we spend waiting on the underlying OPFS timer. See the calls 214 to mTimeStart(), mTimeEnd(), wTimeStart(), and wTimeEnd() 215 throughout this file to see how they're used. 216*/ 217const __mTimer = Object.create(null); 218__mTimer.op = undefined; 219__mTimer.start = undefined; 220const mTimeStart = (op)=>{ 221 __mTimer.start = performance.now(); 222 __mTimer.op = op; 223 //metrics[op] || toss("Maintenance required: missing metrics for",op); 224 ++metrics[op].count; 225}; 226const mTimeEnd = ()=>( 227 metrics[__mTimer.op].time += performance.now() - __mTimer.start 228); 229const __wTimer = Object.create(null); 230__wTimer.op = undefined; 231__wTimer.start = undefined; 232const wTimeStart = (op)=>{ 233 __wTimer.start = performance.now(); 234 __wTimer.op = op; 235 //metrics[op] || toss("Maintenance required: missing metrics for",op); 236}; 237const wTimeEnd = ()=>( 238 metrics[__wTimer.op].wait += performance.now() - __wTimer.start 239); 240 241/** 242 Gets set to true by the 'opfs-async-shutdown' command to quit the 243 wait loop. This is only intended for debugging purposes: we cannot 244 inspect this file's state while the tight waitLoop() is running and 245 need a way to stop that loop for introspection purposes. 246*/ 247let flagAsyncShutdown = false; 248 249 250/** 251 Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods 252 methods, as well as helpers like mkdir(). Maintenance reminder: 253 members are in alphabetical order to simplify finding them. 254*/ 255const vfsAsyncImpls = { 256 'opfs-async-metrics': async ()=>{ 257 mTimeStart('opfs-async-metrics'); 258 metrics.dump(); 259 storeAndNotify('opfs-async-metrics', 0); 260 mTimeEnd(); 261 }, 262 'opfs-async-shutdown': async ()=>{ 263 flagAsyncShutdown = true; 264 storeAndNotify('opfs-async-shutdown', 0); 265 }, 266 mkdir: async (dirname)=>{ 267 mTimeStart('mkdir'); 268 let rc = 0; 269 wTimeStart('mkdir'); 270 try { 271 await getDirForFilename(dirname+"/filepart", true); 272 }catch(e){ 273 state.s11n.storeException(2,e); 274 rc = state.sq3Codes.SQLITE_IOERR; 275 }finally{ 276 wTimeEnd(); 277 } 278 storeAndNotify('mkdir', rc); 279 mTimeEnd(); 280 }, 281 xAccess: async (filename)=>{ 282 mTimeStart('xAccess'); 283 /* OPFS cannot support the full range of xAccess() queries sqlite3 284 calls for. We can essentially just tell if the file is 285 accessible, but if it is it's automatically writable (unless 286 it's locked, which we cannot(?) know without trying to open 287 it). OPFS does not have the notion of read-only. 288 289 The return semantics of this function differ from sqlite3's 290 xAccess semantics because we are limited in what we can 291 communicate back to our synchronous communication partner: 0 = 292 accessible, non-0 means not accessible. 293 */ 294 let rc = 0; 295 wTimeStart('xAccess'); 296 try{ 297 const [dh, fn] = await getDirForFilename(filename); 298 await dh.getFileHandle(fn); 299 }catch(e){ 300 state.s11n.storeException(2,e); 301 rc = state.sq3Codes.SQLITE_IOERR; 302 }finally{ 303 wTimeEnd(); 304 } 305 storeAndNotify('xAccess', rc); 306 mTimeEnd(); 307 }, 308 xClose: async function(fid){ 309 const opName = 'xClose'; 310 mTimeStart(opName); 311 const fh = __openFiles[fid]; 312 let rc = 0; 313 wTimeStart('xClose'); 314 if(fh){ 315 delete __openFiles[fid]; 316 await closeSyncHandle(fh); 317 if(fh.deleteOnClose){ 318 try{ await fh.dirHandle.removeEntry(fh.filenamePart) } 319 catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) } 320 } 321 }else{ 322 state.s11n.serialize(); 323 rc = state.sq3Codes.SQLITE_NOTFOUND; 324 } 325 wTimeEnd(); 326 storeAndNotify(opName, rc); 327 mTimeEnd(); 328 }, 329 xDelete: async function(...args){ 330 mTimeStart('xDelete'); 331 const rc = await vfsAsyncImpls.xDeleteNoWait(...args); 332 storeAndNotify('xDelete', rc); 333 mTimeEnd(); 334 }, 335 xDeleteNoWait: async function(filename, syncDir = 0, recursive = false){ 336 /* The syncDir flag is, for purposes of the VFS API's semantics, 337 ignored here. However, if it has the value 0x1234 then: after 338 deleting the given file, recursively try to delete any empty 339 directories left behind in its wake (ignoring any errors and 340 stopping at the first failure). 341 342 That said: we don't know for sure that removeEntry() fails if 343 the dir is not empty because the API is not documented. It has, 344 however, a "recursive" flag which defaults to false, so 345 presumably it will fail if the dir is not empty and that flag 346 is false. 347 */ 348 let rc = 0; 349 wTimeStart('xDelete'); 350 try { 351 while(filename){ 352 const [hDir, filenamePart] = await getDirForFilename(filename, false); 353 if(!filenamePart) break; 354 await hDir.removeEntry(filenamePart, {recursive}); 355 if(0x1234 !== syncDir) break; 356 filename = getResolvedPath(filename, true); 357 filename.pop(); 358 filename = filename.join('/'); 359 } 360 }catch(e){ 361 state.s11n.storeException(2,e); 362 rc = state.sq3Codes.SQLITE_IOERR_DELETE; 363 } 364 wTimeEnd(); 365 return rc; 366 }, 367 xFileSize: async function(fid){ 368 mTimeStart('xFileSize'); 369 const fh = __openFiles[fid]; 370 let sz; 371 wTimeStart('xFileSize'); 372 try{ 373 sz = await (await getSyncHandle(fh)).getSize(); 374 state.s11n.serialize(Number(sz)); 375 sz = 0; 376 }catch(e){ 377 state.s11n.storeException(2,e); 378 sz = state.sq3Codes.SQLITE_IOERR; 379 } 380 wTimeEnd(); 381 storeAndNotify('xFileSize', sz); 382 mTimeEnd(); 383 }, 384 xLock: async function(fid,lockType){ 385 mTimeStart('xLock'); 386 const fh = __openFiles[fid]; 387 let rc = 0; 388 if( !fh.syncHandle ){ 389 wTimeStart('xLock'); 390 try { await getSyncHandle(fh) } 391 catch(e){ 392 state.s11n.storeException(1,e); 393 rc = state.sq3Codes.SQLITE_IOERR; 394 } 395 wTimeEnd(); 396 } 397 storeAndNotify('xLock',rc); 398 mTimeEnd(); 399 }, 400 xOpen: async function(fid/*sqlite3_file pointer*/, filename, flags){ 401 const opName = 'xOpen'; 402 mTimeStart(opName); 403 const deleteOnClose = (state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags); 404 const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags); 405 wTimeStart('xOpen'); 406 try{ 407 let hDir, filenamePart; 408 try { 409 [hDir, filenamePart] = await getDirForFilename(filename, !!create); 410 }catch(e){ 411 storeAndNotify(opName, state.sql3Codes.SQLITE_NOTFOUND); 412 mTimeEnd(); 413 wTimeEnd(); 414 return; 415 } 416 const hFile = await hDir.getFileHandle(filenamePart, {create}); 417 /** 418 wa-sqlite, at this point, grabs a SyncAccessHandle and 419 assigns it to the syncHandle prop of the file state 420 object, but only for certain cases and it's unclear why it 421 places that limitation on it. 422 */ 423 wTimeEnd(); 424 __openFiles[fid] = Object.assign(Object.create(null),{ 425 filenameAbs: filename, 426 filenamePart: filenamePart, 427 dirHandle: hDir, 428 fileHandle: hFile, 429 sabView: state.sabFileBufView, 430 readOnly: create 431 ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags), 432 deleteOnClose: deleteOnClose 433 }); 434 storeAndNotify(opName, 0); 435 }catch(e){ 436 wTimeEnd(); 437 error(opName,e); 438 state.s11n.storeException(1,e); 439 storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR); 440 } 441 mTimeEnd(); 442 }, 443 xRead: async function(fid,n,offset){ 444 mTimeStart('xRead'); 445 let rc = 0, nRead; 446 const fh = __openFiles[fid]; 447 try{ 448 wTimeStart('xRead'); 449 nRead = (await getSyncHandle(fh)).read( 450 fh.sabView.subarray(0, n), 451 {at: Number(offset)} 452 ); 453 wTimeEnd(); 454 if(nRead < n){/* Zero-fill remaining bytes */ 455 fh.sabView.fill(0, nRead, n); 456 rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ; 457 } 458 }catch(e){ 459 if(undefined===nRead) wTimeEnd(); 460 error("xRead() failed",e,fh); 461 state.s11n.storeException(1,e); 462 rc = state.sq3Codes.SQLITE_IOERR_READ; 463 } 464 storeAndNotify('xRead',rc); 465 mTimeEnd(); 466 }, 467 xSync: async function(fid,flags/*ignored*/){ 468 mTimeStart('xSync'); 469 const fh = __openFiles[fid]; 470 let rc = 0; 471 if(!fh.readOnly && fh.syncHandle){ 472 try { 473 wTimeStart('xSync'); 474 await fh.syncHandle.flush(); 475 }catch(e){ 476 state.s11n.storeException(2,e); 477 } 478 wTimeEnd(); 479 } 480 storeAndNotify('xSync',rc); 481 mTimeEnd(); 482 }, 483 xTruncate: async function(fid,size){ 484 mTimeStart('xTruncate'); 485 let rc = 0; 486 const fh = __openFiles[fid]; 487 wTimeStart('xTruncate'); 488 try{ 489 affirmNotRO('xTruncate', fh); 490 await (await getSyncHandle(fh)).truncate(size); 491 }catch(e){ 492 error("xTruncate():",e,fh); 493 state.s11n.storeException(2,e); 494 rc = state.sq3Codes.SQLITE_IOERR_TRUNCATE; 495 } 496 wTimeEnd(); 497 storeAndNotify('xTruncate',rc); 498 mTimeEnd(); 499 }, 500 xUnlock: async function(fid,lockType){ 501 mTimeStart('xUnlock'); 502 let rc = 0; 503 const fh = __openFiles[fid]; 504 if( state.sq3Codes.SQLITE_LOCK_NONE===lockType 505 && fh.syncHandle ){ 506 wTimeStart('xUnlock'); 507 try { await closeSyncHandle(fh) } 508 catch(e){ 509 state.s11n.storeException(1,e); 510 rc = state.sq3Codes.SQLITE_IOERR; 511 } 512 wTimeEnd(); 513 } 514 storeAndNotify('xUnlock',rc); 515 mTimeEnd(); 516 }, 517 xWrite: async function(fid,n,offset){ 518 mTimeStart('xWrite'); 519 let rc; 520 wTimeStart('xWrite'); 521 try{ 522 const fh = __openFiles[fid]; 523 affirmNotRO('xWrite', fh); 524 rc = ( 525 n === (await getSyncHandle(fh)) 526 .write(fh.sabView.subarray(0, n), 527 {at: Number(offset)}) 528 ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE; 529 }catch(e){ 530 error("xWrite():",e,fh); 531 state.s11n.storeException(1,e); 532 rc = state.sq3Codes.SQLITE_IOERR_WRITE; 533 } 534 wTimeEnd(); 535 storeAndNotify('xWrite',rc); 536 mTimeEnd(); 537 } 538}/*vfsAsyncImpls*/; 539 540const initS11n = ()=>{ 541 /** 542 ACHTUNG: this code is 100% duplicated in the other half of this 543 proxy! The documentation is maintained in the "synchronous half". 544 */ 545 if(state.s11n) return state.s11n; 546 const textDecoder = new TextDecoder(), 547 textEncoder = new TextEncoder('utf-8'), 548 viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize), 549 viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize); 550 state.s11n = Object.create(null); 551 const TypeIds = Object.create(null); 552 TypeIds.number = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' }; 553 TypeIds.bigint = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' }; 554 TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' }; 555 TypeIds.string = { id: 4 }; 556 const getTypeId = (v)=>( 557 TypeIds[typeof v] 558 || toss("Maintenance required: this value type cannot be serialized.",v) 559 ); 560 const getTypeIdById = (tid)=>{ 561 switch(tid){ 562 case TypeIds.number.id: return TypeIds.number; 563 case TypeIds.bigint.id: return TypeIds.bigint; 564 case TypeIds.boolean.id: return TypeIds.boolean; 565 case TypeIds.string.id: return TypeIds.string; 566 default: toss("Invalid type ID:",tid); 567 } 568 }; 569 state.s11n.deserialize = function(){ 570 ++metrics.s11n.deserialize.count; 571 const t = performance.now(); 572 const argc = viewU8[0]; 573 const rc = argc ? [] : null; 574 if(argc){ 575 const typeIds = []; 576 let offset = 1, i, n, v; 577 for(i = 0; i < argc; ++i, ++offset){ 578 typeIds.push(getTypeIdById(viewU8[offset])); 579 } 580 for(i = 0; i < argc; ++i){ 581 const t = typeIds[i]; 582 if(t.getter){ 583 v = viewDV[t.getter](offset, state.littleEndian); 584 offset += t.size; 585 }else{/*String*/ 586 n = viewDV.getInt32(offset, state.littleEndian); 587 offset += 4; 588 v = textDecoder.decode(viewU8.slice(offset, offset+n)); 589 offset += n; 590 } 591 rc.push(v); 592 } 593 } 594 //log("deserialize:",argc, rc); 595 metrics.s11n.deserialize.time += performance.now() - t; 596 return rc; 597 }; 598 state.s11n.serialize = function(...args){ 599 const t = performance.now(); 600 ++metrics.s11n.serialize.count; 601 if(args.length){ 602 //log("serialize():",args); 603 const typeIds = []; 604 let i = 0, offset = 1; 605 viewU8[0] = args.length & 0xff /* header = # of args */; 606 for(; i < args.length; ++i, ++offset){ 607 /* Write the TypeIds.id value into the next args.length 608 bytes. */ 609 typeIds.push(getTypeId(args[i])); 610 viewU8[offset] = typeIds[i].id; 611 } 612 for(i = 0; i < args.length; ++i) { 613 /* Deserialize the following bytes based on their 614 corresponding TypeIds.id from the header. */ 615 const t = typeIds[i]; 616 if(t.setter){ 617 viewDV[t.setter](offset, args[i], state.littleEndian); 618 offset += t.size; 619 }else{/*String*/ 620 const s = textEncoder.encode(args[i]); 621 viewDV.setInt32(offset, s.byteLength, state.littleEndian); 622 offset += 4; 623 viewU8.set(s, offset); 624 offset += s.byteLength; 625 } 626 } 627 //log("serialize() result:",viewU8.slice(0,offset)); 628 }else{ 629 viewU8[0] = 0; 630 } 631 metrics.s11n.serialize.time += performance.now() - t; 632 }; 633 634 state.s11n.storeException = state.asyncS11nExceptions 635 ? ((priority,e)=>{ 636 if(priority<=state.asyncS11nExceptions){ 637 state.s11n.serialize(e.message); 638 } 639 }) 640 : ()=>{}; 641 642 return state.s11n; 643}/*initS11n()*/; 644 645const waitLoop = async function f(){ 646 const opHandlers = Object.create(null); 647 for(let k of Object.keys(state.opIds)){ 648 const vi = vfsAsyncImpls[k]; 649 if(!vi) continue; 650 const o = Object.create(null); 651 opHandlers[state.opIds[k]] = o; 652 o.key = k; 653 o.f = vi; 654 } 655 /** 656 waitTime is how long (ms) to wait for each Atomics.wait(). 657 We need to wake up periodically to give the thread a chance 658 to do other things. 659 */ 660 const waitTime = 1000; 661 while(!flagAsyncShutdown){ 662 try { 663 if('timed-out'===Atomics.wait( 664 state.sabOPView, state.opIds.whichOp, 0, waitTime 665 )){ 666 continue; 667 } 668 const opId = Atomics.load(state.sabOPView, state.opIds.whichOp); 669 Atomics.store(state.sabOPView, state.opIds.whichOp, 0); 670 const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId); 671 const args = state.s11n.deserialize() || []; 672 state.s11n.serialize(/* clear s11n to keep the caller from 673 confusing this with an exception string 674 written by the upcoming operation */); 675 //warn("waitLoop() whichOp =",opId, hnd, args); 676 if(hnd.f) await hnd.f(...args); 677 else error("Missing callback for opId",opId); 678 }catch(e){ 679 error('in waitLoop():',e); 680 } 681 } 682}; 683 684navigator.storage.getDirectory().then(function(d){ 685 const wMsg = (type)=>postMessage({type}); 686 state.rootDir = d; 687 self.onmessage = function({data}){ 688 switch(data.type){ 689 case 'opfs-async-init':{ 690 /* Receive shared state from synchronous partner */ 691 const opt = data.args; 692 state.littleEndian = opt.littleEndian; 693 state.asyncS11nExceptions = opt.asyncS11nExceptions; 694 state.verbose = opt.verbose ?? 2; 695 state.fileBufferSize = opt.fileBufferSize; 696 state.sabS11nOffset = opt.sabS11nOffset; 697 state.sabS11nSize = opt.sabS11nSize; 698 state.sabOP = opt.sabOP; 699 state.sabOPView = new Int32Array(state.sabOP); 700 state.sabIO = opt.sabIO; 701 state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize); 702 state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize); 703 state.opIds = opt.opIds; 704 state.sq3Codes = opt.sq3Codes; 705 Object.keys(vfsAsyncImpls).forEach((k)=>{ 706 if(!Number.isFinite(state.opIds[k])){ 707 toss("Maintenance required: missing state.opIds[",k,"]"); 708 } 709 }); 710 initS11n(); 711 metrics.reset(); 712 log("init state",state); 713 wMsg('opfs-async-inited'); 714 waitLoop(); 715 break; 716 } 717 case 'opfs-async-restart': 718 if(flagAsyncShutdown){ 719 warn("Restarting after opfs-async-shutdown. Might or might not work."); 720 flagAsyncShutdown = false; 721 waitLoop(); 722 } 723 break; 724 case 'opfs-async-metrics': 725 metrics.dump(); 726 break; 727 } 728 }; 729 wMsg('opfs-async-loaded'); 730}).catch((e)=>error("error initializing OPFS asyncer:",e)); 731