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