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