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