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