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', 320 'SQLITE_IOERR', 321 'SQLITE_IOERR_ACCESS', 322 'SQLITE_IOERR_CLOSE', 323 'SQLITE_IOERR_DELETE', 324 'SQLITE_IOERR_FSYNC', 325 'SQLITE_IOERR_LOCK', 326 'SQLITE_IOERR_READ', 327 'SQLITE_IOERR_SHORT_READ', 328 'SQLITE_IOERR_TRUNCATE', 329 'SQLITE_IOERR_UNLOCK', 330 'SQLITE_IOERR_WRITE', 331 'SQLITE_LOCK_EXCLUSIVE', 332 'SQLITE_LOCK_NONE', 333 'SQLITE_LOCK_PENDING', 334 'SQLITE_LOCK_RESERVED', 335 'SQLITE_LOCK_SHARED', 336 'SQLITE_MISUSE', 337 'SQLITE_NOTFOUND', 338 'SQLITE_OPEN_CREATE', 339 'SQLITE_OPEN_DELETEONCLOSE', 340 'SQLITE_OPEN_READONLY' 341 ].forEach((k)=>{ 342 if(undefined === (state.sq3Codes[k] = capi[k])){ 343 toss("Maintenance required: not found:",k); 344 } 345 }); 346 347 /** 348 Runs the given operation (by name) in the async worker 349 counterpart, waits for its response, and returns the result 350 which the async worker writes to SAB[state.opIds.rc]. The 351 2nd and subsequent arguments must be the aruguments for the 352 async op. 353 */ 354 const opRun = (op,...args)=>{ 355 const opNdx = state.opIds[op] || toss("Invalid op ID:",op); 356 state.s11n.serialize(...args); 357 Atomics.store(state.sabOPView, state.opIds.rc, -1); 358 Atomics.store(state.sabOPView, state.opIds.whichOp, opNdx); 359 Atomics.notify(state.sabOPView, state.opIds.whichOp) 360 /* async thread will take over here */; 361 const t = performance.now(); 362 Atomics.wait(state.sabOPView, state.opIds.rc, -1) 363 /* When this wait() call returns, the async half will have 364 completed the operation and reported its results. */; 365 const rc = Atomics.load(state.sabOPView, state.opIds.rc); 366 metrics[op].wait += performance.now() - t; 367 if(rc && state.asyncS11nExceptions){ 368 const err = state.s11n.deserialize(); 369 if(err) error(op+"() async error:",...err); 370 } 371 return rc; 372 }; 373 374 const initS11n = ()=>{ 375 /** 376 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 377 ACHTUNG: this code is 100% duplicated in the other half of 378 this proxy! The documentation is maintained in the 379 "synchronous half". 380 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 381 382 This proxy de/serializes cross-thread function arguments and 383 output-pointer values via the state.sabIO SharedArrayBuffer, 384 using the region defined by (state.sabS11nOffset, 385 state.sabS11nOffset]. Only one dataset is recorded at a time. 386 387 This is not a general-purpose format. It only supports the 388 range of operations, and data sizes, needed by the 389 sqlite3_vfs and sqlite3_io_methods operations. Serialized 390 data are transient and this serialization algorithm may 391 change at any time. 392 393 The data format can be succinctly summarized as: 394 395 Nt...Td...D 396 397 Where: 398 399 - N = number of entries (1 byte) 400 401 - t = type ID of first argument (1 byte) 402 403 - ...T = type IDs of the 2nd and subsequent arguments (1 byte 404 each). 405 406 - d = raw bytes of first argument (per-type size). 407 408 - ...D = raw bytes of the 2nd and subsequent arguments (per-type 409 size). 410 411 All types except strings have fixed sizes. Strings are stored 412 using their TextEncoder/TextDecoder representations. It would 413 arguably make more sense to store them as Int16Arrays of 414 their JS character values, but how best/fastest to get that 415 in and out of string form is an open point. Initial 416 experimentation with that approach did not gain us any speed. 417 418 Historical note: this impl was initially about 1% this size by 419 using using JSON.stringify/parse(), but using fit-to-purpose 420 serialization saves considerable runtime. 421 */ 422 if(state.s11n) return state.s11n; 423 const textDecoder = new TextDecoder(), 424 textEncoder = new TextEncoder('utf-8'), 425 viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize), 426 viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize); 427 state.s11n = Object.create(null); 428 /* Only arguments and return values of these types may be 429 serialized. This covers the whole range of types needed by the 430 sqlite3_vfs API. */ 431 const TypeIds = Object.create(null); 432 TypeIds.number = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' }; 433 TypeIds.bigint = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' }; 434 TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' }; 435 TypeIds.string = { id: 4 }; 436 437 const getTypeId = (v)=>( 438 TypeIds[typeof v] 439 || toss("Maintenance required: this value type cannot be serialized.",v) 440 ); 441 const getTypeIdById = (tid)=>{ 442 switch(tid){ 443 case TypeIds.number.id: return TypeIds.number; 444 case TypeIds.bigint.id: return TypeIds.bigint; 445 case TypeIds.boolean.id: return TypeIds.boolean; 446 case TypeIds.string.id: return TypeIds.string; 447 default: toss("Invalid type ID:",tid); 448 } 449 }; 450 451 /** 452 Returns an array of the deserialized state stored by the most 453 recent serialize() operation (from from this thread or the 454 counterpart thread), or null if the serialization buffer is empty. 455 */ 456 state.s11n.deserialize = function(){ 457 ++metrics.s11n.deserialize.count; 458 const t = performance.now(); 459 const argc = viewU8[0]; 460 const rc = argc ? [] : null; 461 if(argc){ 462 const typeIds = []; 463 let offset = 1, i, n, v; 464 for(i = 0; i < argc; ++i, ++offset){ 465 typeIds.push(getTypeIdById(viewU8[offset])); 466 } 467 for(i = 0; i < argc; ++i){ 468 const t = typeIds[i]; 469 if(t.getter){ 470 v = viewDV[t.getter](offset, state.littleEndian); 471 offset += t.size; 472 }else{/*String*/ 473 n = viewDV.getInt32(offset, state.littleEndian); 474 offset += 4; 475 v = textDecoder.decode(viewU8.slice(offset, offset+n)); 476 offset += n; 477 } 478 rc.push(v); 479 } 480 } 481 //log("deserialize:",argc, rc); 482 metrics.s11n.deserialize.time += performance.now() - t; 483 return rc; 484 }; 485 486 /** 487 Serializes all arguments to the shared buffer for consumption 488 by the counterpart thread. 489 490 This routine is only intended for serializing OPFS VFS 491 arguments and (in at least one special case) result values, 492 and the buffer is sized to be able to comfortably handle 493 those. 494 495 If passed no arguments then it zeroes out the serialization 496 state. 497 */ 498 state.s11n.serialize = function(...args){ 499 const t = performance.now(); 500 ++metrics.s11n.serialize.count; 501 if(args.length){ 502 //log("serialize():",args); 503 const typeIds = []; 504 let i = 0, offset = 1; 505 viewU8[0] = args.length & 0xff /* header = # of args */; 506 for(; i < args.length; ++i, ++offset){ 507 /* Write the TypeIds.id value into the next args.length 508 bytes. */ 509 typeIds.push(getTypeId(args[i])); 510 viewU8[offset] = typeIds[i].id; 511 } 512 for(i = 0; i < args.length; ++i) { 513 /* Deserialize the following bytes based on their 514 corresponding TypeIds.id from the header. */ 515 const t = typeIds[i]; 516 if(t.setter){ 517 viewDV[t.setter](offset, args[i], state.littleEndian); 518 offset += t.size; 519 }else{/*String*/ 520 const s = textEncoder.encode(args[i]); 521 viewDV.setInt32(offset, s.byteLength, state.littleEndian); 522 offset += 4; 523 viewU8.set(s, offset); 524 offset += s.byteLength; 525 } 526 } 527 //log("serialize() result:",viewU8.slice(0,offset)); 528 }else{ 529 viewU8[0] = 0; 530 } 531 metrics.s11n.serialize.time += performance.now() - t; 532 }; 533 return state.s11n; 534 }/*initS11n()*/; 535 536 /** 537 Generates a random ASCII string len characters long, intended for 538 use as a temporary file name. 539 */ 540 const randomFilename = function f(len=16){ 541 if(!f._chars){ 542 f._chars = "abcdefghijklmnopqrstuvwxyz"+ 543 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+ 544 "012346789"; 545 f._n = f._chars.length; 546 } 547 const a = []; 548 let i = 0; 549 for( ; i < len; ++i){ 550 const ndx = Math.random() * (f._n * 64) % f._n | 0; 551 a[i] = f._chars[ndx]; 552 } 553 return a.join(''); 554 }; 555 556 /** 557 Map of sqlite3_file pointers to objects constructed by xOpen(). 558 */ 559 const __openFiles = Object.create(null); 560 561 /** 562 Installs a StructBinder-bound function pointer member of the 563 given name and function in the given StructType target object. 564 It creates a WASM proxy for the given function and arranges for 565 that proxy to be cleaned up when tgt.dispose() is called. Throws 566 on the slightest hint of error (e.g. tgt is-not-a StructType, 567 name does not map to a struct-bound member, etc.). 568 569 Returns a proxy for this function which is bound to tgt and takes 570 2 args (name,func). That function returns the same thing, 571 permitting calls to be chained. 572 573 If called with only 1 arg, it has no side effects but returns a 574 func with the same signature as described above. 575 */ 576 const installMethod = function callee(tgt, name, func){ 577 if(!(tgt instanceof sqlite3.StructBinder.StructType)){ 578 toss("Usage error: target object is-not-a StructType."); 579 } 580 if(1===arguments.length){ 581 return (n,f)=>callee(tgt,n,f); 582 } 583 if(!callee.argcProxy){ 584 callee.argcProxy = function(func,sig){ 585 return function(...args){ 586 if(func.length!==arguments.length){ 587 toss("Argument mismatch. Native signature is:",sig); 588 } 589 return func.apply(this, args); 590 } 591 }; 592 callee.removeFuncList = function(){ 593 if(this.ondispose.__removeFuncList){ 594 this.ondispose.__removeFuncList.forEach( 595 (v,ndx)=>{ 596 if('number'===typeof v){ 597 try{wasm.uninstallFunction(v)} 598 catch(e){/*ignore*/} 599 } 600 /* else it's a descriptive label for the next number in 601 the list. */ 602 } 603 ); 604 delete this.ondispose.__removeFuncList; 605 } 606 }; 607 }/*static init*/ 608 const sigN = tgt.memberSignature(name); 609 if(sigN.length<2){ 610 toss("Member",name," is not a function pointer. Signature =",sigN); 611 } 612 const memKey = tgt.memberKey(name); 613 const fProxy = 0 614 /** This middle-man proxy is only for use during development, to 615 confirm that we always pass the proper number of 616 arguments. We know that the C-level code will always use the 617 correct argument count. */ 618 ? callee.argcProxy(func, sigN) 619 : func; 620 const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true)); 621 tgt[memKey] = pFunc; 622 if(!tgt.ondispose) tgt.ondispose = []; 623 if(!tgt.ondispose.__removeFuncList){ 624 tgt.ondispose.push('ondispose.__removeFuncList handler', 625 callee.removeFuncList); 626 tgt.ondispose.__removeFuncList = []; 627 } 628 tgt.ondispose.__removeFuncList.push(memKey, pFunc); 629 return (n,f)=>callee(tgt, n, f); 630 }/*installMethod*/; 631 632 const opTimer = Object.create(null); 633 opTimer.op = undefined; 634 opTimer.start = undefined; 635 const mTimeStart = (op)=>{ 636 opTimer.start = performance.now(); 637 opTimer.op = op; 638 ++metrics[op].count; 639 }; 640 const mTimeEnd = ()=>( 641 metrics[opTimer.op].time += performance.now() - opTimer.start 642 ); 643 644 /** 645 Impls for the sqlite3_io_methods methods. Maintenance reminder: 646 members are in alphabetical order to simplify finding them. 647 */ 648 const ioSyncWrappers = { 649 xCheckReservedLock: function(pFile,pOut){ 650 /** 651 As of late 2022, only a single lock can be held on an OPFS 652 file. We have no way of checking whether any _other_ db 653 connection has a lock except by trying to obtain and (on 654 success) release a sync-handle for it, but doing so would 655 involve an inherent race condition. For the time being, 656 pending a better solution, we simply report whether the 657 given pFile instance has a lock. 658 */ 659 const f = __openFiles[pFile]; 660 wasm.setMemValue(pOut, f.lockMode ? 1 : 0, 'i32'); 661 return 0; 662 }, 663 xClose: function(pFile){ 664 mTimeStart('xClose'); 665 let rc = 0; 666 const f = __openFiles[pFile]; 667 if(f){ 668 delete __openFiles[pFile]; 669 rc = opRun('xClose', pFile); 670 if(f.sq3File) f.sq3File.dispose(); 671 } 672 mTimeEnd(); 673 return rc; 674 }, 675 xDeviceCharacteristics: function(pFile){ 676 //debug("xDeviceCharacteristics(",pFile,")"); 677 return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN; 678 }, 679 xFileControl: function(pFile, opId, pArg){ 680 mTimeStart('xFileControl'); 681 const rc = (capi.SQLITE_FCNTL_SYNC===opId) 682 ? opRun('xSync', pFile, 0) 683 : capi.SQLITE_NOTFOUND; 684 mTimeEnd(); 685 return rc; 686 }, 687 xFileSize: function(pFile,pSz64){ 688 mTimeStart('xFileSize'); 689 const rc = opRun('xFileSize', pFile); 690 if(0==rc){ 691 const sz = state.s11n.deserialize()[0]; 692 wasm.setMemValue(pSz64, sz, 'i64'); 693 } 694 mTimeEnd(); 695 return rc; 696 }, 697 xLock: function(pFile,lockType){ 698 mTimeStart('xLock'); 699 const f = __openFiles[pFile]; 700 let rc = 0; 701 if( capi.SQLITE_LOCK_NONE === f.lockType ) { 702 rc = opRun('xLock', pFile, lockType); 703 if( 0===rc ) f.lockType = lockType; 704 }else{ 705 f.lockType = lockType; 706 } 707 mTimeEnd(); 708 return rc; 709 }, 710 xRead: function(pFile,pDest,n,offset64){ 711 mTimeStart('xRead'); 712 const f = __openFiles[pFile]; 713 let rc; 714 try { 715 rc = opRun('xRead',pFile, n, Number(offset64)); 716 if(0===rc || capi.SQLITE_IOERR_SHORT_READ===rc){ 717 /** 718 Results get written to the SharedArrayBuffer f.sabView. 719 Because the heap is _not_ a SharedArrayBuffer, we have 720 to copy the results. TypedArray.set() seems to be the 721 fastest way to copy this. */ 722 wasm.heap8u().set(f.sabView.subarray(0, n), pDest); 723 } 724 }catch(e){ 725 error("xRead(",arguments,") failed:",e,f); 726 rc = capi.SQLITE_IOERR_READ; 727 } 728 mTimeEnd(); 729 return rc; 730 }, 731 xSync: function(pFile,flags){ 732 ++metrics.xSync.count; 733 return 0; // impl'd in xFileControl() 734 }, 735 xTruncate: function(pFile,sz64){ 736 mTimeStart('xTruncate'); 737 const rc = opRun('xTruncate', pFile, Number(sz64)); 738 mTimeEnd(); 739 return rc; 740 }, 741 xUnlock: function(pFile,lockType){ 742 mTimeStart('xUnlock'); 743 const f = __openFiles[pFile]; 744 let rc = 0; 745 if( capi.SQLITE_LOCK_NONE === lockType 746 && f.lockType ){ 747 rc = opRun('xUnlock', pFile, lockType); 748 } 749 if( 0===rc ) f.lockType = lockType; 750 mTimeEnd(); 751 return rc; 752 }, 753 xWrite: function(pFile,pSrc,n,offset64){ 754 mTimeStart('xWrite'); 755 const f = __openFiles[pFile]; 756 let rc; 757 try { 758 f.sabView.set(wasm.heap8u().subarray(pSrc, pSrc+n)); 759 rc = opRun('xWrite', pFile, n, Number(offset64)); 760 }catch(e){ 761 error("xWrite(",arguments,") failed:",e,f); 762 rc = capi.SQLITE_IOERR_WRITE; 763 } 764 mTimeEnd(); 765 return rc; 766 } 767 }/*ioSyncWrappers*/; 768 769 /** 770 Impls for the sqlite3_vfs methods. Maintenance reminder: members 771 are in alphabetical order to simplify finding them. 772 */ 773 const vfsSyncWrappers = { 774 xAccess: function(pVfs,zName,flags,pOut){ 775 mTimeStart('xAccess'); 776 const rc = opRun('xAccess', wasm.cstringToJs(zName)); 777 wasm.setMemValue( pOut, (rc ? 0 : 1), 'i32' ); 778 mTimeEnd(); 779 return 0; 780 }, 781 xCurrentTime: function(pVfs,pOut){ 782 /* If it turns out that we need to adjust for timezone, see: 783 https://stackoverflow.com/a/11760121/1458521 */ 784 wasm.setMemValue(pOut, 2440587.5 + (new Date().getTime()/86400000), 785 'double'); 786 return 0; 787 }, 788 xCurrentTimeInt64: function(pVfs,pOut){ 789 // TODO: confirm that this calculation is correct 790 wasm.setMemValue(pOut, (2440587.5 * 86400000) + new Date().getTime(), 791 'i64'); 792 return 0; 793 }, 794 xDelete: function(pVfs, zName, doSyncDir){ 795 mTimeStart('xDelete'); 796 opRun('xDelete', wasm.cstringToJs(zName), doSyncDir, false); 797 /* We're ignoring errors because we cannot yet differentiate 798 between harmless and non-harmless failures. */ 799 mTimeEnd(); 800 return 0; 801 }, 802 xFullPathname: function(pVfs,zName,nOut,pOut){ 803 /* Until/unless we have some notion of "current dir" 804 in OPFS, simply copy zName to pOut... */ 805 const i = wasm.cstrncpy(pOut, zName, nOut); 806 return i<nOut ? 0 : capi.SQLITE_CANTOPEN 807 /*CANTOPEN is required by the docs but SQLITE_RANGE would be a closer match*/; 808 }, 809 xGetLastError: function(pVfs,nOut,pOut){ 810 /* TODO: store exception.message values from the async 811 partner in a dedicated SharedArrayBuffer, noting that we'd have 812 to encode them... TextEncoder can do that for us. */ 813 warn("OPFS xGetLastError() has nothing sensible to return."); 814 return 0; 815 }, 816 //xSleep is optionally defined below 817 xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){ 818 mTimeStart('xOpen'); 819 if(0===zName){ 820 zName = randomFilename(); 821 }else if('number'===typeof zName){ 822 zName = wasm.cstringToJs(zName); 823 } 824 const fh = Object.create(null); 825 fh.fid = pFile; 826 fh.filename = zName; 827 fh.sab = new SharedArrayBuffer(state.fileBufferSize); 828 fh.flags = flags; 829 const rc = opRun('xOpen', pFile, zName, flags); 830 if(!rc){ 831 /* Recall that sqlite3_vfs::xClose() will be called, even on 832 error, unless pFile->pMethods is NULL. */ 833 if(fh.readOnly){ 834 wasm.setMemValue(pOutFlags, capi.SQLITE_OPEN_READONLY, 'i32'); 835 } 836 __openFiles[pFile] = fh; 837 fh.sabView = state.sabFileBufView; 838 fh.sq3File = new sqlite3_file(pFile); 839 fh.sq3File.$pMethods = opfsIoMethods.pointer; 840 fh.lockType = capi.SQLITE_LOCK_NONE; 841 } 842 mTimeEnd(); 843 return rc; 844 }/*xOpen()*/ 845 }/*vfsSyncWrappers*/; 846 847 if(dVfs){ 848 opfsVfs.$xRandomness = dVfs.$xRandomness; 849 opfsVfs.$xSleep = dVfs.$xSleep; 850 } 851 if(!opfsVfs.$xRandomness){ 852 /* If the default VFS has no xRandomness(), add a basic JS impl... */ 853 vfsSyncWrappers.xRandomness = function(pVfs, nOut, pOut){ 854 const heap = wasm.heap8u(); 855 let i = 0; 856 for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF; 857 return i; 858 }; 859 } 860 if(!opfsVfs.$xSleep){ 861 /* If we can inherit an xSleep() impl from the default VFS then 862 assume it's sane and use it, otherwise install a JS-based 863 one. */ 864 vfsSyncWrappers.xSleep = function(pVfs,ms){ 865 Atomics.wait(state.sabOPView, state.opIds.xSleep, 0, ms); 866 return 0; 867 }; 868 } 869 870 /* Install the vfs/io_methods into their C-level shared instances... */ 871 for(let k of Object.keys(ioSyncWrappers)){ 872 installMethod(opfsIoMethods, k, ioSyncWrappers[k]); 873 } 874 for(let k of Object.keys(vfsSyncWrappers)){ 875 installMethod(opfsVfs, k, vfsSyncWrappers[k]); 876 } 877 878 /** 879 Syncronously deletes the given OPFS filesystem entry, ignoring 880 any errors. As this environment has no notion of "current 881 directory", the given name must be an absolute path. If the 2nd 882 argument is truthy, deletion is recursive (use with caution!). 883 884 Returns true if the deletion succeeded and false if it fails, 885 but cannot report the nature of the failure. 886 */ 887 opfsUtil.deleteEntry = function(fsEntryName,recursive=false){ 888 mTimeStart('xDelete'); 889 const rc = opRun('xDelete', fsEntryName, 0, recursive); 890 mTimeEnd(); 891 return 0===rc; 892 }; 893 /** 894 Synchronously creates the given directory name, recursively, in 895 the OPFS filesystem. Returns true if it succeeds or the 896 directory already exists, else false. 897 */ 898 opfsUtil.mkdir = function(absDirName){ 899 mTimeStart('mkdir'); 900 const rc = opRun('mkdir', absDirName); 901 mTimeEnd(); 902 return 0===rc; 903 }; 904 /** 905 Synchronously checks whether the given OPFS filesystem exists, 906 returning true if it does, false if it doesn't. 907 */ 908 opfsUtil.entryExists = function(fsEntryName){ 909 return 0===opRun('xAccess', fsEntryName); 910 }; 911 912 /** 913 Generates a random ASCII string, intended for use as a 914 temporary file name. Its argument is the length of the string, 915 defaulting to 16. 916 */ 917 opfsUtil.randomFilename = randomFilename; 918 919 /** 920 Re-registers the OPFS VFS. This is intended only for odd use 921 cases which have to call sqlite3_shutdown() as part of their 922 initialization process, which will unregister the VFS 923 registered by installOpfsVfs(). If passed a truthy value, the 924 OPFS VFS is registered as the default VFS, else it is not made 925 the default. Returns the result of the the 926 sqlite3_vfs_register() call. 927 928 Design note: the problem of having to re-register things after 929 a shutdown/initialize pair is more general. How to best plug 930 that in to the library is unclear. In particular, we cannot 931 hook in to any C-side calls to sqlite3_initialize(), so we 932 cannot add an after-initialize callback mechanism. 933 */ 934 opfsUtil.registerVfs = (asDefault=false)=>{ 935 return wasm.exports.sqlite3_vfs_register( 936 opfsVfs.pointer, asDefault ? 1 : 0 937 ); 938 }; 939 940 /** 941 Only for test/development use. 942 */ 943 opfsUtil.debug = { 944 asyncShutdown: ()=>{ 945 warn("Shutting down OPFS async listener. OPFS will no longer work."); 946 opRun('opfs-async-shutdown'); 947 }, 948 asyncRestart: ()=>{ 949 warn("Attempting to restart OPFS async listener. Might work, might not."); 950 W.postMessage({type: 'opfs-async-restart'}); 951 } 952 }; 953 954 //TODO to support fiddle db upload: 955 //opfsUtil.createFile = function(absName, content=undefined){...} 956 957 if(sqlite3.oo1){ 958 opfsUtil.OpfsDb = function(...args){ 959 const opt = sqlite3.oo1.DB.dbCtorHelper.normalizeArgs(...args); 960 opt.vfs = opfsVfs.$zName; 961 sqlite3.oo1.DB.dbCtorHelper.call(this, opt); 962 }; 963 opfsUtil.OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype); 964 sqlite3.oo1.DB.dbCtorHelper.setVfsPostOpenSql( 965 opfsVfs.pointer, 966 [ 967 /* Truncate journal mode is faster than delete or wal for 968 this vfs, per speedtest1. */ 969 "pragma journal_mode=truncate;" 970 /* 971 This vfs benefits hugely from cache on moderate/large 972 speedtest1 --size 50 and --size 100 workloads. We currently 973 rely on setting a non-default cache size when building 974 sqlite3.wasm. If that policy changes, the cache can 975 be set here. 976 */ 977 //"pragma cache_size=-8388608;" 978 ].join('') 979 ); 980 } 981 982 /** 983 Potential TODOs: 984 985 - Expose one or both of the Worker objects via opfsUtil and 986 publish an interface for proxying the higher-level OPFS 987 features like getting a directory listing. 988 */ 989 const sanityCheck = function(){ 990 const scope = wasm.scopedAllocPush(); 991 const sq3File = new sqlite3_file(); 992 try{ 993 const fid = sq3File.pointer; 994 const openFlags = capi.SQLITE_OPEN_CREATE 995 | capi.SQLITE_OPEN_READWRITE 996 //| capi.SQLITE_OPEN_DELETEONCLOSE 997 | capi.SQLITE_OPEN_MAIN_DB; 998 const pOut = wasm.scopedAlloc(8); 999 const dbFile = "/sanity/check/file"+randomFilename(8); 1000 const zDbFile = wasm.scopedAllocCString(dbFile); 1001 let rc; 1002 state.s11n.serialize("This is ä string."); 1003 rc = state.s11n.deserialize(); 1004 log("deserialize() says:",rc); 1005 if("This is ä string."!==rc[0]) toss("String d13n error."); 1006 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut); 1007 rc = wasm.getMemValue(pOut,'i32'); 1008 log("xAccess(",dbFile,") exists ?=",rc); 1009 rc = vfsSyncWrappers.xOpen(opfsVfs.pointer, zDbFile, 1010 fid, openFlags, pOut); 1011 log("open rc =",rc,"state.sabOPView[xOpen] =", 1012 state.sabOPView[state.opIds.xOpen]); 1013 if(0!==rc){ 1014 error("open failed with code",rc); 1015 return; 1016 } 1017 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut); 1018 rc = wasm.getMemValue(pOut,'i32'); 1019 if(!rc) toss("xAccess() failed to detect file."); 1020 rc = ioSyncWrappers.xSync(sq3File.pointer, 0); 1021 if(rc) toss('sync failed w/ rc',rc); 1022 rc = ioSyncWrappers.xTruncate(sq3File.pointer, 1024); 1023 if(rc) toss('truncate failed w/ rc',rc); 1024 wasm.setMemValue(pOut,0,'i64'); 1025 rc = ioSyncWrappers.xFileSize(sq3File.pointer, pOut); 1026 if(rc) toss('xFileSize failed w/ rc',rc); 1027 log("xFileSize says:",wasm.getMemValue(pOut, 'i64')); 1028 rc = ioSyncWrappers.xWrite(sq3File.pointer, zDbFile, 10, 1); 1029 if(rc) toss("xWrite() failed!"); 1030 const readBuf = wasm.scopedAlloc(16); 1031 rc = ioSyncWrappers.xRead(sq3File.pointer, readBuf, 6, 2); 1032 wasm.setMemValue(readBuf+6,0); 1033 let jRead = wasm.cstringToJs(readBuf); 1034 log("xRead() got:",jRead); 1035 if("sanity"!==jRead) toss("Unexpected xRead() value."); 1036 if(vfsSyncWrappers.xSleep){ 1037 log("xSleep()ing before close()ing..."); 1038 vfsSyncWrappers.xSleep(opfsVfs.pointer,2000); 1039 log("waking up from xSleep()"); 1040 } 1041 rc = ioSyncWrappers.xClose(fid); 1042 log("xClose rc =",rc,"sabOPView =",state.sabOPView); 1043 log("Deleting file:",dbFile); 1044 vfsSyncWrappers.xDelete(opfsVfs.pointer, zDbFile, 0x1234); 1045 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut); 1046 rc = wasm.getMemValue(pOut,'i32'); 1047 if(rc) toss("Expecting 0 from xAccess(",dbFile,") after xDelete()."); 1048 warn("End of OPFS sanity checks."); 1049 }finally{ 1050 sq3File.dispose(); 1051 wasm.scopedAllocPop(scope); 1052 } 1053 }/*sanityCheck()*/; 1054 1055 W.onmessage = function({data}){ 1056 //log("Worker.onmessage:",data); 1057 switch(data.type){ 1058 case 'opfs-async-loaded': 1059 /*Arrives as soon as the asyc proxy finishes loading. 1060 Pass our config and shared state on to the async worker.*/ 1061 W.postMessage({type: 'opfs-async-init',args: state}); 1062 break; 1063 case 'opfs-async-inited':{ 1064 /*Indicates that the async partner has received the 'init' 1065 and has finished initializing, so the real work can 1066 begin...*/ 1067 try { 1068 const rc = capi.sqlite3_vfs_register(opfsVfs.pointer, 0); 1069 if(rc){ 1070 toss("sqlite3_vfs_register(OPFS) failed with rc",rc); 1071 } 1072 if(opfsVfs.pointer !== capi.sqlite3_vfs_find("opfs")){ 1073 toss("BUG: sqlite3_vfs_find() failed for just-installed OPFS VFS"); 1074 } 1075 capi.sqlite3_vfs_register.addReference(opfsVfs, opfsIoMethods); 1076 state.sabOPView = new Int32Array(state.sabOP); 1077 state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize); 1078 state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize); 1079 initS11n(); 1080 if(options.sanityChecks){ 1081 warn("Running sanity checks because of opfs-sanity-check URL arg..."); 1082 sanityCheck(); 1083 } 1084 navigator.storage.getDirectory().then((d)=>{ 1085 W.onerror = W._originalOnError; 1086 delete W._originalOnError; 1087 sqlite3.opfs = opfsUtil; 1088 opfsUtil.rootDirectory = d; 1089 log("End of OPFS sqlite3_vfs setup.", opfsVfs); 1090 promiseResolve(sqlite3); 1091 }); 1092 }catch(e){ 1093 error(e); 1094 promiseReject(e); 1095 } 1096 break; 1097 } 1098 default: 1099 promiseReject(e); 1100 error("Unexpected message from the async worker:",data); 1101 break; 1102 }/*switch(data.type)*/ 1103 }/*W.onmessage()*/; 1104 })/*thePromise*/; 1105 return thePromise; 1106}/*installOpfsVfs()*/; 1107installOpfsVfs.defaultProxyUri = 1108 "sqlite3-opfs-async-proxy.js"; 1109self.sqlite3ApiBootstrap.initializersAsync.push(async (sqlite3)=>{ 1110 if(sqlite3.scriptInfo && !sqlite3.scriptInfo.isWorker){ 1111 return; 1112 } 1113 try{ 1114 let proxyJs = installOpfsVfs.defaultProxyUri; 1115 if(sqlite3.scriptInfo.sqlite3Dir){ 1116 installOpfsVfs.defaultProxyUri = 1117 sqlite3.scriptInfo.sqlite3Dir + proxyJs; 1118 //console.warn("installOpfsVfs.defaultProxyUri =",installOpfsVfs.defaultProxyUri); 1119 } 1120 return installOpfsVfs().catch((e)=>{ 1121 console.warn("Ignoring inability to install OPFS sqlite3_vfs:",e.message); 1122 }); 1123 }catch(e){ 1124 console.error("installOpfsVfs() exception:",e); 1125 throw e; 1126 } 1127}); 1128}/*sqlite3ApiBootstrap.initializers.push()*/); 1129