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