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