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