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