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