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 A proxy for closeSyncHandle() which is guaranteed to not throw. 219 220 This function is part of a lock/unlock step in functions which 221 require a sync access handle but may be called without xLock() 222 having been called first. Such calls need to release that 223 handle to avoid locking the file for all of time. This is an 224 _attempt_ at reducing cross-tab contention but it may prove 225 to be more of a problem than a solution and may need to be 226 removed. 227*/ 228const closeSyncHandleNoThrow = async (fh)=>{ 229 try{await closeSyncHandle(fh)} 230 catch(e){ 231 warn("closeSyncHandleNoThrow() ignoring:",e,fh); 232 } 233}; 234 235/** 236 Stores the given value at state.sabOPView[state.opIds.rc] and then 237 Atomics.notify()'s it. 238*/ 239const storeAndNotify = (opName, value)=>{ 240 log(opName+"() => notify(",value,")"); 241 Atomics.store(state.sabOPView, state.opIds.rc, value); 242 Atomics.notify(state.sabOPView, state.opIds.rc); 243}; 244 245/** 246 Throws if fh is a file-holding object which is flagged as read-only. 247*/ 248const affirmNotRO = function(opName,fh){ 249 if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs); 250}; 251const affirmLocked = function(opName,fh){ 252 //if(!fh.syncHandle) toss(opName+"(): File does not have a lock: "+fh.filenameAbs); 253 /** 254 Currently a no-op, as speedtest1 triggers xRead() without a 255 lock (that seems like a bug but it's currently uninvestigated). 256 This means, however, that some OPFS VFS routines may trigger 257 acquisition of a lock but never let it go until xUnlock() is 258 called (which it likely won't be if xLock() was not called). 259 */ 260}; 261 262/** 263 We track 2 different timers: the "metrics" timer records how much 264 time we spend performing work. The "wait" timer records how much 265 time we spend waiting on the underlying OPFS timer. See the calls 266 to mTimeStart(), mTimeEnd(), wTimeStart(), and wTimeEnd() 267 throughout this file to see how they're used. 268*/ 269const __mTimer = Object.create(null); 270__mTimer.op = undefined; 271__mTimer.start = undefined; 272const mTimeStart = (op)=>{ 273 __mTimer.start = performance.now(); 274 __mTimer.op = op; 275 //metrics[op] || toss("Maintenance required: missing metrics for",op); 276 ++metrics[op].count; 277}; 278const mTimeEnd = ()=>( 279 metrics[__mTimer.op].time += performance.now() - __mTimer.start 280); 281const __wTimer = Object.create(null); 282__wTimer.op = undefined; 283__wTimer.start = undefined; 284const wTimeStart = (op)=>{ 285 __wTimer.start = performance.now(); 286 __wTimer.op = op; 287 //metrics[op] || toss("Maintenance required: missing metrics for",op); 288}; 289const wTimeEnd = ()=>( 290 metrics[__wTimer.op].wait += performance.now() - __wTimer.start 291); 292 293/** 294 Gets set to true by the 'opfs-async-shutdown' command to quit the 295 wait loop. This is only intended for debugging purposes: we cannot 296 inspect this file's state while the tight waitLoop() is running and 297 need a way to stop that loop for introspection purposes. 298*/ 299let flagAsyncShutdown = false; 300 301 302/** 303 Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods 304 methods, as well as helpers like mkdir(). Maintenance reminder: 305 members are in alphabetical order to simplify finding them. 306*/ 307const vfsAsyncImpls = { 308 'opfs-async-metrics': async ()=>{ 309 mTimeStart('opfs-async-metrics'); 310 metrics.dump(); 311 storeAndNotify('opfs-async-metrics', 0); 312 mTimeEnd(); 313 }, 314 'opfs-async-shutdown': async ()=>{ 315 flagAsyncShutdown = true; 316 storeAndNotify('opfs-async-shutdown', 0); 317 }, 318 mkdir: async (dirname)=>{ 319 mTimeStart('mkdir'); 320 let rc = 0; 321 wTimeStart('mkdir'); 322 try { 323 await getDirForFilename(dirname+"/filepart", true); 324 }catch(e){ 325 state.s11n.storeException(2,e); 326 rc = state.sq3Codes.SQLITE_IOERR; 327 }finally{ 328 wTimeEnd(); 329 } 330 storeAndNotify('mkdir', rc); 331 mTimeEnd(); 332 }, 333 xAccess: async (filename)=>{ 334 mTimeStart('xAccess'); 335 /* OPFS cannot support the full range of xAccess() queries sqlite3 336 calls for. We can essentially just tell if the file is 337 accessible, but if it is it's automatically writable (unless 338 it's locked, which we cannot(?) know without trying to open 339 it). OPFS does not have the notion of read-only. 340 341 The return semantics of this function differ from sqlite3's 342 xAccess semantics because we are limited in what we can 343 communicate back to our synchronous communication partner: 0 = 344 accessible, non-0 means not accessible. 345 */ 346 let rc = 0; 347 wTimeStart('xAccess'); 348 try{ 349 const [dh, fn] = await getDirForFilename(filename); 350 await dh.getFileHandle(fn); 351 }catch(e){ 352 state.s11n.storeException(2,e); 353 rc = state.sq3Codes.SQLITE_IOERR; 354 }finally{ 355 wTimeEnd(); 356 } 357 storeAndNotify('xAccess', rc); 358 mTimeEnd(); 359 }, 360 xClose: async function(fid/*sqlite3_file pointer*/){ 361 const opName = 'xClose'; 362 mTimeStart(opName); 363 const fh = __openFiles[fid]; 364 let rc = 0; 365 wTimeStart('xClose'); 366 if(fh){ 367 delete __openFiles[fid]; 368 await closeSyncHandle(fh); 369 if(fh.deleteOnClose){ 370 try{ await fh.dirHandle.removeEntry(fh.filenamePart) } 371 catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) } 372 } 373 }else{ 374 state.s11n.serialize(); 375 rc = state.sq3Codes.SQLITE_NOTFOUND; 376 } 377 wTimeEnd(); 378 storeAndNotify(opName, rc); 379 mTimeEnd(); 380 }, 381 xDelete: async function(...args){ 382 mTimeStart('xDelete'); 383 const rc = await vfsAsyncImpls.xDeleteNoWait(...args); 384 storeAndNotify('xDelete', rc); 385 mTimeEnd(); 386 }, 387 xDeleteNoWait: async function(filename, syncDir = 0, recursive = false){ 388 /* The syncDir flag is, for purposes of the VFS API's semantics, 389 ignored here. However, if it has the value 0x1234 then: after 390 deleting the given file, recursively try to delete any empty 391 directories left behind in its wake (ignoring any errors and 392 stopping at the first failure). 393 394 That said: we don't know for sure that removeEntry() fails if 395 the dir is not empty because the API is not documented. It has, 396 however, a "recursive" flag which defaults to false, so 397 presumably it will fail if the dir is not empty and that flag 398 is false. 399 */ 400 let rc = 0; 401 wTimeStart('xDelete'); 402 try { 403 while(filename){ 404 const [hDir, filenamePart] = await getDirForFilename(filename, false); 405 if(!filenamePart) break; 406 await hDir.removeEntry(filenamePart, {recursive}); 407 if(0x1234 !== syncDir) break; 408 recursive = false; 409 filename = getResolvedPath(filename, true); 410 filename.pop(); 411 filename = filename.join('/'); 412 } 413 }catch(e){ 414 state.s11n.storeException(2,e); 415 rc = state.sq3Codes.SQLITE_IOERR_DELETE; 416 } 417 wTimeEnd(); 418 return rc; 419 }, 420 xFileSize: async function(fid/*sqlite3_file pointer*/){ 421 mTimeStart('xFileSize'); 422 const fh = __openFiles[fid]; 423 let rc; 424 const hadLock = !!fh.syncHandle; 425 wTimeStart('xFileSize'); 426 try{ 427 affirmLocked('xFileSize',fh); 428 rc = await (await getSyncHandle(fh)).getSize(); 429 state.s11n.serialize(Number(rc)); 430 rc = 0; 431 }catch(e){ 432 state.s11n.storeException(2,e); 433 rc = state.sq3Codes.SQLITE_IOERR; 434 } 435 if(!hadLock) closeSyncHandleNoThrow(fh); 436 wTimeEnd(); 437 storeAndNotify('xFileSize', rc); 438 mTimeEnd(); 439 }, 440 xLock: async function(fid/*sqlite3_file pointer*/, 441 lockType/*SQLITE_LOCK_...*/){ 442 mTimeStart('xLock'); 443 const fh = __openFiles[fid]; 444 let rc = 0; 445 if( !fh.syncHandle ){ 446 wTimeStart('xLock'); 447 try { await getSyncHandle(fh) } 448 catch(e){ 449 state.s11n.storeException(1,e); 450 rc = state.sq3Codes.SQLITE_IOERR_LOCK; 451 } 452 wTimeEnd(); 453 } 454 storeAndNotify('xLock',rc); 455 mTimeEnd(); 456 }, 457 xOpen: async function(fid/*sqlite3_file pointer*/, filename, 458 flags/*SQLITE_OPEN_...*/){ 459 const opName = 'xOpen'; 460 mTimeStart(opName); 461 const deleteOnClose = (state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags); 462 const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags); 463 wTimeStart('xOpen'); 464 try{ 465 let hDir, filenamePart; 466 try { 467 [hDir, filenamePart] = await getDirForFilename(filename, !!create); 468 }catch(e){ 469 state.s11n.storeException(1,e); 470 storeAndNotify(opName, state.sq3Codes.SQLITE_NOTFOUND); 471 mTimeEnd(); 472 wTimeEnd(); 473 return; 474 } 475 const hFile = await hDir.getFileHandle(filenamePart, {create}); 476 /** 477 wa-sqlite, at this point, grabs a SyncAccessHandle and 478 assigns it to the syncHandle prop of the file state 479 object, but only for certain cases and it's unclear why it 480 places that limitation on it. 481 */ 482 wTimeEnd(); 483 __openFiles[fid] = Object.assign(Object.create(null),{ 484 filenameAbs: filename, 485 filenamePart: filenamePart, 486 dirHandle: hDir, 487 fileHandle: hFile, 488 sabView: state.sabFileBufView, 489 readOnly: create 490 ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags), 491 deleteOnClose: deleteOnClose 492 }); 493 storeAndNotify(opName, 0); 494 }catch(e){ 495 wTimeEnd(); 496 error(opName,e); 497 state.s11n.storeException(1,e); 498 storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR); 499 } 500 mTimeEnd(); 501 }, 502 xRead: async function(fid/*sqlite3_file pointer*/,n,offset64){ 503 mTimeStart('xRead'); 504 let rc = 0, nRead; 505 const fh = __openFiles[fid]; 506 const hadLock = !!fh.syncHandle; 507 try{ 508 affirmLocked('xRead',fh); 509 wTimeStart('xRead'); 510 nRead = (await getSyncHandle(fh)).read( 511 fh.sabView.subarray(0, n), 512 {at: Number(offset64)} 513 ); 514 wTimeEnd(); 515 if(nRead < n){/* Zero-fill remaining bytes */ 516 fh.sabView.fill(0, nRead, n); 517 rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ; 518 } 519 }catch(e){ 520 if(undefined===nRead) wTimeEnd(); 521 error("xRead() failed",e,fh); 522 state.s11n.storeException(1,e); 523 rc = state.sq3Codes.SQLITE_IOERR_READ; 524 } 525 if(!hadLock) closeSyncHandleNoThrow(fh); 526 storeAndNotify('xRead',rc); 527 mTimeEnd(); 528 }, 529 xSync: async function(fid/*sqlite3_file pointer*/,flags/*ignored*/){ 530 mTimeStart('xSync'); 531 const fh = __openFiles[fid]; 532 let rc = 0; 533 if(!fh.readOnly && fh.syncHandle){ 534 try { 535 wTimeStart('xSync'); 536 await fh.syncHandle.flush(); 537 }catch(e){ 538 state.s11n.storeException(2,e); 539 rc = state.sq3Codes.SQLITE_IOERR_FSYNC; 540 } 541 wTimeEnd(); 542 } 543 storeAndNotify('xSync',rc); 544 mTimeEnd(); 545 }, 546 xTruncate: async function(fid/*sqlite3_file pointer*/,size){ 547 mTimeStart('xTruncate'); 548 let rc = 0; 549 const fh = __openFiles[fid]; 550 const hadLock = !!fh.syncHandle; 551 wTimeStart('xTruncate'); 552 try{ 553 affirmLocked('xTruncate',fh); 554 affirmNotRO('xTruncate', fh); 555 await (await getSyncHandle(fh)).truncate(size); 556 }catch(e){ 557 error("xTruncate():",e,fh); 558 state.s11n.storeException(2,e); 559 rc = state.sq3Codes.SQLITE_IOERR_TRUNCATE; 560 } 561 if(!hadLock) closeSyncHandleNoThrow(fh); 562 wTimeEnd(); 563 storeAndNotify('xTruncate',rc); 564 mTimeEnd(); 565 }, 566 xUnlock: async function(fid/*sqlite3_file pointer*/, 567 lockType/*SQLITE_LOCK_...*/){ 568 mTimeStart('xUnlock'); 569 let rc = 0; 570 const fh = __openFiles[fid]; 571 if( state.sq3Codes.SQLITE_LOCK_NONE===lockType 572 && fh.syncHandle ){ 573 wTimeStart('xUnlock'); 574 try { await closeSyncHandle(fh) } 575 catch(e){ 576 state.s11n.storeException(1,e); 577 rc = state.sq3Codes.SQLITE_IOERR_UNLOCK; 578 } 579 wTimeEnd(); 580 } 581 storeAndNotify('xUnlock',rc); 582 mTimeEnd(); 583 }, 584 xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){ 585 mTimeStart('xWrite'); 586 let rc; 587 const fh = __openFiles[fid]; 588 const hadLock = !!fh.syncHandle; 589 wTimeStart('xWrite'); 590 try{ 591 affirmLocked('xWrite',fh); 592 affirmNotRO('xWrite', fh); 593 rc = ( 594 n === (await getSyncHandle(fh)) 595 .write(fh.sabView.subarray(0, n), 596 {at: Number(offset64)}) 597 ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE; 598 }catch(e){ 599 error("xWrite():",e,fh); 600 state.s11n.storeException(1,e); 601 rc = state.sq3Codes.SQLITE_IOERR_WRITE; 602 } 603 if(!hadLock) closeSyncHandleNoThrow(fh); 604 wTimeEnd(); 605 storeAndNotify('xWrite',rc); 606 mTimeEnd(); 607 } 608}/*vfsAsyncImpls*/; 609 610const initS11n = ()=>{ 611 /** 612 ACHTUNG: this code is 100% duplicated in the other half of this 613 proxy! The documentation is maintained in the "synchronous half". 614 */ 615 if(state.s11n) return state.s11n; 616 const textDecoder = new TextDecoder(), 617 textEncoder = new TextEncoder('utf-8'), 618 viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize), 619 viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize); 620 state.s11n = Object.create(null); 621 const TypeIds = Object.create(null); 622 TypeIds.number = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' }; 623 TypeIds.bigint = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' }; 624 TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' }; 625 TypeIds.string = { id: 4 }; 626 const getTypeId = (v)=>( 627 TypeIds[typeof v] 628 || toss("Maintenance required: this value type cannot be serialized.",v) 629 ); 630 const getTypeIdById = (tid)=>{ 631 switch(tid){ 632 case TypeIds.number.id: return TypeIds.number; 633 case TypeIds.bigint.id: return TypeIds.bigint; 634 case TypeIds.boolean.id: return TypeIds.boolean; 635 case TypeIds.string.id: return TypeIds.string; 636 default: toss("Invalid type ID:",tid); 637 } 638 }; 639 state.s11n.deserialize = function(){ 640 ++metrics.s11n.deserialize.count; 641 const t = performance.now(); 642 const argc = viewU8[0]; 643 const rc = argc ? [] : null; 644 if(argc){ 645 const typeIds = []; 646 let offset = 1, i, n, v; 647 for(i = 0; i < argc; ++i, ++offset){ 648 typeIds.push(getTypeIdById(viewU8[offset])); 649 } 650 for(i = 0; i < argc; ++i){ 651 const t = typeIds[i]; 652 if(t.getter){ 653 v = viewDV[t.getter](offset, state.littleEndian); 654 offset += t.size; 655 }else{/*String*/ 656 n = viewDV.getInt32(offset, state.littleEndian); 657 offset += 4; 658 v = textDecoder.decode(viewU8.slice(offset, offset+n)); 659 offset += n; 660 } 661 rc.push(v); 662 } 663 } 664 //log("deserialize:",argc, rc); 665 metrics.s11n.deserialize.time += performance.now() - t; 666 return rc; 667 }; 668 state.s11n.serialize = function(...args){ 669 const t = performance.now(); 670 ++metrics.s11n.serialize.count; 671 if(args.length){ 672 //log("serialize():",args); 673 const typeIds = []; 674 let i = 0, offset = 1; 675 viewU8[0] = args.length & 0xff /* header = # of args */; 676 for(; i < args.length; ++i, ++offset){ 677 /* Write the TypeIds.id value into the next args.length 678 bytes. */ 679 typeIds.push(getTypeId(args[i])); 680 viewU8[offset] = typeIds[i].id; 681 } 682 for(i = 0; i < args.length; ++i) { 683 /* Deserialize the following bytes based on their 684 corresponding TypeIds.id from the header. */ 685 const t = typeIds[i]; 686 if(t.setter){ 687 viewDV[t.setter](offset, args[i], state.littleEndian); 688 offset += t.size; 689 }else{/*String*/ 690 const s = textEncoder.encode(args[i]); 691 viewDV.setInt32(offset, s.byteLength, state.littleEndian); 692 offset += 4; 693 viewU8.set(s, offset); 694 offset += s.byteLength; 695 } 696 } 697 //log("serialize() result:",viewU8.slice(0,offset)); 698 }else{ 699 viewU8[0] = 0; 700 } 701 metrics.s11n.serialize.time += performance.now() - t; 702 }; 703 704 state.s11n.storeException = state.asyncS11nExceptions 705 ? ((priority,e)=>{ 706 if(priority<=state.asyncS11nExceptions){ 707 state.s11n.serialize([e.name,': ',e.message].join("")); 708 } 709 }) 710 : ()=>{}; 711 712 return state.s11n; 713}/*initS11n()*/; 714 715const waitLoop = async function f(){ 716 const opHandlers = Object.create(null); 717 for(let k of Object.keys(state.opIds)){ 718 const vi = vfsAsyncImpls[k]; 719 if(!vi) continue; 720 const o = Object.create(null); 721 opHandlers[state.opIds[k]] = o; 722 o.key = k; 723 o.f = vi; 724 } 725 /** 726 waitTime is how long (ms) to wait for each Atomics.wait(). 727 We need to wake up periodically to give the thread a chance 728 to do other things. 729 */ 730 const waitTime = 1000; 731 while(!flagAsyncShutdown){ 732 try { 733 if('timed-out'===Atomics.wait( 734 state.sabOPView, state.opIds.whichOp, 0, waitTime 735 )){ 736 continue; 737 } 738 const opId = Atomics.load(state.sabOPView, state.opIds.whichOp); 739 Atomics.store(state.sabOPView, state.opIds.whichOp, 0); 740 const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId); 741 const args = state.s11n.deserialize() || []; 742 state.s11n.serialize(/* clear s11n to keep the caller from 743 confusing this with an exception string 744 written by the upcoming operation */); 745 //warn("waitLoop() whichOp =",opId, hnd, args); 746 if(hnd.f) await hnd.f(...args); 747 else error("Missing callback for opId",opId); 748 }catch(e){ 749 error('in waitLoop():',e); 750 } 751 } 752}; 753 754navigator.storage.getDirectory().then(function(d){ 755 const wMsg = (type)=>postMessage({type}); 756 state.rootDir = d; 757 self.onmessage = function({data}){ 758 switch(data.type){ 759 case 'opfs-async-init':{ 760 /* Receive shared state from synchronous partner */ 761 const opt = data.args; 762 state.littleEndian = opt.littleEndian; 763 state.asyncS11nExceptions = opt.asyncS11nExceptions; 764 state.verbose = opt.verbose ?? 2; 765 state.fileBufferSize = opt.fileBufferSize; 766 state.sabS11nOffset = opt.sabS11nOffset; 767 state.sabS11nSize = opt.sabS11nSize; 768 state.sabOP = opt.sabOP; 769 state.sabOPView = new Int32Array(state.sabOP); 770 state.sabIO = opt.sabIO; 771 state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize); 772 state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize); 773 state.opIds = opt.opIds; 774 state.sq3Codes = opt.sq3Codes; 775 Object.keys(vfsAsyncImpls).forEach((k)=>{ 776 if(!Number.isFinite(state.opIds[k])){ 777 toss("Maintenance required: missing state.opIds[",k,"]"); 778 } 779 }); 780 initS11n(); 781 metrics.reset(); 782 log("init state",state); 783 wMsg('opfs-async-inited'); 784 waitLoop(); 785 break; 786 } 787 case 'opfs-async-restart': 788 if(flagAsyncShutdown){ 789 warn("Restarting after opfs-async-shutdown. Might or might not work."); 790 flagAsyncShutdown = false; 791 waitLoop(); 792 } 793 break; 794 case 'opfs-async-metrics': 795 metrics.dump(); 796 break; 797 } 798 }; 799 wMsg('opfs-async-loaded'); 800}).catch((e)=>error("error initializing OPFS asyncer:",e)); 801