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