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 recursive = false; 381 filename = getResolvedPath(filename, true); 382 filename.pop(); 383 filename = filename.join('/'); 384 } 385 }catch(e){ 386 state.s11n.storeException(2,e); 387 rc = state.sq3Codes.SQLITE_IOERR_DELETE; 388 } 389 wTimeEnd(); 390 return rc; 391 }, 392 xFileSize: async function(fid/*sqlite3_file pointer*/){ 393 mTimeStart('xFileSize'); 394 const fh = __openFiles[fid]; 395 let rc; 396 wTimeStart('xFileSize'); 397 try{ 398 rc = await (await getSyncHandle(fh)).getSize(); 399 state.s11n.serialize(Number(rc)); 400 rc = 0; 401 }catch(e){ 402 state.s11n.storeException(2,e); 403 rc = state.sq3Codes.SQLITE_IOERR; 404 } 405 wTimeEnd(); 406 storeAndNotify('xFileSize', rc); 407 mTimeEnd(); 408 }, 409 xLock: async function(fid/*sqlite3_file pointer*/, 410 lockType/*SQLITE_LOCK_...*/){ 411 mTimeStart('xLock'); 412 const fh = __openFiles[fid]; 413 let rc = 0; 414 if( !fh.syncHandle ){ 415 wTimeStart('xLock'); 416 try { await getSyncHandle(fh) } 417 catch(e){ 418 state.s11n.storeException(1,e); 419 rc = state.sq3Codes.SQLITE_IOERR_LOCK; 420 } 421 wTimeEnd(); 422 } 423 storeAndNotify('xLock',rc); 424 mTimeEnd(); 425 }, 426 xOpen: async function(fid/*sqlite3_file pointer*/, filename, 427 flags/*SQLITE_OPEN_...*/){ 428 const opName = 'xOpen'; 429 mTimeStart(opName); 430 const deleteOnClose = (state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags); 431 const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags); 432 wTimeStart('xOpen'); 433 try{ 434 let hDir, filenamePart; 435 try { 436 [hDir, filenamePart] = await getDirForFilename(filename, !!create); 437 }catch(e){ 438 state.s11n.storeException(1,e); 439 storeAndNotify(opName, state.sq3Codes.SQLITE_NOTFOUND); 440 mTimeEnd(); 441 wTimeEnd(); 442 return; 443 } 444 const hFile = await hDir.getFileHandle(filenamePart, {create}); 445 /** 446 wa-sqlite, at this point, grabs a SyncAccessHandle and 447 assigns it to the syncHandle prop of the file state 448 object, but only for certain cases and it's unclear why it 449 places that limitation on it. 450 */ 451 wTimeEnd(); 452 __openFiles[fid] = Object.assign(Object.create(null),{ 453 filenameAbs: filename, 454 filenamePart: filenamePart, 455 dirHandle: hDir, 456 fileHandle: hFile, 457 sabView: state.sabFileBufView, 458 readOnly: create 459 ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags), 460 deleteOnClose: deleteOnClose 461 }); 462 storeAndNotify(opName, 0); 463 }catch(e){ 464 wTimeEnd(); 465 error(opName,e); 466 state.s11n.storeException(1,e); 467 storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR); 468 } 469 mTimeEnd(); 470 }, 471 xRead: async function(fid/*sqlite3_file pointer*/,n,offset64){ 472 mTimeStart('xRead'); 473 let rc = 0, nRead; 474 const fh = __openFiles[fid]; 475 try{ 476 wTimeStart('xRead'); 477 nRead = (await getSyncHandle(fh)).read( 478 fh.sabView.subarray(0, n), 479 {at: Number(offset64)} 480 ); 481 wTimeEnd(); 482 if(nRead < n){/* Zero-fill remaining bytes */ 483 fh.sabView.fill(0, nRead, n); 484 rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ; 485 } 486 }catch(e){ 487 if(undefined===nRead) wTimeEnd(); 488 error("xRead() failed",e,fh); 489 state.s11n.storeException(1,e); 490 rc = state.sq3Codes.SQLITE_IOERR_READ; 491 } 492 storeAndNotify('xRead',rc); 493 mTimeEnd(); 494 }, 495 xSync: async function(fid/*sqlite3_file pointer*/,flags/*ignored*/){ 496 mTimeStart('xSync'); 497 const fh = __openFiles[fid]; 498 let rc = 0; 499 if(!fh.readOnly && fh.syncHandle){ 500 try { 501 wTimeStart('xSync'); 502 await fh.syncHandle.flush(); 503 }catch(e){ 504 state.s11n.storeException(2,e); 505 rc = state.sq3Codes.SQLITE_IOERR_FSYNC; 506 } 507 wTimeEnd(); 508 } 509 storeAndNotify('xSync',rc); 510 mTimeEnd(); 511 }, 512 xTruncate: async function(fid/*sqlite3_file pointer*/,size){ 513 mTimeStart('xTruncate'); 514 let rc = 0; 515 const fh = __openFiles[fid]; 516 wTimeStart('xTruncate'); 517 try{ 518 affirmNotRO('xTruncate', fh); 519 await (await getSyncHandle(fh)).truncate(size); 520 }catch(e){ 521 error("xTruncate():",e,fh); 522 state.s11n.storeException(2,e); 523 rc = state.sq3Codes.SQLITE_IOERR_TRUNCATE; 524 } 525 wTimeEnd(); 526 storeAndNotify('xTruncate',rc); 527 mTimeEnd(); 528 }, 529 xUnlock: async function(fid/*sqlite3_file pointer*/, 530 lockType/*SQLITE_LOCK_...*/){ 531 mTimeStart('xUnlock'); 532 let rc = 0; 533 const fh = __openFiles[fid]; 534 if( state.sq3Codes.SQLITE_LOCK_NONE===lockType 535 && fh.syncHandle ){ 536 wTimeStart('xUnlock'); 537 try { await closeSyncHandle(fh) } 538 catch(e){ 539 state.s11n.storeException(1,e); 540 rc = state.sq3Codes.SQLITE_IOERR_UNLOCK; 541 } 542 wTimeEnd(); 543 } 544 storeAndNotify('xUnlock',rc); 545 mTimeEnd(); 546 }, 547 xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){ 548 mTimeStart('xWrite'); 549 let rc; 550 wTimeStart('xWrite'); 551 try{ 552 const fh = __openFiles[fid]; 553 affirmNotRO('xWrite', fh); 554 rc = ( 555 n === (await getSyncHandle(fh)) 556 .write(fh.sabView.subarray(0, n), 557 {at: Number(offset64)}) 558 ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE; 559 }catch(e){ 560 error("xWrite():",e,fh); 561 state.s11n.storeException(1,e); 562 rc = state.sq3Codes.SQLITE_IOERR_WRITE; 563 } 564 wTimeEnd(); 565 storeAndNotify('xWrite',rc); 566 mTimeEnd(); 567 } 568}/*vfsAsyncImpls*/; 569 570const initS11n = ()=>{ 571 /** 572 ACHTUNG: this code is 100% duplicated in the other half of this 573 proxy! The documentation is maintained in the "synchronous half". 574 */ 575 if(state.s11n) return state.s11n; 576 const textDecoder = new TextDecoder(), 577 textEncoder = new TextEncoder('utf-8'), 578 viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize), 579 viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize); 580 state.s11n = Object.create(null); 581 const TypeIds = Object.create(null); 582 TypeIds.number = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' }; 583 TypeIds.bigint = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' }; 584 TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' }; 585 TypeIds.string = { id: 4 }; 586 const getTypeId = (v)=>( 587 TypeIds[typeof v] 588 || toss("Maintenance required: this value type cannot be serialized.",v) 589 ); 590 const getTypeIdById = (tid)=>{ 591 switch(tid){ 592 case TypeIds.number.id: return TypeIds.number; 593 case TypeIds.bigint.id: return TypeIds.bigint; 594 case TypeIds.boolean.id: return TypeIds.boolean; 595 case TypeIds.string.id: return TypeIds.string; 596 default: toss("Invalid type ID:",tid); 597 } 598 }; 599 state.s11n.deserialize = function(){ 600 ++metrics.s11n.deserialize.count; 601 const t = performance.now(); 602 const argc = viewU8[0]; 603 const rc = argc ? [] : null; 604 if(argc){ 605 const typeIds = []; 606 let offset = 1, i, n, v; 607 for(i = 0; i < argc; ++i, ++offset){ 608 typeIds.push(getTypeIdById(viewU8[offset])); 609 } 610 for(i = 0; i < argc; ++i){ 611 const t = typeIds[i]; 612 if(t.getter){ 613 v = viewDV[t.getter](offset, state.littleEndian); 614 offset += t.size; 615 }else{/*String*/ 616 n = viewDV.getInt32(offset, state.littleEndian); 617 offset += 4; 618 v = textDecoder.decode(viewU8.slice(offset, offset+n)); 619 offset += n; 620 } 621 rc.push(v); 622 } 623 } 624 //log("deserialize:",argc, rc); 625 metrics.s11n.deserialize.time += performance.now() - t; 626 return rc; 627 }; 628 state.s11n.serialize = function(...args){ 629 const t = performance.now(); 630 ++metrics.s11n.serialize.count; 631 if(args.length){ 632 //log("serialize():",args); 633 const typeIds = []; 634 let i = 0, offset = 1; 635 viewU8[0] = args.length & 0xff /* header = # of args */; 636 for(; i < args.length; ++i, ++offset){ 637 /* Write the TypeIds.id value into the next args.length 638 bytes. */ 639 typeIds.push(getTypeId(args[i])); 640 viewU8[offset] = typeIds[i].id; 641 } 642 for(i = 0; i < args.length; ++i) { 643 /* Deserialize the following bytes based on their 644 corresponding TypeIds.id from the header. */ 645 const t = typeIds[i]; 646 if(t.setter){ 647 viewDV[t.setter](offset, args[i], state.littleEndian); 648 offset += t.size; 649 }else{/*String*/ 650 const s = textEncoder.encode(args[i]); 651 viewDV.setInt32(offset, s.byteLength, state.littleEndian); 652 offset += 4; 653 viewU8.set(s, offset); 654 offset += s.byteLength; 655 } 656 } 657 //log("serialize() result:",viewU8.slice(0,offset)); 658 }else{ 659 viewU8[0] = 0; 660 } 661 metrics.s11n.serialize.time += performance.now() - t; 662 }; 663 664 state.s11n.storeException = state.asyncS11nExceptions 665 ? ((priority,e)=>{ 666 if(priority<=state.asyncS11nExceptions){ 667 state.s11n.serialize([e.name,': ',e.message].join('')); 668 } 669 }) 670 : ()=>{}; 671 672 return state.s11n; 673}/*initS11n()*/; 674 675const waitLoop = async function f(){ 676 const opHandlers = Object.create(null); 677 for(let k of Object.keys(state.opIds)){ 678 const vi = vfsAsyncImpls[k]; 679 if(!vi) continue; 680 const o = Object.create(null); 681 opHandlers[state.opIds[k]] = o; 682 o.key = k; 683 o.f = vi; 684 } 685 /** 686 waitTime is how long (ms) to wait for each Atomics.wait(). 687 We need to wake up periodically to give the thread a chance 688 to do other things. 689 */ 690 const waitTime = 1000; 691 while(!flagAsyncShutdown){ 692 try { 693 if('timed-out'===Atomics.wait( 694 state.sabOPView, state.opIds.whichOp, 0, waitTime 695 )){ 696 continue; 697 } 698 const opId = Atomics.load(state.sabOPView, state.opIds.whichOp); 699 Atomics.store(state.sabOPView, state.opIds.whichOp, 0); 700 const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId); 701 const args = state.s11n.deserialize() || []; 702 state.s11n.serialize(/* clear s11n to keep the caller from 703 confusing this with an exception string 704 written by the upcoming operation */); 705 //warn("waitLoop() whichOp =",opId, hnd, args); 706 if(hnd.f) await hnd.f(...args); 707 else error("Missing callback for opId",opId); 708 }catch(e){ 709 error('in waitLoop():',e); 710 } 711 } 712}; 713 714navigator.storage.getDirectory().then(function(d){ 715 const wMsg = (type)=>postMessage({type}); 716 state.rootDir = d; 717 self.onmessage = function({data}){ 718 switch(data.type){ 719 case 'opfs-async-init':{ 720 /* Receive shared state from synchronous partner */ 721 const opt = data.args; 722 state.littleEndian = opt.littleEndian; 723 state.asyncS11nExceptions = opt.asyncS11nExceptions; 724 state.verbose = opt.verbose ?? 2; 725 state.fileBufferSize = opt.fileBufferSize; 726 state.sabS11nOffset = opt.sabS11nOffset; 727 state.sabS11nSize = opt.sabS11nSize; 728 state.sabOP = opt.sabOP; 729 state.sabOPView = new Int32Array(state.sabOP); 730 state.sabIO = opt.sabIO; 731 state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize); 732 state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize); 733 state.opIds = opt.opIds; 734 state.sq3Codes = opt.sq3Codes; 735 Object.keys(vfsAsyncImpls).forEach((k)=>{ 736 if(!Number.isFinite(state.opIds[k])){ 737 toss("Maintenance required: missing state.opIds[",k,"]"); 738 } 739 }); 740 initS11n(); 741 metrics.reset(); 742 log("init state",state); 743 wMsg('opfs-async-inited'); 744 waitLoop(); 745 break; 746 } 747 case 'opfs-async-restart': 748 if(flagAsyncShutdown){ 749 warn("Restarting after opfs-async-shutdown. Might or might not work."); 750 flagAsyncShutdown = false; 751 waitLoop(); 752 } 753 break; 754 case 'opfs-async-metrics': 755 metrics.dump(); 756 break; 757 } 758 }; 759 wMsg('opfs-async-loaded'); 760}).catch((e)=>error("error initializing OPFS asyncer:",e)); 761