1/* 2 2022-09-18 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 This file holds the synchronous half of an sqlite3_vfs 14 implementation which proxies, in a synchronous fashion, the 15 asynchronous Origin-Private FileSystem (OPFS) APIs using a second 16 Worker, implemented in sqlite3-opfs-async-proxy.js. This file is 17 intended to be appended to the main sqlite3 JS deliverable somewhere 18 after sqlite3-api-oo1.js and before sqlite3-api-cleanup.js. 19*/ 20'use strict'; 21self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ 22/** 23 installOpfsVfs() returns a Promise which, on success, installs an 24 sqlite3_vfs named "opfs", suitable for use with all sqlite3 APIs 25 which accept a VFS. It is intended to be called via 26 sqlite3ApiBootstrap.initializersAsync or an equivalent mechanism. 27 28 The installed VFS uses the Origin-Private FileSystem API for 29 all file storage. On error it is rejected with an exception 30 explaining the problem. Reasons for rejection include, but are 31 not limited to: 32 33 - The counterpart Worker (see below) could not be loaded. 34 35 - The environment does not support OPFS. That includes when 36 this function is called from the main window thread. 37 38 Significant notes and limitations: 39 40 - As of this writing, OPFS is still very much in flux and only 41 available in bleeding-edge versions of Chrome (v102+, noting that 42 that number will increase as the OPFS API matures). 43 44 - The OPFS features used here are only available in dedicated Worker 45 threads. This file tries to detect that case, resulting in a 46 rejected Promise if those features do not seem to be available. 47 48 - It requires the SharedArrayBuffer and Atomics classes, and the 49 former is only available if the HTTP server emits the so-called 50 COOP and COEP response headers. These features are required for 51 proxying OPFS's synchronous API via the synchronous interface 52 required by the sqlite3_vfs API. 53 54 - This function may only be called a single time. When called, this 55 function removes itself from the sqlite3 object. 56 57 All arguments to this function are for internal/development purposes 58 only. They do not constitute a public API and may change at any 59 time. 60 61 The argument may optionally be a plain object with the following 62 configuration options: 63 64 - proxyUri: as described above 65 66 - verbose (=2): an integer 0-3. 0 disables all logging, 1 enables 67 logging of errors. 2 enables logging of warnings and errors. 3 68 additionally enables debugging info. 69 70 - sanityChecks (=false): if true, some basic sanity tests are 71 run on the OPFS VFS API after it's initialized, before the 72 returned Promise resolves. 73 74 On success, the Promise resolves to the top-most sqlite3 namespace 75 object and that object gets a new object installed in its 76 `opfs` property, containing several OPFS-specific utilities. 77*/ 78const installOpfsVfs = function callee(options){ 79 if(!self.SharedArrayBuffer || 80 !self.Atomics || 81 !self.FileSystemHandle || 82 !self.FileSystemDirectoryHandle || 83 !self.FileSystemFileHandle || 84 !self.FileSystemFileHandle.prototype.createSyncAccessHandle || 85 !navigator.storage.getDirectory){ 86 return Promise.reject( 87 new Error("This environment does not have OPFS support.") 88 ); 89 } 90 if(!options || 'object'!==typeof options){ 91 options = Object.create(null); 92 } 93 const urlParams = new URL(self.location.href).searchParams; 94 if(undefined===options.verbose){ 95 options.verbose = urlParams.has('opfs-verbose') ? 3 : 2; 96 } 97 if(undefined===options.sanityChecks){ 98 options.sanityChecks = urlParams.has('opfs-sanity-check'); 99 } 100 if(undefined===options.proxyUri){ 101 options.proxyUri = callee.defaultProxyUri; 102 } 103 104 if('function' === typeof options.proxyUri){ 105 options.proxyUri = options.proxyUri(); 106 } 107 const thePromise = new Promise(function(promiseResolve, promiseReject_){ 108 const loggers = { 109 0:console.error.bind(console), 110 1:console.warn.bind(console), 111 2:console.log.bind(console) 112 }; 113 const logImpl = (level,...args)=>{ 114 if(options.verbose>level) loggers[level]("OPFS syncer:",...args); 115 }; 116 const log = (...args)=>logImpl(2, ...args); 117 const warn = (...args)=>logImpl(1, ...args); 118 const error = (...args)=>logImpl(0, ...args); 119 const toss = function(...args){throw new Error(args.join(' '))}; 120 const capi = sqlite3.capi; 121 const wasm = sqlite3.wasm; 122 const sqlite3_vfs = capi.sqlite3_vfs; 123 const sqlite3_file = capi.sqlite3_file; 124 const sqlite3_io_methods = capi.sqlite3_io_methods; 125 /** 126 Generic utilities for working with OPFS. This will get filled out 127 by the Promise setup and, on success, installed as sqlite3.opfs. 128 */ 129 const opfsUtil = Object.create(null); 130 /** 131 Not part of the public API. Solely for internal/development 132 use. 133 */ 134 opfsUtil.metrics = { 135 dump: function(){ 136 let k, n = 0, t = 0, w = 0; 137 for(k in state.opIds){ 138 const m = metrics[k]; 139 n += m.count; 140 t += m.time; 141 w += m.wait; 142 m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0; 143 m.avgWait = (m.count && m.wait) ? (m.wait / m.count) : 0; 144 } 145 console.log(self.location.href, 146 "metrics for",self.location.href,":",metrics, 147 "\nTotal of",n,"op(s) for",t, 148 "ms (incl. "+w+" ms of waiting on the async side)"); 149 console.log("Serialization metrics:",metrics.s11n); 150 W.postMessage({type:'opfs-async-metrics'}); 151 }, 152 reset: function(){ 153 let k; 154 const r = (m)=>(m.count = m.time = m.wait = 0); 155 for(k in state.opIds){ 156 r(metrics[k] = Object.create(null)); 157 } 158 let s = metrics.s11n = Object.create(null); 159 s = s.serialize = Object.create(null); 160 s.count = s.time = 0; 161 s = metrics.s11n.deserialize = Object.create(null); 162 s.count = s.time = 0; 163 } 164 }/*metrics*/; 165 const promiseReject = function(err){ 166 opfsVfs.dispose(); 167 return promiseReject_(err); 168 }; 169 const W = new Worker(options.proxyUri); 170 W._originalOnError = W.onerror /* will be restored later */; 171 W.onerror = function(err){ 172 // The error object doesn't contain any useful info when the 173 // failure is, e.g., that the remote script is 404. 174 error("Error initializing OPFS asyncer:",err); 175 promiseReject(new Error("Loading OPFS async Worker failed for unknown reasons.")); 176 }; 177 const pDVfs = capi.sqlite3_vfs_find(null)/*pointer to default VFS*/; 178 const dVfs = pDVfs 179 ? new sqlite3_vfs(pDVfs) 180 : null /* dVfs will be null when sqlite3 is built with 181 SQLITE_OS_OTHER. Though we cannot currently handle 182 that case, the hope is to eventually be able to. */; 183 const opfsVfs = new sqlite3_vfs(); 184 const opfsIoMethods = new sqlite3_io_methods(); 185 opfsVfs.$iVersion = 2/*yes, two*/; 186 opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof; 187 opfsVfs.$mxPathname = 1024/*sure, why not?*/; 188 opfsVfs.$zName = wasm.allocCString("opfs"); 189 // All C-side memory of opfsVfs is zeroed out, but just to be explicit: 190 opfsVfs.$xDlOpen = opfsVfs.$xDlError = opfsVfs.$xDlSym = opfsVfs.$xDlClose = null; 191 opfsVfs.ondispose = [ 192 '$zName', opfsVfs.$zName, 193 'cleanup default VFS wrapper', ()=>(dVfs ? dVfs.dispose() : null), 194 'cleanup opfsIoMethods', ()=>opfsIoMethods.dispose() 195 ]; 196 /** 197 Pedantic sidebar about opfsVfs.ondispose: the entries in that array 198 are items to clean up when opfsVfs.dispose() is called, but in this 199 environment it will never be called. The VFS instance simply 200 hangs around until the WASM module instance is cleaned up. We 201 "could" _hypothetically_ clean it up by "importing" an 202 sqlite3_os_end() impl into the wasm build, but the shutdown order 203 of the wasm engine and the JS one are undefined so there is no 204 guaranty that the opfsVfs instance would be available in one 205 environment or the other when sqlite3_os_end() is called (_if_ it 206 gets called at all in a wasm build, which is undefined). 207 */ 208 /** 209 State which we send to the async-api Worker or share with it. 210 This object must initially contain only cloneable or sharable 211 objects. After the worker's "inited" message arrives, other types 212 of data may be added to it. 213 214 For purposes of Atomics.wait() and Atomics.notify(), we use a 215 SharedArrayBuffer with one slot reserved for each of the API 216 proxy's methods. The sync side of the API uses Atomics.wait() 217 on the corresponding slot and the async side uses 218 Atomics.notify() on that slot. 219 220 The approach of using a single SAB to serialize comms for all 221 instances might(?) lead to deadlock situations in multi-db 222 cases. We should probably have one SAB here with a single slot 223 for locking a per-file initialization step and then allocate a 224 separate SAB like the above one for each file. That will 225 require a bit of acrobatics but should be feasible. The most 226 problematic part is that xOpen() would have to use 227 postMessage() to communicate its SharedArrayBuffer, and mixing 228 that approach with Atomics.wait/notify() gets a bit messy. 229 */ 230 const state = Object.create(null); 231 state.verbose = options.verbose; 232 state.littleEndian = (()=>{ 233 const buffer = new ArrayBuffer(2); 234 new DataView(buffer).setInt16(0, 256, true /* ==>littleEndian */); 235 // Int16Array uses the platform's endianness. 236 return new Int16Array(buffer)[0] === 256; 237 })(); 238 /** 239 Whether the async counterpart should log exceptions to 240 the serialization channel. That produces a great deal of 241 noise for seemingly innocuous things like xAccess() checks 242 for missing files, so this option may have one of 3 values: 243 244 0 = no exception logging 245 246 1 = only log exceptions for "significant" ops like xOpen(), 247 xRead(), and xWrite(). 248 249 2 = log all exceptions. 250 */ 251 state.asyncS11nExceptions = 1; 252 /* Size of file I/O buffer block. 64k = max sqlite3 page size, and 253 xRead/xWrite() will never deal in blocks larger than that. */ 254 state.fileBufferSize = 1024 * 64; 255 state.sabS11nOffset = state.fileBufferSize; 256 /** 257 The size of the block in our SAB for serializing arguments and 258 result values. Needs to be large enough to hold serialized 259 values of any of the proxied APIs. Filenames are the largest 260 part but are limited to opfsVfs.$mxPathname bytes. 261 */ 262 state.sabS11nSize = opfsVfs.$mxPathname * 2; 263 /** 264 The SAB used for all data I/O between the synchronous and 265 async halves (file i/o and arg/result s11n). 266 */ 267 state.sabIO = new SharedArrayBuffer( 268 state.fileBufferSize/* file i/o block */ 269 + state.sabS11nSize/* argument/result serialization block */ 270 ); 271 state.opIds = Object.create(null); 272 const metrics = Object.create(null); 273 { 274 /* Indexes for use in our SharedArrayBuffer... */ 275 let i = 0; 276 /* SAB slot used to communicate which operation is desired 277 between both workers. This worker writes to it and the other 278 listens for changes. */ 279 state.opIds.whichOp = i++; 280 /* Slot for storing return values. This worker listens to that 281 slot and the other worker writes to it. */ 282 state.opIds.rc = i++; 283 /* Each function gets an ID which this worker writes to 284 the whichOp slot. The async-api worker uses Atomic.wait() 285 on the whichOp slot to figure out which operation to run 286 next. */ 287 state.opIds.xAccess = i++; 288 state.opIds.xClose = i++; 289 state.opIds.xDelete = i++; 290 state.opIds.xDeleteNoWait = i++; 291 state.opIds.xFileControl = i++; 292 state.opIds.xFileSize = i++; 293 state.opIds.xLock = i++; 294 state.opIds.xOpen = i++; 295 state.opIds.xRead = i++; 296 state.opIds.xSleep = i++; 297 state.opIds.xSync = i++; 298 state.opIds.xTruncate = i++; 299 state.opIds.xUnlock = i++; 300 state.opIds.xWrite = i++; 301 state.opIds.mkdir = i++; 302 state.opIds['opfs-async-metrics'] = i++; 303 state.opIds['opfs-async-shutdown'] = i++; 304 /* The retry slot is used by the async part for wait-and-retry 305 semantics. Though we could hypothetically use the xSleep slot 306 for that, doing so might lead to undesired side effects. */ 307 state.opIds.retry = i++; 308 state.sabOP = new SharedArrayBuffer( 309 i * 4/* ==sizeof int32, noting that Atomics.wait() and friends 310 can only function on Int32Array views of an SAB. */); 311 opfsUtil.metrics.reset(); 312 } 313 /** 314 SQLITE_xxx constants to export to the async worker 315 counterpart... 316 */ 317 state.sq3Codes = Object.create(null); 318 [ 319 'SQLITE_ERROR', 'SQLITE_IOERR', 320 'SQLITE_NOTFOUND', 'SQLITE_MISUSE', 321 'SQLITE_IOERR_READ', 'SQLITE_IOERR_SHORT_READ', 322 'SQLITE_IOERR_WRITE', 'SQLITE_IOERR_FSYNC', 323 'SQLITE_IOERR_TRUNCATE', 'SQLITE_IOERR_DELETE', 324 'SQLITE_IOERR_ACCESS', 'SQLITE_IOERR_CLOSE', 325 'SQLITE_IOERR_DELETE', 326 'SQLITE_LOCK_NONE', 327 'SQLITE_LOCK_SHARED', 328 'SQLITE_LOCK_RESERVED', 329 'SQLITE_LOCK_PENDING', 330 'SQLITE_LOCK_EXCLUSIVE', 331 'SQLITE_OPEN_CREATE', 'SQLITE_OPEN_DELETEONCLOSE', 332 'SQLITE_OPEN_READONLY' 333 ].forEach((k)=>{ 334 if(undefined === (state.sq3Codes[k] = capi[k])){ 335 toss("Maintenance required: not found:",k); 336 } 337 }); 338 339 /** 340 Runs the given operation (by name) in the async worker 341 counterpart, waits for its response, and returns the result 342 which the async worker writes to SAB[state.opIds.rc]. The 343 2nd and subsequent arguments must be the aruguments for the 344 async op. 345 */ 346 const opRun = (op,...args)=>{ 347 const opNdx = state.opIds[op] || toss("Invalid op ID:",op); 348 state.s11n.serialize(...args); 349 Atomics.store(state.sabOPView, state.opIds.rc, -1); 350 Atomics.store(state.sabOPView, state.opIds.whichOp, opNdx); 351 Atomics.notify(state.sabOPView, state.opIds.whichOp) 352 /* async thread will take over here */; 353 const t = performance.now(); 354 Atomics.wait(state.sabOPView, state.opIds.rc, -1) 355 /* When this wait() call returns, the async half will have 356 completed the operation and reported its results. */; 357 const rc = Atomics.load(state.sabOPView, state.opIds.rc); 358 metrics[op].wait += performance.now() - t; 359 if(rc && state.asyncS11nExceptions){ 360 const err = state.s11n.deserialize(); 361 if(err) error(op+"() async error:",...err); 362 } 363 return rc; 364 }; 365 366 const initS11n = ()=>{ 367 /** 368 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 369 ACHTUNG: this code is 100% duplicated in the other half of 370 this proxy! The documentation is maintained in the 371 "synchronous half". 372 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 373 374 This proxy de/serializes cross-thread function arguments and 375 output-pointer values via the state.sabIO SharedArrayBuffer, 376 using the region defined by (state.sabS11nOffset, 377 state.sabS11nOffset]. Only one dataset is recorded at a time. 378 379 This is not a general-purpose format. It only supports the 380 range of operations, and data sizes, needed by the 381 sqlite3_vfs and sqlite3_io_methods operations. Serialized 382 data are transient and this serialization algorithm may 383 change at any time. 384 385 The data format can be succinctly summarized as: 386 387 Nt...Td...D 388 389 Where: 390 391 - N = number of entries (1 byte) 392 393 - t = type ID of first argument (1 byte) 394 395 - ...T = type IDs of the 2nd and subsequent arguments (1 byte 396 each). 397 398 - d = raw bytes of first argument (per-type size). 399 400 - ...D = raw bytes of the 2nd and subsequent arguments (per-type 401 size). 402 403 All types except strings have fixed sizes. Strings are stored 404 using their TextEncoder/TextDecoder representations. It would 405 arguably make more sense to store them as Int16Arrays of 406 their JS character values, but how best/fastest to get that 407 in and out of string form is an open point. Initial 408 experimentation with that approach did not gain us any speed. 409 410 Historical note: this impl was initially about 1% this size by 411 using using JSON.stringify/parse(), but using fit-to-purpose 412 serialization saves considerable runtime. 413 */ 414 if(state.s11n) return state.s11n; 415 const textDecoder = new TextDecoder(), 416 textEncoder = new TextEncoder('utf-8'), 417 viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize), 418 viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize); 419 state.s11n = Object.create(null); 420 /* Only arguments and return values of these types may be 421 serialized. This covers the whole range of types needed by the 422 sqlite3_vfs API. */ 423 const TypeIds = Object.create(null); 424 TypeIds.number = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' }; 425 TypeIds.bigint = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' }; 426 TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' }; 427 TypeIds.string = { id: 4 }; 428 429 const getTypeId = (v)=>( 430 TypeIds[typeof v] 431 || toss("Maintenance required: this value type cannot be serialized.",v) 432 ); 433 const getTypeIdById = (tid)=>{ 434 switch(tid){ 435 case TypeIds.number.id: return TypeIds.number; 436 case TypeIds.bigint.id: return TypeIds.bigint; 437 case TypeIds.boolean.id: return TypeIds.boolean; 438 case TypeIds.string.id: return TypeIds.string; 439 default: toss("Invalid type ID:",tid); 440 } 441 }; 442 443 /** 444 Returns an array of the deserialized state stored by the most 445 recent serialize() operation (from from this thread or the 446 counterpart thread), or null if the serialization buffer is empty. 447 */ 448 state.s11n.deserialize = function(){ 449 ++metrics.s11n.deserialize.count; 450 const t = performance.now(); 451 const argc = viewU8[0]; 452 const rc = argc ? [] : null; 453 if(argc){ 454 const typeIds = []; 455 let offset = 1, i, n, v; 456 for(i = 0; i < argc; ++i, ++offset){ 457 typeIds.push(getTypeIdById(viewU8[offset])); 458 } 459 for(i = 0; i < argc; ++i){ 460 const t = typeIds[i]; 461 if(t.getter){ 462 v = viewDV[t.getter](offset, state.littleEndian); 463 offset += t.size; 464 }else{/*String*/ 465 n = viewDV.getInt32(offset, state.littleEndian); 466 offset += 4; 467 v = textDecoder.decode(viewU8.slice(offset, offset+n)); 468 offset += n; 469 } 470 rc.push(v); 471 } 472 } 473 //log("deserialize:",argc, rc); 474 metrics.s11n.deserialize.time += performance.now() - t; 475 return rc; 476 }; 477 478 /** 479 Serializes all arguments to the shared buffer for consumption 480 by the counterpart thread. 481 482 This routine is only intended for serializing OPFS VFS 483 arguments and (in at least one special case) result values, 484 and the buffer is sized to be able to comfortably handle 485 those. 486 487 If passed no arguments then it zeroes out the serialization 488 state. 489 */ 490 state.s11n.serialize = function(...args){ 491 const t = performance.now(); 492 ++metrics.s11n.serialize.count; 493 if(args.length){ 494 //log("serialize():",args); 495 const typeIds = []; 496 let i = 0, offset = 1; 497 viewU8[0] = args.length & 0xff /* header = # of args */; 498 for(; i < args.length; ++i, ++offset){ 499 /* Write the TypeIds.id value into the next args.length 500 bytes. */ 501 typeIds.push(getTypeId(args[i])); 502 viewU8[offset] = typeIds[i].id; 503 } 504 for(i = 0; i < args.length; ++i) { 505 /* Deserialize the following bytes based on their 506 corresponding TypeIds.id from the header. */ 507 const t = typeIds[i]; 508 if(t.setter){ 509 viewDV[t.setter](offset, args[i], state.littleEndian); 510 offset += t.size; 511 }else{/*String*/ 512 const s = textEncoder.encode(args[i]); 513 viewDV.setInt32(offset, s.byteLength, state.littleEndian); 514 offset += 4; 515 viewU8.set(s, offset); 516 offset += s.byteLength; 517 } 518 } 519 //log("serialize() result:",viewU8.slice(0,offset)); 520 }else{ 521 viewU8[0] = 0; 522 } 523 metrics.s11n.serialize.time += performance.now() - t; 524 }; 525 return state.s11n; 526 }/*initS11n()*/; 527 528 /** 529 Generates a random ASCII string len characters long, intended for 530 use as a temporary file name. 531 */ 532 const randomFilename = function f(len=16){ 533 if(!f._chars){ 534 f._chars = "abcdefghijklmnopqrstuvwxyz"+ 535 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+ 536 "012346789"; 537 f._n = f._chars.length; 538 } 539 const a = []; 540 let i = 0; 541 for( ; i < len; ++i){ 542 const ndx = Math.random() * (f._n * 64) % f._n | 0; 543 a[i] = f._chars[ndx]; 544 } 545 return a.join(''); 546 }; 547 548 /** 549 Map of sqlite3_file pointers to objects constructed by xOpen(). 550 */ 551 const __openFiles = Object.create(null); 552 553 /** 554 Installs a StructBinder-bound function pointer member of the 555 given name and function in the given StructType target object. 556 It creates a WASM proxy for the given function and arranges for 557 that proxy to be cleaned up when tgt.dispose() is called. Throws 558 on the slightest hint of error (e.g. tgt is-not-a StructType, 559 name does not map to a struct-bound member, etc.). 560 561 Returns a proxy for this function which is bound to tgt and takes 562 2 args (name,func). That function returns the same thing, 563 permitting calls to be chained. 564 565 If called with only 1 arg, it has no side effects but returns a 566 func with the same signature as described above. 567 */ 568 const installMethod = function callee(tgt, name, func){ 569 if(!(tgt instanceof sqlite3.StructBinder.StructType)){ 570 toss("Usage error: target object is-not-a StructType."); 571 } 572 if(1===arguments.length){ 573 return (n,f)=>callee(tgt,n,f); 574 } 575 if(!callee.argcProxy){ 576 callee.argcProxy = function(func,sig){ 577 return function(...args){ 578 if(func.length!==arguments.length){ 579 toss("Argument mismatch. Native signature is:",sig); 580 } 581 return func.apply(this, args); 582 } 583 }; 584 callee.removeFuncList = function(){ 585 if(this.ondispose.__removeFuncList){ 586 this.ondispose.__removeFuncList.forEach( 587 (v,ndx)=>{ 588 if('number'===typeof v){ 589 try{wasm.uninstallFunction(v)} 590 catch(e){/*ignore*/} 591 } 592 /* else it's a descriptive label for the next number in 593 the list. */ 594 } 595 ); 596 delete this.ondispose.__removeFuncList; 597 } 598 }; 599 }/*static init*/ 600 const sigN = tgt.memberSignature(name); 601 if(sigN.length<2){ 602 toss("Member",name," is not a function pointer. Signature =",sigN); 603 } 604 const memKey = tgt.memberKey(name); 605 const fProxy = 0 606 /** This middle-man proxy is only for use during development, to 607 confirm that we always pass the proper number of 608 arguments. We know that the C-level code will always use the 609 correct argument count. */ 610 ? callee.argcProxy(func, sigN) 611 : func; 612 const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true)); 613 tgt[memKey] = pFunc; 614 if(!tgt.ondispose) tgt.ondispose = []; 615 if(!tgt.ondispose.__removeFuncList){ 616 tgt.ondispose.push('ondispose.__removeFuncList handler', 617 callee.removeFuncList); 618 tgt.ondispose.__removeFuncList = []; 619 } 620 tgt.ondispose.__removeFuncList.push(memKey, pFunc); 621 return (n,f)=>callee(tgt, n, f); 622 }/*installMethod*/; 623 624 const opTimer = Object.create(null); 625 opTimer.op = undefined; 626 opTimer.start = undefined; 627 const mTimeStart = (op)=>{ 628 opTimer.start = performance.now(); 629 opTimer.op = op; 630 ++metrics[op].count; 631 }; 632 const mTimeEnd = ()=>( 633 metrics[opTimer.op].time += performance.now() - opTimer.start 634 ); 635 636 /** 637 Impls for the sqlite3_io_methods methods. Maintenance reminder: 638 members are in alphabetical order to simplify finding them. 639 */ 640 const ioSyncWrappers = { 641 xCheckReservedLock: function(pFile,pOut){ 642 /** 643 As of late 2022, only a single lock can be held on an OPFS 644 file. We have no way of checking whether any _other_ db 645 connection has a lock except by trying to obtain and (on 646 success) release a sync-handle for it, but doing so would 647 involve an inherent race condition. For the time being, 648 pending a better solution, we simply report whether the 649 given pFile instance has a lock. 650 */ 651 const f = __openFiles[pFile]; 652 wasm.setMemValue(pOut, f.lockMode ? 1 : 0, 'i32'); 653 return 0; 654 }, 655 xClose: function(pFile){ 656 mTimeStart('xClose'); 657 let rc = 0; 658 const f = __openFiles[pFile]; 659 if(f){ 660 delete __openFiles[pFile]; 661 rc = opRun('xClose', pFile); 662 if(f.sq3File) f.sq3File.dispose(); 663 } 664 mTimeEnd(); 665 return rc; 666 }, 667 xDeviceCharacteristics: function(pFile){ 668 //debug("xDeviceCharacteristics(",pFile,")"); 669 return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN; 670 }, 671 xFileControl: function(pFile, opId, pArg){ 672 mTimeStart('xFileControl'); 673 const rc = (capi.SQLITE_FCNTL_SYNC===opId) 674 ? opRun('xSync', pFile, 0) 675 : capi.SQLITE_NOTFOUND; 676 mTimeEnd(); 677 return rc; 678 }, 679 xFileSize: function(pFile,pSz64){ 680 mTimeStart('xFileSize'); 681 const rc = opRun('xFileSize', pFile); 682 if(0==rc){ 683 const sz = state.s11n.deserialize()[0]; 684 wasm.setMemValue(pSz64, sz, 'i64'); 685 } 686 mTimeEnd(); 687 return rc; 688 }, 689 xLock: function(pFile,lockType){ 690 mTimeStart('xLock'); 691 const f = __openFiles[pFile]; 692 let rc = 0; 693 if( capi.SQLITE_LOCK_NONE === f.lockType ) { 694 rc = opRun('xLock', pFile, lockType); 695 if( 0===rc ) f.lockType = lockType; 696 }else{ 697 f.lockType = lockType; 698 } 699 mTimeEnd(); 700 return rc; 701 }, 702 xRead: function(pFile,pDest,n,offset64){ 703 mTimeStart('xRead'); 704 const f = __openFiles[pFile]; 705 let rc; 706 try { 707 rc = opRun('xRead',pFile, n, Number(offset64)); 708 if(0===rc || capi.SQLITE_IOERR_SHORT_READ===rc){ 709 /** 710 Results get written to the SharedArrayBuffer f.sabView. 711 Because the heap is _not_ a SharedArrayBuffer, we have 712 to copy the results. TypedArray.set() seems to be the 713 fastest way to copy this. */ 714 wasm.heap8u().set(f.sabView.subarray(0, n), pDest); 715 } 716 }catch(e){ 717 error("xRead(",arguments,") failed:",e,f); 718 rc = capi.SQLITE_IOERR_READ; 719 } 720 mTimeEnd(); 721 return rc; 722 }, 723 xSync: function(pFile,flags){ 724 ++metrics.xSync.count; 725 return 0; // impl'd in xFileControl() 726 }, 727 xTruncate: function(pFile,sz64){ 728 mTimeStart('xTruncate'); 729 const rc = opRun('xTruncate', pFile, Number(sz64)); 730 mTimeEnd(); 731 return rc; 732 }, 733 xUnlock: function(pFile,lockType){ 734 mTimeStart('xUnlock'); 735 const f = __openFiles[pFile]; 736 let rc = 0; 737 if( capi.SQLITE_LOCK_NONE === lockType 738 && f.lockType ){ 739 rc = opRun('xUnlock', pFile, lockType); 740 } 741 if( 0===rc ) f.lockType = lockType; 742 mTimeEnd(); 743 return rc; 744 }, 745 xWrite: function(pFile,pSrc,n,offset64){ 746 mTimeStart('xWrite'); 747 const f = __openFiles[pFile]; 748 let rc; 749 try { 750 f.sabView.set(wasm.heap8u().subarray(pSrc, pSrc+n)); 751 rc = opRun('xWrite', pFile, n, Number(offset64)); 752 }catch(e){ 753 error("xWrite(",arguments,") failed:",e,f); 754 rc = capi.SQLITE_IOERR_WRITE; 755 } 756 mTimeEnd(); 757 return rc; 758 } 759 }/*ioSyncWrappers*/; 760 761 /** 762 Impls for the sqlite3_vfs methods. Maintenance reminder: members 763 are in alphabetical order to simplify finding them. 764 */ 765 const vfsSyncWrappers = { 766 xAccess: function(pVfs,zName,flags,pOut){ 767 mTimeStart('xAccess'); 768 const rc = opRun('xAccess', wasm.cstringToJs(zName)); 769 wasm.setMemValue( pOut, (rc ? 0 : 1), 'i32' ); 770 mTimeEnd(); 771 return 0; 772 }, 773 xCurrentTime: function(pVfs,pOut){ 774 /* If it turns out that we need to adjust for timezone, see: 775 https://stackoverflow.com/a/11760121/1458521 */ 776 wasm.setMemValue(pOut, 2440587.5 + (new Date().getTime()/86400000), 777 'double'); 778 return 0; 779 }, 780 xCurrentTimeInt64: function(pVfs,pOut){ 781 // TODO: confirm that this calculation is correct 782 wasm.setMemValue(pOut, (2440587.5 * 86400000) + new Date().getTime(), 783 'i64'); 784 return 0; 785 }, 786 xDelete: function(pVfs, zName, doSyncDir){ 787 mTimeStart('xDelete'); 788 opRun('xDelete', wasm.cstringToJs(zName), doSyncDir, false); 789 /* We're ignoring errors because we cannot yet differentiate 790 between harmless and non-harmless failures. */ 791 mTimeEnd(); 792 return 0; 793 }, 794 xFullPathname: function(pVfs,zName,nOut,pOut){ 795 /* Until/unless we have some notion of "current dir" 796 in OPFS, simply copy zName to pOut... */ 797 const i = wasm.cstrncpy(pOut, zName, nOut); 798 return i<nOut ? 0 : capi.SQLITE_CANTOPEN 799 /*CANTOPEN is required by the docs but SQLITE_RANGE would be a closer match*/; 800 }, 801 xGetLastError: function(pVfs,nOut,pOut){ 802 /* TODO: store exception.message values from the async 803 partner in a dedicated SharedArrayBuffer, noting that we'd have 804 to encode them... TextEncoder can do that for us. */ 805 warn("OPFS xGetLastError() has nothing sensible to return."); 806 return 0; 807 }, 808 //xSleep is optionally defined below 809 xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){ 810 mTimeStart('xOpen'); 811 if(0===zName){ 812 zName = randomFilename(); 813 }else if('number'===typeof zName){ 814 zName = wasm.cstringToJs(zName); 815 } 816 const fh = Object.create(null); 817 fh.fid = pFile; 818 fh.filename = zName; 819 fh.sab = new SharedArrayBuffer(state.fileBufferSize); 820 fh.flags = flags; 821 const rc = opRun('xOpen', pFile, zName, flags); 822 if(!rc){ 823 /* Recall that sqlite3_vfs::xClose() will be called, even on 824 error, unless pFile->pMethods is NULL. */ 825 if(fh.readOnly){ 826 wasm.setMemValue(pOutFlags, capi.SQLITE_OPEN_READONLY, 'i32'); 827 } 828 __openFiles[pFile] = fh; 829 fh.sabView = state.sabFileBufView; 830 fh.sq3File = new sqlite3_file(pFile); 831 fh.sq3File.$pMethods = opfsIoMethods.pointer; 832 fh.lockType = capi.SQLITE_LOCK_NONE; 833 } 834 mTimeEnd(); 835 return rc; 836 }/*xOpen()*/ 837 }/*vfsSyncWrappers*/; 838 839 if(dVfs){ 840 opfsVfs.$xRandomness = dVfs.$xRandomness; 841 opfsVfs.$xSleep = dVfs.$xSleep; 842 } 843 if(!opfsVfs.$xRandomness){ 844 /* If the default VFS has no xRandomness(), add a basic JS impl... */ 845 vfsSyncWrappers.xRandomness = function(pVfs, nOut, pOut){ 846 const heap = wasm.heap8u(); 847 let i = 0; 848 for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF; 849 return i; 850 }; 851 } 852 if(!opfsVfs.$xSleep){ 853 /* If we can inherit an xSleep() impl from the default VFS then 854 assume it's sane and use it, otherwise install a JS-based 855 one. */ 856 vfsSyncWrappers.xSleep = function(pVfs,ms){ 857 Atomics.wait(state.sabOPView, state.opIds.xSleep, 0, ms); 858 return 0; 859 }; 860 } 861 862 /* Install the vfs/io_methods into their C-level shared instances... */ 863 for(let k of Object.keys(ioSyncWrappers)){ 864 installMethod(opfsIoMethods, k, ioSyncWrappers[k]); 865 } 866 for(let k of Object.keys(vfsSyncWrappers)){ 867 installMethod(opfsVfs, k, vfsSyncWrappers[k]); 868 } 869 870 /** 871 Syncronously deletes the given OPFS filesystem entry, ignoring 872 any errors. As this environment has no notion of "current 873 directory", the given name must be an absolute path. If the 2nd 874 argument is truthy, deletion is recursive (use with caution!). 875 876 Returns true if the deletion succeeded and false if it fails, 877 but cannot report the nature of the failure. 878 */ 879 opfsUtil.deleteEntry = function(fsEntryName,recursive=false){ 880 mTimeStart('xDelete'); 881 const rc = opRun('xDelete', fsEntryName, 0, recursive); 882 mTimeEnd(); 883 return 0===rc; 884 }; 885 /** 886 Synchronously creates the given directory name, recursively, in 887 the OPFS filesystem. Returns true if it succeeds or the 888 directory already exists, else false. 889 */ 890 opfsUtil.mkdir = function(absDirName){ 891 mTimeStart('mkdir'); 892 const rc = opRun('mkdir', absDirName); 893 mTimeEnd(); 894 return 0===rc; 895 }; 896 /** 897 Synchronously checks whether the given OPFS filesystem exists, 898 returning true if it does, false if it doesn't. 899 */ 900 opfsUtil.entryExists = function(fsEntryName){ 901 return 0===opRun('xAccess', fsEntryName); 902 }; 903 904 /** 905 Generates a random ASCII string, intended for use as a 906 temporary file name. Its argument is the length of the string, 907 defaulting to 16. 908 */ 909 opfsUtil.randomFilename = randomFilename; 910 911 /** 912 Re-registers the OPFS VFS. This is intended only for odd use 913 cases which have to call sqlite3_shutdown() as part of their 914 initialization process, which will unregister the VFS 915 registered by installOpfsVfs(). If passed a truthy value, the 916 OPFS VFS is registered as the default VFS, else it is not made 917 the default. Returns the result of the the 918 sqlite3_vfs_register() call. 919 920 Design note: the problem of having to re-register things after 921 a shutdown/initialize pair is more general. How to best plug 922 that in to the library is unclear. In particular, we cannot 923 hook in to any C-side calls to sqlite3_initialize(), so we 924 cannot add an after-initialize callback mechanism. 925 */ 926 opfsUtil.registerVfs = (asDefault=false)=>{ 927 return wasm.exports.sqlite3_vfs_register( 928 opfsVfs.pointer, asDefault ? 1 : 0 929 ); 930 }; 931 932 /** 933 Only for test/development use. 934 */ 935 opfsUtil.debug = { 936 asyncShutdown: ()=>{ 937 warn("Shutting down OPFS async listener. OPFS will no longer work."); 938 opRun('opfs-async-shutdown'); 939 }, 940 asyncRestart: ()=>{ 941 warn("Attempting to restart OPFS async listener. Might work, might not."); 942 W.postMessage({type: 'opfs-async-restart'}); 943 } 944 }; 945 946 //TODO to support fiddle db upload: 947 //opfsUtil.createFile = function(absName, content=undefined){...} 948 949 if(sqlite3.oo1){ 950 opfsUtil.OpfsDb = function(...args){ 951 const opt = sqlite3.oo1.DB.dbCtorHelper.normalizeArgs(...args); 952 opt.vfs = opfsVfs.$zName; 953 sqlite3.oo1.DB.dbCtorHelper.call(this, opt); 954 }; 955 opfsUtil.OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype); 956 sqlite3.oo1.DB.dbCtorHelper.setVfsPostOpenSql( 957 opfsVfs.pointer, 958 [ 959 /* Truncate journal mode is faster than delete or wal for 960 this vfs, per speedtest1. */ 961 "pragma journal_mode=truncate;" 962 /* 963 This vfs benefits hugely from cache on moderate/large 964 speedtest1 --size 50 and --size 100 workloads. We currently 965 rely on setting a non-default cache size when building 966 sqlite3.wasm. If that policy changes, the cache can 967 be set here. 968 */ 969 //"pragma cache_size=-8388608;" 970 ].join('') 971 ); 972 } 973 974 /** 975 Potential TODOs: 976 977 - Expose one or both of the Worker objects via opfsUtil and 978 publish an interface for proxying the higher-level OPFS 979 features like getting a directory listing. 980 */ 981 const sanityCheck = function(){ 982 const scope = wasm.scopedAllocPush(); 983 const sq3File = new sqlite3_file(); 984 try{ 985 const fid = sq3File.pointer; 986 const openFlags = capi.SQLITE_OPEN_CREATE 987 | capi.SQLITE_OPEN_READWRITE 988 //| capi.SQLITE_OPEN_DELETEONCLOSE 989 | capi.SQLITE_OPEN_MAIN_DB; 990 const pOut = wasm.scopedAlloc(8); 991 const dbFile = "/sanity/check/file"+randomFilename(8); 992 const zDbFile = wasm.scopedAllocCString(dbFile); 993 let rc; 994 state.s11n.serialize("This is ä string."); 995 rc = state.s11n.deserialize(); 996 log("deserialize() says:",rc); 997 if("This is ä string."!==rc[0]) toss("String d13n error."); 998 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut); 999 rc = wasm.getMemValue(pOut,'i32'); 1000 log("xAccess(",dbFile,") exists ?=",rc); 1001 rc = vfsSyncWrappers.xOpen(opfsVfs.pointer, zDbFile, 1002 fid, openFlags, pOut); 1003 log("open rc =",rc,"state.sabOPView[xOpen] =", 1004 state.sabOPView[state.opIds.xOpen]); 1005 if(0!==rc){ 1006 error("open failed with code",rc); 1007 return; 1008 } 1009 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut); 1010 rc = wasm.getMemValue(pOut,'i32'); 1011 if(!rc) toss("xAccess() failed to detect file."); 1012 rc = ioSyncWrappers.xSync(sq3File.pointer, 0); 1013 if(rc) toss('sync failed w/ rc',rc); 1014 rc = ioSyncWrappers.xTruncate(sq3File.pointer, 1024); 1015 if(rc) toss('truncate failed w/ rc',rc); 1016 wasm.setMemValue(pOut,0,'i64'); 1017 rc = ioSyncWrappers.xFileSize(sq3File.pointer, pOut); 1018 if(rc) toss('xFileSize failed w/ rc',rc); 1019 log("xFileSize says:",wasm.getMemValue(pOut, 'i64')); 1020 rc = ioSyncWrappers.xWrite(sq3File.pointer, zDbFile, 10, 1); 1021 if(rc) toss("xWrite() failed!"); 1022 const readBuf = wasm.scopedAlloc(16); 1023 rc = ioSyncWrappers.xRead(sq3File.pointer, readBuf, 6, 2); 1024 wasm.setMemValue(readBuf+6,0); 1025 let jRead = wasm.cstringToJs(readBuf); 1026 log("xRead() got:",jRead); 1027 if("sanity"!==jRead) toss("Unexpected xRead() value."); 1028 if(vfsSyncWrappers.xSleep){ 1029 log("xSleep()ing before close()ing..."); 1030 vfsSyncWrappers.xSleep(opfsVfs.pointer,2000); 1031 log("waking up from xSleep()"); 1032 } 1033 rc = ioSyncWrappers.xClose(fid); 1034 log("xClose rc =",rc,"sabOPView =",state.sabOPView); 1035 log("Deleting file:",dbFile); 1036 vfsSyncWrappers.xDelete(opfsVfs.pointer, zDbFile, 0x1234); 1037 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut); 1038 rc = wasm.getMemValue(pOut,'i32'); 1039 if(rc) toss("Expecting 0 from xAccess(",dbFile,") after xDelete()."); 1040 warn("End of OPFS sanity checks."); 1041 }finally{ 1042 sq3File.dispose(); 1043 wasm.scopedAllocPop(scope); 1044 } 1045 }/*sanityCheck()*/; 1046 1047 W.onmessage = function({data}){ 1048 //log("Worker.onmessage:",data); 1049 switch(data.type){ 1050 case 'opfs-async-loaded': 1051 /*Arrives as soon as the asyc proxy finishes loading. 1052 Pass our config and shared state on to the async worker.*/ 1053 W.postMessage({type: 'opfs-async-init',args: state}); 1054 break; 1055 case 'opfs-async-inited':{ 1056 /*Indicates that the async partner has received the 'init' 1057 and has finished initializing, so the real work can 1058 begin...*/ 1059 try { 1060 const rc = capi.sqlite3_vfs_register(opfsVfs.pointer, 0); 1061 if(rc){ 1062 toss("sqlite3_vfs_register(OPFS) failed with rc",rc); 1063 } 1064 if(opfsVfs.pointer !== capi.sqlite3_vfs_find("opfs")){ 1065 toss("BUG: sqlite3_vfs_find() failed for just-installed OPFS VFS"); 1066 } 1067 capi.sqlite3_vfs_register.addReference(opfsVfs, opfsIoMethods); 1068 state.sabOPView = new Int32Array(state.sabOP); 1069 state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize); 1070 state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize); 1071 initS11n(); 1072 if(options.sanityChecks){ 1073 warn("Running sanity checks because of opfs-sanity-check URL arg..."); 1074 sanityCheck(); 1075 } 1076 navigator.storage.getDirectory().then((d)=>{ 1077 W.onerror = W._originalOnError; 1078 delete W._originalOnError; 1079 sqlite3.opfs = opfsUtil; 1080 opfsUtil.rootDirectory = d; 1081 log("End of OPFS sqlite3_vfs setup.", opfsVfs); 1082 promiseResolve(sqlite3); 1083 }); 1084 }catch(e){ 1085 error(e); 1086 promiseReject(e); 1087 } 1088 break; 1089 } 1090 default: 1091 promiseReject(e); 1092 error("Unexpected message from the async worker:",data); 1093 break; 1094 }/*switch(data.type)*/ 1095 }/*W.onmessage()*/; 1096 })/*thePromise*/; 1097 return thePromise; 1098}/*installOpfsVfs()*/; 1099installOpfsVfs.defaultProxyUri = 1100 "sqlite3-opfs-async-proxy.js"; 1101self.sqlite3ApiBootstrap.initializersAsync.push(async (sqlite3)=>{ 1102 if(sqlite3.scriptInfo && !sqlite3.scriptInfo.isWorker){ 1103 return; 1104 } 1105 try{ 1106 let proxyJs = installOpfsVfs.defaultProxyUri; 1107 if(sqlite3.scriptInfo.sqlite3Dir){ 1108 installOpfsVfs.defaultProxyUri = 1109 sqlite3.scriptInfo.sqlite3Dir + proxyJs; 1110 //console.warn("installOpfsVfs.defaultProxyUri =",installOpfsVfs.defaultProxyUri); 1111 } 1112 return installOpfsVfs().catch((e)=>{ 1113 console.warn("Ignoring inability to install OPFS sqlite3_vfs:",e.message); 1114 }); 1115 }catch(e){ 1116 console.error("installOpfsVfs() exception:",e); 1117 throw e; 1118 } 1119}); 1120}/*sqlite3ApiBootstrap.initializers.push()*/); 1121