13961b263Sstephan/* 2c5313afeSstephan 2022-09-18 33961b263Sstephan 43961b263Sstephan The author disclaims copyright to this source code. In place of a 53961b263Sstephan legal notice, here is a blessing: 63961b263Sstephan 73961b263Sstephan * May you do good and not evil. 83961b263Sstephan * May you find forgiveness for yourself and forgive others. 93961b263Sstephan * May you share freely, never taking more than you give. 103961b263Sstephan 113961b263Sstephan *********************************************************************** 123961b263Sstephan 13c5313afeSstephan This file holds the synchronous half of an sqlite3_vfs 14c5313afeSstephan implementation which proxies, in a synchronous fashion, the 15c5313afeSstephan asynchronous Origin-Private FileSystem (OPFS) APIs using a second 16c5313afeSstephan Worker, implemented in sqlite3-opfs-async-proxy.js. This file is 17c5313afeSstephan intended to be appended to the main sqlite3 JS deliverable somewhere 18f861b36bSstephan after sqlite3-api-oo1.js and before sqlite3-api-cleanup.js. 19c5313afeSstephan*/ 20c5313afeSstephan'use strict'; 21c5313afeSstephanself.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ 22c5313afeSstephan/** 23f861b36bSstephan installOpfsVfs() returns a Promise which, on success, installs an 24f861b36bSstephan sqlite3_vfs named "opfs", suitable for use with all sqlite3 APIs 25f861b36bSstephan which accept a VFS. It is intended to be called via 26f861b36bSstephan sqlite3ApiBootstrap.initializersAsync or an equivalent mechanism. 27f861b36bSstephan 28f861b36bSstephan The installed VFS uses the Origin-Private FileSystem API for 29c5313afeSstephan all file storage. On error it is rejected with an exception 30c5313afeSstephan explaining the problem. Reasons for rejection include, but are 31c5313afeSstephan not limited to: 32c5313afeSstephan 33c5313afeSstephan - The counterpart Worker (see below) could not be loaded. 34c5313afeSstephan 35c5313afeSstephan - The environment does not support OPFS. That includes when 36c5313afeSstephan this function is called from the main window thread. 37c5313afeSstephan 383961b263Sstephan Significant notes and limitations: 393961b263Sstephan 403961b263Sstephan - As of this writing, OPFS is still very much in flux and only 413961b263Sstephan available in bleeding-edge versions of Chrome (v102+, noting that 423961b263Sstephan that number will increase as the OPFS API matures). 433961b263Sstephan 44c5313afeSstephan - The OPFS features used here are only available in dedicated Worker 45f3860120Sstephan threads. This file tries to detect that case, resulting in a 46f3860120Sstephan rejected Promise if those features do not seem to be available. 473961b263Sstephan 48c5313afeSstephan - It requires the SharedArrayBuffer and Atomics classes, and the 49c5313afeSstephan former is only available if the HTTP server emits the so-called 50c5313afeSstephan COOP and COEP response headers. These features are required for 51c5313afeSstephan proxying OPFS's synchronous API via the synchronous interface 52c5313afeSstephan required by the sqlite3_vfs API. 53c5313afeSstephan 54f861b36bSstephan - This function may only be called a single time. When called, this 55f861b36bSstephan function removes itself from the sqlite3 object. 56f861b36bSstephan 57f861b36bSstephan All arguments to this function are for internal/development purposes 58f861b36bSstephan only. They do not constitute a public API and may change at any 59f861b36bSstephan time. 60c5313afeSstephan 61c5313afeSstephan The argument may optionally be a plain object with the following 62c5313afeSstephan configuration options: 63c5313afeSstephan 64c5313afeSstephan - proxyUri: as described above 65c5313afeSstephan 66c5313afeSstephan - verbose (=2): an integer 0-3. 0 disables all logging, 1 enables 67c5313afeSstephan logging of errors. 2 enables logging of warnings and errors. 3 68c5313afeSstephan additionally enables debugging info. 69c5313afeSstephan 70c5313afeSstephan - sanityChecks (=false): if true, some basic sanity tests are 71c5313afeSstephan run on the OPFS VFS API after it's initialized, before the 72c5313afeSstephan returned Promise resolves. 73c5313afeSstephan 74c5313afeSstephan On success, the Promise resolves to the top-most sqlite3 namespace 75f3860120Sstephan object and that object gets a new object installed in its 76f3860120Sstephan `opfs` property, containing several OPFS-specific utilities. 77c5313afeSstephan*/ 78f861b36bSstephanconst installOpfsVfs = function callee(options){ 794cffb644Sstephan if(!self.SharedArrayBuffer || 80f6c686c9Sstephan !self.Atomics || 81c5313afeSstephan !self.FileSystemHandle || 82c5313afeSstephan !self.FileSystemDirectoryHandle || 83c5313afeSstephan !self.FileSystemFileHandle || 84c5313afeSstephan !self.FileSystemFileHandle.prototype.createSyncAccessHandle || 85c5313afeSstephan !navigator.storage.getDirectory){ 86509f4052Sstephan return Promise.reject( 87509f4052Sstephan new Error("This environment does not have OPFS support.") 88509f4052Sstephan ); 893961b263Sstephan } 90f861b36bSstephan if(!options || 'object'!==typeof options){ 91f861b36bSstephan options = Object.create(null); 92f861b36bSstephan } 93509f4052Sstephan const urlParams = new URL(self.location.href).searchParams; 94509f4052Sstephan if(undefined===options.verbose){ 95509f4052Sstephan options.verbose = urlParams.has('opfs-verbose') ? 3 : 2; 96509f4052Sstephan } 97509f4052Sstephan if(undefined===options.sanityChecks){ 98509f4052Sstephan options.sanityChecks = urlParams.has('opfs-sanity-check'); 99509f4052Sstephan } 100509f4052Sstephan if(undefined===options.proxyUri){ 101509f4052Sstephan options.proxyUri = callee.defaultProxyUri; 102509f4052Sstephan } 103509f4052Sstephan 104cd0df83cSstephan if('function' === typeof options.proxyUri){ 105cd0df83cSstephan options.proxyUri = options.proxyUri(); 106cd0df83cSstephan } 107e8afca3fSstephan const thePromise = new Promise(function(promiseResolve, promiseReject_){ 108509f4052Sstephan const loggers = { 109509f4052Sstephan 0:console.error.bind(console), 110509f4052Sstephan 1:console.warn.bind(console), 111509f4052Sstephan 2:console.log.bind(console) 112509f4052Sstephan }; 113509f4052Sstephan const logImpl = (level,...args)=>{ 114509f4052Sstephan if(options.verbose>level) loggers[level]("OPFS syncer:",...args); 115509f4052Sstephan }; 116509f4052Sstephan const log = (...args)=>logImpl(2, ...args); 117509f4052Sstephan const warn = (...args)=>logImpl(1, ...args); 118509f4052Sstephan const error = (...args)=>logImpl(0, ...args); 119c5313afeSstephan const toss = function(...args){throw new Error(args.join(' '))}; 120c5313afeSstephan const capi = sqlite3.capi; 1218948fbeeSstephan const wasm = sqlite3.wasm; 122c5313afeSstephan const sqlite3_vfs = capi.sqlite3_vfs; 123c5313afeSstephan const sqlite3_file = capi.sqlite3_file; 124c5313afeSstephan const sqlite3_io_methods = capi.sqlite3_io_methods; 125509f4052Sstephan /** 126509f4052Sstephan Generic utilities for working with OPFS. This will get filled out 127509f4052Sstephan by the Promise setup and, on success, installed as sqlite3.opfs. 128509f4052Sstephan */ 129509f4052Sstephan const opfsUtil = Object.create(null); 130f815011aSstephan /** 131f815011aSstephan Not part of the public API. Solely for internal/development 132f815011aSstephan use. 133f815011aSstephan */ 134f815011aSstephan opfsUtil.metrics = { 135f815011aSstephan dump: function(){ 136aec046a2Sstephan let k, n = 0, t = 0, w = 0; 137aec046a2Sstephan for(k in state.opIds){ 138f815011aSstephan const m = metrics[k]; 139f815011aSstephan n += m.count; 140f815011aSstephan t += m.time; 141aec046a2Sstephan w += m.wait; 142f815011aSstephan m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0; 143f815011aSstephan m.avgWait = (m.count && m.wait) ? (m.wait / m.count) : 0; 144f815011aSstephan } 145aec046a2Sstephan console.log(self.location.href, 146aec046a2Sstephan "metrics for",self.location.href,":",metrics, 147aec046a2Sstephan "\nTotal of",n,"op(s) for",t, 148aec046a2Sstephan "ms (incl. "+w+" ms of waiting on the async side)"); 14956fae744Sstephan console.log("Serialization metrics:",metrics.s11n); 1503c272ba3Sstephan W.postMessage({type:'opfs-async-metrics'}); 151f815011aSstephan }, 152f815011aSstephan reset: function(){ 153f815011aSstephan let k; 154f815011aSstephan const r = (m)=>(m.count = m.time = m.wait = 0); 155f815011aSstephan for(k in state.opIds){ 156f815011aSstephan r(metrics[k] = Object.create(null)); 157f815011aSstephan } 158b8c8d4e4Sstephan let s = metrics.s11n = Object.create(null); 159b8c8d4e4Sstephan s = s.serialize = Object.create(null); 160b8c8d4e4Sstephan s.count = s.time = 0; 161b8c8d4e4Sstephan s = metrics.s11n.deserialize = Object.create(null); 162b8c8d4e4Sstephan s.count = s.time = 0; 163f815011aSstephan } 164f815011aSstephan }/*metrics*/; 16556fae744Sstephan const promiseReject = function(err){ 16656fae744Sstephan opfsVfs.dispose(); 16756fae744Sstephan return promiseReject_(err); 16856fae744Sstephan }; 16956fae744Sstephan const W = new Worker(options.proxyUri); 17056fae744Sstephan W._originalOnError = W.onerror /* will be restored later */; 17156fae744Sstephan W.onerror = function(err){ 17256fae744Sstephan // The error object doesn't contain any useful info when the 17356fae744Sstephan // failure is, e.g., that the remote script is 404. 1749a55773bSstephan error("Error initializing OPFS asyncer:",err); 17556fae744Sstephan promiseReject(new Error("Loading OPFS async Worker failed for unknown reasons.")); 17656fae744Sstephan }; 177c9e2602eSstephan const pDVfs = capi.sqlite3_vfs_find(null)/*pointer to default VFS*/; 178c9e2602eSstephan const dVfs = pDVfs 179c9e2602eSstephan ? new sqlite3_vfs(pDVfs) 180c9e2602eSstephan : null /* dVfs will be null when sqlite3 is built with 181c9e2602eSstephan SQLITE_OS_OTHER. Though we cannot currently handle 182c9e2602eSstephan that case, the hope is to eventually be able to. */; 183c9e2602eSstephan const opfsVfs = new sqlite3_vfs(); 184c9e2602eSstephan const opfsIoMethods = new sqlite3_io_methods(); 185c9e2602eSstephan opfsVfs.$iVersion = 2/*yes, two*/; 186c9e2602eSstephan opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof; 187c9e2602eSstephan opfsVfs.$mxPathname = 1024/*sure, why not?*/; 188c9e2602eSstephan opfsVfs.$zName = wasm.allocCString("opfs"); 189c9e2602eSstephan // All C-side memory of opfsVfs is zeroed out, but just to be explicit: 190c9e2602eSstephan opfsVfs.$xDlOpen = opfsVfs.$xDlError = opfsVfs.$xDlSym = opfsVfs.$xDlClose = null; 191c9e2602eSstephan opfsVfs.ondispose = [ 192c9e2602eSstephan '$zName', opfsVfs.$zName, 193c9e2602eSstephan 'cleanup default VFS wrapper', ()=>(dVfs ? dVfs.dispose() : null), 194c9e2602eSstephan 'cleanup opfsIoMethods', ()=>opfsIoMethods.dispose() 195c9e2602eSstephan ]; 196c9e2602eSstephan /** 197c9e2602eSstephan Pedantic sidebar about opfsVfs.ondispose: the entries in that array 198c9e2602eSstephan are items to clean up when opfsVfs.dispose() is called, but in this 199c9e2602eSstephan environment it will never be called. The VFS instance simply 200c9e2602eSstephan hangs around until the WASM module instance is cleaned up. We 201c9e2602eSstephan "could" _hypothetically_ clean it up by "importing" an 202c9e2602eSstephan sqlite3_os_end() impl into the wasm build, but the shutdown order 203c9e2602eSstephan of the wasm engine and the JS one are undefined so there is no 204c9e2602eSstephan guaranty that the opfsVfs instance would be available in one 205c9e2602eSstephan environment or the other when sqlite3_os_end() is called (_if_ it 206c9e2602eSstephan gets called at all in a wasm build, which is undefined). 207c9e2602eSstephan */ 208c5313afeSstephan /** 209c5313afeSstephan State which we send to the async-api Worker or share with it. 210c5313afeSstephan This object must initially contain only cloneable or sharable 211c5313afeSstephan objects. After the worker's "inited" message arrives, other types 212c5313afeSstephan of data may be added to it. 213f3860120Sstephan 214f3860120Sstephan For purposes of Atomics.wait() and Atomics.notify(), we use a 215f3860120Sstephan SharedArrayBuffer with one slot reserved for each of the API 216f3860120Sstephan proxy's methods. The sync side of the API uses Atomics.wait() 217f3860120Sstephan on the corresponding slot and the async side uses 218f3860120Sstephan Atomics.notify() on that slot. 219f3860120Sstephan 220f3860120Sstephan The approach of using a single SAB to serialize comms for all 221f3860120Sstephan instances might(?) lead to deadlock situations in multi-db 222f3860120Sstephan cases. We should probably have one SAB here with a single slot 223f3860120Sstephan for locking a per-file initialization step and then allocate a 224f3860120Sstephan separate SAB like the above one for each file. That will 225f861b36bSstephan require a bit of acrobatics but should be feasible. The most 226f861b36bSstephan problematic part is that xOpen() would have to use 227f861b36bSstephan postMessage() to communicate its SharedArrayBuffer, and mixing 228f861b36bSstephan that approach with Atomics.wait/notify() gets a bit messy. 229c5313afeSstephan */ 230c5313afeSstephan const state = Object.create(null); 231c5313afeSstephan state.verbose = options.verbose; 2329a55773bSstephan state.littleEndian = (()=>{ 2339a55773bSstephan const buffer = new ArrayBuffer(2); 234f861b36bSstephan new DataView(buffer).setInt16(0, 256, true /* ==>littleEndian */); 2359a55773bSstephan // Int16Array uses the platform's endianness. 2369a55773bSstephan return new Int16Array(buffer)[0] === 256; 2379a55773bSstephan })(); 238f861b36bSstephan /** 239f861b36bSstephan Whether the async counterpart should log exceptions to 240e8afca3fSstephan the serialization channel. That produces a great deal of 241e8afca3fSstephan noise for seemingly innocuous things like xAccess() checks 24256fae744Sstephan for missing files, so this option may have one of 3 values: 24356fae744Sstephan 24456fae744Sstephan 0 = no exception logging 24556fae744Sstephan 24656fae744Sstephan 1 = only log exceptions for "significant" ops like xOpen(), 24756fae744Sstephan xRead(), and xWrite(). 24856fae744Sstephan 24956fae744Sstephan 2 = log all exceptions. 25056fae744Sstephan */ 25156fae744Sstephan state.asyncS11nExceptions = 1; 252f861b36bSstephan /* Size of file I/O buffer block. 64k = max sqlite3 page size, and 253f861b36bSstephan xRead/xWrite() will never deal in blocks larger than that. */ 254f861b36bSstephan state.fileBufferSize = 1024 * 64; 255138647a5Sstephan state.sabS11nOffset = state.fileBufferSize; 256c9e2602eSstephan /** 257c9e2602eSstephan The size of the block in our SAB for serializing arguments and 258e8afca3fSstephan result values. Needs to be large enough to hold serialized 259c9e2602eSstephan values of any of the proxied APIs. Filenames are the largest 260c9e2602eSstephan part but are limited to opfsVfs.$mxPathname bytes. 261c9e2602eSstephan */ 262c9e2602eSstephan state.sabS11nSize = opfsVfs.$mxPathname * 2; 263c9e2602eSstephan /** 264f861b36bSstephan The SAB used for all data I/O between the synchronous and 265f861b36bSstephan async halves (file i/o and arg/result s11n). 266c9e2602eSstephan */ 267c4b87be3Sstephan state.sabIO = new SharedArrayBuffer( 268c9e2602eSstephan state.fileBufferSize/* file i/o block */ 269c9e2602eSstephan + state.sabS11nSize/* argument/result serialization block */ 270c4b87be3Sstephan ); 271c5313afeSstephan state.opIds = Object.create(null); 272f815011aSstephan const metrics = Object.create(null); 273c5313afeSstephan { 274c9e2602eSstephan /* Indexes for use in our SharedArrayBuffer... */ 275c5313afeSstephan let i = 0; 276c9e2602eSstephan /* SAB slot used to communicate which operation is desired 277c9e2602eSstephan between both workers. This worker writes to it and the other 278c9e2602eSstephan listens for changes. */ 279138647a5Sstephan state.opIds.whichOp = i++; 2809a55773bSstephan /* Slot for storing return values. This worker listens to that 281c9e2602eSstephan slot and the other worker writes to it. */ 282c9e2602eSstephan state.opIds.rc = i++; 283c9e2602eSstephan /* Each function gets an ID which this worker writes to 284c9e2602eSstephan the whichOp slot. The async-api worker uses Atomic.wait() 285c9e2602eSstephan on the whichOp slot to figure out which operation to run 286c9e2602eSstephan next. */ 287c5313afeSstephan state.opIds.xAccess = i++; 288c5313afeSstephan state.opIds.xClose = i++; 289c5313afeSstephan state.opIds.xDelete = i++; 290f3860120Sstephan state.opIds.xDeleteNoWait = i++; 29156fae744Sstephan state.opIds.xFileControl = i++; 292c5313afeSstephan state.opIds.xFileSize = i++; 2939a55773bSstephan state.opIds.xLock = i++; 294c5313afeSstephan state.opIds.xOpen = i++; 295c5313afeSstephan state.opIds.xRead = i++; 296c5313afeSstephan state.opIds.xSleep = i++; 297c5313afeSstephan state.opIds.xSync = i++; 298c5313afeSstephan state.opIds.xTruncate = i++; 2999a55773bSstephan state.opIds.xUnlock = i++; 300c5313afeSstephan state.opIds.xWrite = i++; 301f3860120Sstephan state.opIds.mkdir = i++; 3023c272ba3Sstephan state.opIds['opfs-async-metrics'] = i++; 3033c272ba3Sstephan state.opIds['opfs-async-shutdown'] = i++; 304f861b36bSstephan /* The retry slot is used by the async part for wait-and-retry 305f861b36bSstephan semantics. Though we could hypothetically use the xSleep slot 306f861b36bSstephan for that, doing so might lead to undesired side effects. */ 307f861b36bSstephan state.opIds.retry = i++; 308f861b36bSstephan state.sabOP = new SharedArrayBuffer( 309f861b36bSstephan i * 4/* ==sizeof int32, noting that Atomics.wait() and friends 310f861b36bSstephan can only function on Int32Array views of an SAB. */); 311f815011aSstephan opfsUtil.metrics.reset(); 312c5313afeSstephan } 313c9e2602eSstephan /** 314c9e2602eSstephan SQLITE_xxx constants to export to the async worker 315c9e2602eSstephan counterpart... 316c9e2602eSstephan */ 317c5313afeSstephan state.sq3Codes = Object.create(null); 318c9e2602eSstephan [ 319f45c3370Sstephan 'SQLITE_ACCESS_EXISTS', 320f45c3370Sstephan 'SQLITE_ACCESS_READWRITE', 32143b442a6Sstephan 'SQLITE_ERROR', 32243b442a6Sstephan 'SQLITE_IOERR', 32343b442a6Sstephan 'SQLITE_IOERR_ACCESS', 32443b442a6Sstephan 'SQLITE_IOERR_CLOSE', 325c4b87be3Sstephan 'SQLITE_IOERR_DELETE', 32643b442a6Sstephan 'SQLITE_IOERR_FSYNC', 32743b442a6Sstephan 'SQLITE_IOERR_LOCK', 32843b442a6Sstephan 'SQLITE_IOERR_READ', 32943b442a6Sstephan 'SQLITE_IOERR_SHORT_READ', 33043b442a6Sstephan 'SQLITE_IOERR_TRUNCATE', 33143b442a6Sstephan 'SQLITE_IOERR_UNLOCK', 33243b442a6Sstephan 'SQLITE_IOERR_WRITE', 3339a55773bSstephan 'SQLITE_LOCK_EXCLUSIVE', 33443b442a6Sstephan 'SQLITE_LOCK_NONE', 33543b442a6Sstephan 'SQLITE_LOCK_PENDING', 33643b442a6Sstephan 'SQLITE_LOCK_RESERVED', 33743b442a6Sstephan 'SQLITE_LOCK_SHARED', 33843b442a6Sstephan 'SQLITE_MISUSE', 33943b442a6Sstephan 'SQLITE_NOTFOUND', 34043b442a6Sstephan 'SQLITE_OPEN_CREATE', 34143b442a6Sstephan 'SQLITE_OPEN_DELETEONCLOSE', 342c4b87be3Sstephan 'SQLITE_OPEN_READONLY' 3439a55773bSstephan ].forEach((k)=>{ 3449a55773bSstephan if(undefined === (state.sq3Codes[k] = capi[k])){ 3459a55773bSstephan toss("Maintenance required: not found:",k); 3469a55773bSstephan } 347c5313afeSstephan }); 348c5313afeSstephan 349c5313afeSstephan /** 350c9e2602eSstephan Runs the given operation (by name) in the async worker 351c9e2602eSstephan counterpart, waits for its response, and returns the result 352c9e2602eSstephan which the async worker writes to SAB[state.opIds.rc]. The 353c9e2602eSstephan 2nd and subsequent arguments must be the aruguments for the 354c9e2602eSstephan async op. 355c5313afeSstephan */ 356138647a5Sstephan const opRun = (op,...args)=>{ 3575e8bb0aaSstephan const opNdx = state.opIds[op] || toss("Invalid op ID:",op); 3585e8bb0aaSstephan state.s11n.serialize(...args); 359c9e2602eSstephan Atomics.store(state.sabOPView, state.opIds.rc, -1); 3605e8bb0aaSstephan Atomics.store(state.sabOPView, state.opIds.whichOp, opNdx); 361f861b36bSstephan Atomics.notify(state.sabOPView, state.opIds.whichOp) 362f861b36bSstephan /* async thread will take over here */; 363f815011aSstephan const t = performance.now(); 364f861b36bSstephan Atomics.wait(state.sabOPView, state.opIds.rc, -1) 365f861b36bSstephan /* When this wait() call returns, the async half will have 366f861b36bSstephan completed the operation and reported its results. */; 367c9e2602eSstephan const rc = Atomics.load(state.sabOPView, state.opIds.rc); 368e8afca3fSstephan metrics[op].wait += performance.now() - t; 369e8afca3fSstephan if(rc && state.asyncS11nExceptions){ 37072ab400dSstephan const err = state.s11n.deserialize(); 37172ab400dSstephan if(err) error(op+"() async error:",...err); 37272ab400dSstephan } 3735e8bb0aaSstephan return rc; 374c5313afeSstephan }; 375c5313afeSstephan 37649048b14Sstephan /** 37749048b14Sstephan Not part of the public API. Only for test/development use. 37849048b14Sstephan */ 37949048b14Sstephan opfsUtil.debug = { 38049048b14Sstephan asyncShutdown: ()=>{ 38149048b14Sstephan warn("Shutting down OPFS async listener. The OPFS VFS will no longer work."); 38249048b14Sstephan opRun('opfs-async-shutdown'); 38349048b14Sstephan }, 38449048b14Sstephan asyncRestart: ()=>{ 38549048b14Sstephan warn("Attempting to restart OPFS VFS async listener. Might work, might not."); 38649048b14Sstephan W.postMessage({type: 'opfs-async-restart'}); 38749048b14Sstephan } 38849048b14Sstephan }; 38949048b14Sstephan 390138647a5Sstephan const initS11n = ()=>{ 391b8c8d4e4Sstephan /** 392f861b36bSstephan !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 393f861b36bSstephan ACHTUNG: this code is 100% duplicated in the other half of 394f861b36bSstephan this proxy! The documentation is maintained in the 395f861b36bSstephan "synchronous half". 396f861b36bSstephan !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 397b8c8d4e4Sstephan 39872ab400dSstephan This proxy de/serializes cross-thread function arguments and 39972ab400dSstephan output-pointer values via the state.sabIO SharedArrayBuffer, 40072ab400dSstephan using the region defined by (state.sabS11nOffset, 40172ab400dSstephan state.sabS11nOffset]. Only one dataset is recorded at a time. 40272ab400dSstephan 403f861b36bSstephan This is not a general-purpose format. It only supports the 404f861b36bSstephan range of operations, and data sizes, needed by the 405f861b36bSstephan sqlite3_vfs and sqlite3_io_methods operations. Serialized 406f861b36bSstephan data are transient and this serialization algorithm may 407f861b36bSstephan change at any time. 40872ab400dSstephan 40972ab400dSstephan The data format can be succinctly summarized as: 41072ab400dSstephan 41172ab400dSstephan Nt...Td...D 41272ab400dSstephan 41372ab400dSstephan Where: 41472ab400dSstephan 41572ab400dSstephan - N = number of entries (1 byte) 41672ab400dSstephan 41772ab400dSstephan - t = type ID of first argument (1 byte) 41872ab400dSstephan 41972ab400dSstephan - ...T = type IDs of the 2nd and subsequent arguments (1 byte 42072ab400dSstephan each). 42172ab400dSstephan 42272ab400dSstephan - d = raw bytes of first argument (per-type size). 42372ab400dSstephan 42472ab400dSstephan - ...D = raw bytes of the 2nd and subsequent arguments (per-type 42572ab400dSstephan size). 42672ab400dSstephan 42772ab400dSstephan All types except strings have fixed sizes. Strings are stored 42872ab400dSstephan using their TextEncoder/TextDecoder representations. It would 42972ab400dSstephan arguably make more sense to store them as Int16Arrays of 43072ab400dSstephan their JS character values, but how best/fastest to get that 431f861b36bSstephan in and out of string form is an open point. Initial 432f861b36bSstephan experimentation with that approach did not gain us any speed. 43372ab400dSstephan 43472ab400dSstephan Historical note: this impl was initially about 1% this size by 43572ab400dSstephan using using JSON.stringify/parse(), but using fit-to-purpose 43672ab400dSstephan serialization saves considerable runtime. 437b8c8d4e4Sstephan */ 438138647a5Sstephan if(state.s11n) return state.s11n; 439b8c8d4e4Sstephan const textDecoder = new TextDecoder(), 440b8c8d4e4Sstephan textEncoder = new TextEncoder('utf-8'), 441b8c8d4e4Sstephan viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize), 442b8c8d4e4Sstephan viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize); 443138647a5Sstephan state.s11n = Object.create(null); 44472ab400dSstephan /* Only arguments and return values of these types may be 44572ab400dSstephan serialized. This covers the whole range of types needed by the 44672ab400dSstephan sqlite3_vfs API. */ 447b8c8d4e4Sstephan const TypeIds = Object.create(null); 448b8c8d4e4Sstephan TypeIds.number = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' }; 449b8c8d4e4Sstephan TypeIds.bigint = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' }; 450b8c8d4e4Sstephan TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' }; 451b8c8d4e4Sstephan TypeIds.string = { id: 4 }; 45272ab400dSstephan 45372ab400dSstephan const getTypeId = (v)=>( 45472ab400dSstephan TypeIds[typeof v] 45572ab400dSstephan || toss("Maintenance required: this value type cannot be serialized.",v) 45672ab400dSstephan ); 457b8c8d4e4Sstephan const getTypeIdById = (tid)=>{ 458b8c8d4e4Sstephan switch(tid){ 459b8c8d4e4Sstephan case TypeIds.number.id: return TypeIds.number; 460b8c8d4e4Sstephan case TypeIds.bigint.id: return TypeIds.bigint; 461b8c8d4e4Sstephan case TypeIds.boolean.id: return TypeIds.boolean; 462b8c8d4e4Sstephan case TypeIds.string.id: return TypeIds.string; 463b8c8d4e4Sstephan default: toss("Invalid type ID:",tid); 464b8c8d4e4Sstephan } 465b8c8d4e4Sstephan }; 46672ab400dSstephan 467138647a5Sstephan /** 46872ab400dSstephan Returns an array of the deserialized state stored by the most 46972ab400dSstephan recent serialize() operation (from from this thread or the 470*da264159Sstephan counterpart thread), or null if the serialization buffer is 471*da264159Sstephan empty. If passed a truthy argument, the serialization buffer 472*da264159Sstephan is cleared after deserialization. 473138647a5Sstephan */ 474*da264159Sstephan state.s11n.deserialize = function(clear=false){ 475b8c8d4e4Sstephan ++metrics.s11n.deserialize.count; 476b8c8d4e4Sstephan const t = performance.now(); 477b8c8d4e4Sstephan const argc = viewU8[0]; 47872ab400dSstephan const rc = argc ? [] : null; 479b8c8d4e4Sstephan if(argc){ 48072ab400dSstephan const typeIds = []; 48172ab400dSstephan let offset = 1, i, n, v; 482b8c8d4e4Sstephan for(i = 0; i < argc; ++i, ++offset){ 483b8c8d4e4Sstephan typeIds.push(getTypeIdById(viewU8[offset])); 484138647a5Sstephan } 485b8c8d4e4Sstephan for(i = 0; i < argc; ++i){ 486b8c8d4e4Sstephan const t = typeIds[i]; 487b8c8d4e4Sstephan if(t.getter){ 488b8c8d4e4Sstephan v = viewDV[t.getter](offset, state.littleEndian); 489b8c8d4e4Sstephan offset += t.size; 49072ab400dSstephan }else{/*String*/ 491b8c8d4e4Sstephan n = viewDV.getInt32(offset, state.littleEndian); 492b8c8d4e4Sstephan offset += 4; 493b8c8d4e4Sstephan v = textDecoder.decode(viewU8.slice(offset, offset+n)); 494b8c8d4e4Sstephan offset += n; 495b8c8d4e4Sstephan } 496b8c8d4e4Sstephan rc.push(v); 497b8c8d4e4Sstephan } 498b8c8d4e4Sstephan } 499*da264159Sstephan if(clear) viewU8[0] = 0; 500b8c8d4e4Sstephan //log("deserialize:",argc, rc); 501b8c8d4e4Sstephan metrics.s11n.deserialize.time += performance.now() - t; 502b8c8d4e4Sstephan return rc; 503b8c8d4e4Sstephan }; 50472ab400dSstephan 505138647a5Sstephan /** 506138647a5Sstephan Serializes all arguments to the shared buffer for consumption 507b8c8d4e4Sstephan by the counterpart thread. 5085e8bb0aaSstephan 509b8c8d4e4Sstephan This routine is only intended for serializing OPFS VFS 510b8c8d4e4Sstephan arguments and (in at least one special case) result values, 511b8c8d4e4Sstephan and the buffer is sized to be able to comfortably handle 512b8c8d4e4Sstephan those. 5135e8bb0aaSstephan 5145e8bb0aaSstephan If passed no arguments then it zeroes out the serialization 5155e8bb0aaSstephan state. 516138647a5Sstephan */ 517138647a5Sstephan state.s11n.serialize = function(...args){ 518b8c8d4e4Sstephan const t = performance.now(); 51972ab400dSstephan ++metrics.s11n.serialize.count; 5205e8bb0aaSstephan if(args.length){ 521b8c8d4e4Sstephan //log("serialize():",args); 52272ab400dSstephan const typeIds = []; 52372ab400dSstephan let i = 0, offset = 1; 52472ab400dSstephan viewU8[0] = args.length & 0xff /* header = # of args */; 525b8c8d4e4Sstephan for(; i < args.length; ++i, ++offset){ 52672ab400dSstephan /* Write the TypeIds.id value into the next args.length 52772ab400dSstephan bytes. */ 528b8c8d4e4Sstephan typeIds.push(getTypeId(args[i])); 529b8c8d4e4Sstephan viewU8[offset] = typeIds[i].id; 5305e8bb0aaSstephan } 531b8c8d4e4Sstephan for(i = 0; i < args.length; ++i) { 53272ab400dSstephan /* Deserialize the following bytes based on their 53372ab400dSstephan corresponding TypeIds.id from the header. */ 534b8c8d4e4Sstephan const t = typeIds[i]; 535b8c8d4e4Sstephan if(t.setter){ 536b8c8d4e4Sstephan viewDV[t.setter](offset, args[i], state.littleEndian); 537b8c8d4e4Sstephan offset += t.size; 53872ab400dSstephan }else{/*String*/ 539b8c8d4e4Sstephan const s = textEncoder.encode(args[i]); 540b8c8d4e4Sstephan viewDV.setInt32(offset, s.byteLength, state.littleEndian); 541b8c8d4e4Sstephan offset += 4; 542b8c8d4e4Sstephan viewU8.set(s, offset); 543b8c8d4e4Sstephan offset += s.byteLength; 544b8c8d4e4Sstephan } 545b8c8d4e4Sstephan } 546b8c8d4e4Sstephan //log("serialize() result:",viewU8.slice(0,offset)); 547b8c8d4e4Sstephan }else{ 548b8c8d4e4Sstephan viewU8[0] = 0; 549b8c8d4e4Sstephan } 550b8c8d4e4Sstephan metrics.s11n.serialize.time += performance.now() - t; 551138647a5Sstephan }; 552138647a5Sstephan return state.s11n; 553b8c8d4e4Sstephan }/*initS11n()*/; 554138647a5Sstephan 555c5313afeSstephan /** 556c5313afeSstephan Generates a random ASCII string len characters long, intended for 557c5313afeSstephan use as a temporary file name. 558c5313afeSstephan */ 559c5313afeSstephan const randomFilename = function f(len=16){ 560c5313afeSstephan if(!f._chars){ 561c5313afeSstephan f._chars = "abcdefghijklmnopqrstuvwxyz"+ 562c5313afeSstephan "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+ 563c5313afeSstephan "012346789"; 564c5313afeSstephan f._n = f._chars.length; 565c5313afeSstephan } 566c5313afeSstephan const a = []; 567c5313afeSstephan let i = 0; 568c5313afeSstephan for( ; i < len; ++i){ 569c5313afeSstephan const ndx = Math.random() * (f._n * 64) % f._n | 0; 570c5313afeSstephan a[i] = f._chars[ndx]; 571c5313afeSstephan } 572c5313afeSstephan return a.join(''); 573c5313afeSstephan }; 574c5313afeSstephan 575c5313afeSstephan /** 576c5313afeSstephan Map of sqlite3_file pointers to objects constructed by xOpen(). 577c5313afeSstephan */ 578c5313afeSstephan const __openFiles = Object.create(null); 5793961b263Sstephan 5803961b263Sstephan /** 5813961b263Sstephan Installs a StructBinder-bound function pointer member of the 5823961b263Sstephan given name and function in the given StructType target object. 5833961b263Sstephan It creates a WASM proxy for the given function and arranges for 5843961b263Sstephan that proxy to be cleaned up when tgt.dispose() is called. Throws 5853961b263Sstephan on the slightest hint of error (e.g. tgt is-not-a StructType, 5863961b263Sstephan name does not map to a struct-bound member, etc.). 5873961b263Sstephan 5883961b263Sstephan Returns a proxy for this function which is bound to tgt and takes 5893961b263Sstephan 2 args (name,func). That function returns the same thing, 5903961b263Sstephan permitting calls to be chained. 5913961b263Sstephan 5923961b263Sstephan If called with only 1 arg, it has no side effects but returns a 5933961b263Sstephan func with the same signature as described above. 5943961b263Sstephan */ 5953961b263Sstephan const installMethod = function callee(tgt, name, func){ 596f3860120Sstephan if(!(tgt instanceof sqlite3.StructBinder.StructType)){ 5973961b263Sstephan toss("Usage error: target object is-not-a StructType."); 5983961b263Sstephan } 5993961b263Sstephan if(1===arguments.length){ 6003961b263Sstephan return (n,f)=>callee(tgt,n,f); 6013961b263Sstephan } 6023961b263Sstephan if(!callee.argcProxy){ 6033961b263Sstephan callee.argcProxy = function(func,sig){ 6043961b263Sstephan return function(...args){ 6053961b263Sstephan if(func.length!==arguments.length){ 6063961b263Sstephan toss("Argument mismatch. Native signature is:",sig); 6073961b263Sstephan } 6083961b263Sstephan return func.apply(this, args); 6093961b263Sstephan } 6103961b263Sstephan }; 6113961b263Sstephan callee.removeFuncList = function(){ 6123961b263Sstephan if(this.ondispose.__removeFuncList){ 6133961b263Sstephan this.ondispose.__removeFuncList.forEach( 6143961b263Sstephan (v,ndx)=>{ 6153961b263Sstephan if('number'===typeof v){ 6163961b263Sstephan try{wasm.uninstallFunction(v)} 6173961b263Sstephan catch(e){/*ignore*/} 6183961b263Sstephan } 6193961b263Sstephan /* else it's a descriptive label for the next number in 6203961b263Sstephan the list. */ 6213961b263Sstephan } 6223961b263Sstephan ); 6233961b263Sstephan delete this.ondispose.__removeFuncList; 6243961b263Sstephan } 6253961b263Sstephan }; 6263961b263Sstephan }/*static init*/ 6273961b263Sstephan const sigN = tgt.memberSignature(name); 6283961b263Sstephan if(sigN.length<2){ 6293961b263Sstephan toss("Member",name," is not a function pointer. Signature =",sigN); 6303961b263Sstephan } 6313961b263Sstephan const memKey = tgt.memberKey(name); 632c2ccd676Sstephan const fProxy = 0 633f861b36bSstephan /** This middle-man proxy is only for use during development, to 634f861b36bSstephan confirm that we always pass the proper number of 635f861b36bSstephan arguments. We know that the C-level code will always use the 636f861b36bSstephan correct argument count. */ 6373961b263Sstephan ? callee.argcProxy(func, sigN) 6383961b263Sstephan : func; 6393961b263Sstephan const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true)); 6403961b263Sstephan tgt[memKey] = pFunc; 6413961b263Sstephan if(!tgt.ondispose) tgt.ondispose = []; 6423961b263Sstephan if(!tgt.ondispose.__removeFuncList){ 6433961b263Sstephan tgt.ondispose.push('ondispose.__removeFuncList handler', 6443961b263Sstephan callee.removeFuncList); 6453961b263Sstephan tgt.ondispose.__removeFuncList = []; 6463961b263Sstephan } 6473961b263Sstephan tgt.ondispose.__removeFuncList.push(memKey, pFunc); 6483961b263Sstephan return (n,f)=>callee(tgt, n, f); 6493961b263Sstephan }/*installMethod*/; 6503961b263Sstephan 651f815011aSstephan const opTimer = Object.create(null); 652f815011aSstephan opTimer.op = undefined; 653f815011aSstephan opTimer.start = undefined; 654f815011aSstephan const mTimeStart = (op)=>{ 655f815011aSstephan opTimer.start = performance.now(); 656f815011aSstephan opTimer.op = op; 657f815011aSstephan ++metrics[op].count; 658f815011aSstephan }; 659f815011aSstephan const mTimeEnd = ()=>( 660f815011aSstephan metrics[opTimer.op].time += performance.now() - opTimer.start 661f815011aSstephan ); 662f815011aSstephan 6633961b263Sstephan /** 664c5313afeSstephan Impls for the sqlite3_io_methods methods. Maintenance reminder: 665c5313afeSstephan members are in alphabetical order to simplify finding them. 6663961b263Sstephan */ 667c5313afeSstephan const ioSyncWrappers = { 668c5313afeSstephan xCheckReservedLock: function(pFile,pOut){ 669f861b36bSstephan /** 670f861b36bSstephan As of late 2022, only a single lock can be held on an OPFS 671f861b36bSstephan file. We have no way of checking whether any _other_ db 672f861b36bSstephan connection has a lock except by trying to obtain and (on 673f861b36bSstephan success) release a sync-handle for it, but doing so would 674f861b36bSstephan involve an inherent race condition. For the time being, 675f861b36bSstephan pending a better solution, we simply report whether the 676f861b36bSstephan given pFile instance has a lock. 677f861b36bSstephan */ 6789a55773bSstephan const f = __openFiles[pFile]; 6799a55773bSstephan wasm.setMemValue(pOut, f.lockMode ? 1 : 0, 'i32'); 680c5313afeSstephan return 0; 681c5313afeSstephan }, 682c5313afeSstephan xClose: function(pFile){ 683f815011aSstephan mTimeStart('xClose'); 684c5313afeSstephan let rc = 0; 685c5313afeSstephan const f = __openFiles[pFile]; 686c5313afeSstephan if(f){ 687c5313afeSstephan delete __openFiles[pFile]; 688c5313afeSstephan rc = opRun('xClose', pFile); 689c5313afeSstephan if(f.sq3File) f.sq3File.dispose(); 6903961b263Sstephan } 691f815011aSstephan mTimeEnd(); 692c5313afeSstephan return rc; 693c5313afeSstephan }, 694c5313afeSstephan xDeviceCharacteristics: function(pFile){ 695c5313afeSstephan //debug("xDeviceCharacteristics(",pFile,")"); 696c5313afeSstephan return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN; 697c5313afeSstephan }, 698f815011aSstephan xFileControl: function(pFile, opId, pArg){ 699f815011aSstephan mTimeStart('xFileControl'); 700aec046a2Sstephan const rc = (capi.SQLITE_FCNTL_SYNC===opId) 701138647a5Sstephan ? opRun('xSync', pFile, 0) 702aec046a2Sstephan : capi.SQLITE_NOTFOUND; 703f815011aSstephan mTimeEnd(); 704aec046a2Sstephan return rc; 705c5313afeSstephan }, 706c5313afeSstephan xFileSize: function(pFile,pSz64){ 707f815011aSstephan mTimeStart('xFileSize'); 708c5313afeSstephan const rc = opRun('xFileSize', pFile); 709e8afca3fSstephan if(0==rc){ 710138647a5Sstephan const sz = state.s11n.deserialize()[0]; 711278d3fafSstephan wasm.setMemValue(pSz64, sz, 'i64'); 712c5313afeSstephan } 713f815011aSstephan mTimeEnd(); 714c5313afeSstephan return rc; 715c5313afeSstephan }, 716c5313afeSstephan xLock: function(pFile,lockType){ 7179a55773bSstephan mTimeStart('xLock'); 7189a55773bSstephan const f = __openFiles[pFile]; 7199a55773bSstephan let rc = 0; 7209a55773bSstephan if( capi.SQLITE_LOCK_NONE === f.lockType ) { 7219a55773bSstephan rc = opRun('xLock', pFile, lockType); 7229a55773bSstephan if( 0===rc ) f.lockType = lockType; 7239a55773bSstephan }else{ 7249a55773bSstephan f.lockType = lockType; 7259a55773bSstephan } 7269a55773bSstephan mTimeEnd(); 7279a55773bSstephan return rc; 728c5313afeSstephan }, 729138647a5Sstephan xRead: function(pFile,pDest,n,offset64){ 730f815011aSstephan mTimeStart('xRead'); 731c5313afeSstephan const f = __openFiles[pFile]; 732c5313afeSstephan let rc; 733c5313afeSstephan try { 734138647a5Sstephan rc = opRun('xRead',pFile, n, Number(offset64)); 735862281fcSstephan if(0===rc || capi.SQLITE_IOERR_SHORT_READ===rc){ 736f861b36bSstephan /** 737f861b36bSstephan Results get written to the SharedArrayBuffer f.sabView. 738f861b36bSstephan Because the heap is _not_ a SharedArrayBuffer, we have 739f861b36bSstephan to copy the results. TypedArray.set() seems to be the 740f861b36bSstephan fastest way to copy this. */ 741f815011aSstephan wasm.heap8u().set(f.sabView.subarray(0, n), pDest); 742862281fcSstephan } 743c5313afeSstephan }catch(e){ 744c5313afeSstephan error("xRead(",arguments,") failed:",e,f); 745c5313afeSstephan rc = capi.SQLITE_IOERR_READ; 7463961b263Sstephan } 747f815011aSstephan mTimeEnd(); 748c5313afeSstephan return rc; 749c5313afeSstephan }, 750c5313afeSstephan xSync: function(pFile,flags){ 751aec046a2Sstephan ++metrics.xSync.count; 752138647a5Sstephan return 0; // impl'd in xFileControl() 753c5313afeSstephan }, 754c5313afeSstephan xTruncate: function(pFile,sz64){ 755f815011aSstephan mTimeStart('xTruncate'); 756138647a5Sstephan const rc = opRun('xTruncate', pFile, Number(sz64)); 757f815011aSstephan mTimeEnd(); 758f815011aSstephan return rc; 759c5313afeSstephan }, 760c5313afeSstephan xUnlock: function(pFile,lockType){ 7619a55773bSstephan mTimeStart('xUnlock'); 7629a55773bSstephan const f = __openFiles[pFile]; 7639a55773bSstephan let rc = 0; 7649a55773bSstephan if( capi.SQLITE_LOCK_NONE === lockType 7659a55773bSstephan && f.lockType ){ 7669a55773bSstephan rc = opRun('xUnlock', pFile, lockType); 7679a55773bSstephan } 7689a55773bSstephan if( 0===rc ) f.lockType = lockType; 7699a55773bSstephan mTimeEnd(); 7709a55773bSstephan return rc; 771c5313afeSstephan }, 772138647a5Sstephan xWrite: function(pFile,pSrc,n,offset64){ 773f815011aSstephan mTimeStart('xWrite'); 774c5313afeSstephan const f = __openFiles[pFile]; 775f815011aSstephan let rc; 776c5313afeSstephan try { 777f815011aSstephan f.sabView.set(wasm.heap8u().subarray(pSrc, pSrc+n)); 778138647a5Sstephan rc = opRun('xWrite', pFile, n, Number(offset64)); 779c5313afeSstephan }catch(e){ 780c5313afeSstephan error("xWrite(",arguments,") failed:",e,f); 781f815011aSstephan rc = capi.SQLITE_IOERR_WRITE; 782c5313afeSstephan } 783f815011aSstephan mTimeEnd(); 784f815011aSstephan return rc; 785c5313afeSstephan } 786c5313afeSstephan }/*ioSyncWrappers*/; 787c5313afeSstephan 788c5313afeSstephan /** 789c5313afeSstephan Impls for the sqlite3_vfs methods. Maintenance reminder: members 790c5313afeSstephan are in alphabetical order to simplify finding them. 791c5313afeSstephan */ 792c5313afeSstephan const vfsSyncWrappers = { 793c5313afeSstephan xAccess: function(pVfs,zName,flags,pOut){ 794f815011aSstephan mTimeStart('xAccess'); 7955e8bb0aaSstephan const rc = opRun('xAccess', wasm.cstringToJs(zName)); 7965e8bb0aaSstephan wasm.setMemValue( pOut, (rc ? 0 : 1), 'i32' ); 797f815011aSstephan mTimeEnd(); 7983961b263Sstephan return 0; 799c5313afeSstephan }, 800c5313afeSstephan xCurrentTime: function(pVfs,pOut){ 8013961b263Sstephan /* If it turns out that we need to adjust for timezone, see: 8023961b263Sstephan https://stackoverflow.com/a/11760121/1458521 */ 8033961b263Sstephan wasm.setMemValue(pOut, 2440587.5 + (new Date().getTime()/86400000), 8043961b263Sstephan 'double'); 8053961b263Sstephan return 0; 806c5313afeSstephan }, 807c5313afeSstephan xCurrentTimeInt64: function(pVfs,pOut){ 8083961b263Sstephan // TODO: confirm that this calculation is correct 8093961b263Sstephan wasm.setMemValue(pOut, (2440587.5 * 86400000) + new Date().getTime(), 8103961b263Sstephan 'i64'); 8113961b263Sstephan return 0; 812c5313afeSstephan }, 813c5313afeSstephan xDelete: function(pVfs, zName, doSyncDir){ 814f815011aSstephan mTimeStart('xDelete'); 815138647a5Sstephan opRun('xDelete', wasm.cstringToJs(zName), doSyncDir, false); 816f3860120Sstephan /* We're ignoring errors because we cannot yet differentiate 817f3860120Sstephan between harmless and non-harmless failures. */ 818f815011aSstephan mTimeEnd(); 819f3860120Sstephan return 0; 820c5313afeSstephan }, 821c5313afeSstephan xFullPathname: function(pVfs,zName,nOut,pOut){ 822c5313afeSstephan /* Until/unless we have some notion of "current dir" 823c5313afeSstephan in OPFS, simply copy zName to pOut... */ 824c5313afeSstephan const i = wasm.cstrncpy(pOut, zName, nOut); 825c5313afeSstephan return i<nOut ? 0 : capi.SQLITE_CANTOPEN 826c5313afeSstephan /*CANTOPEN is required by the docs but SQLITE_RANGE would be a closer match*/; 827c5313afeSstephan }, 828c5313afeSstephan xGetLastError: function(pVfs,nOut,pOut){ 829c5313afeSstephan /* TODO: store exception.message values from the async 830c5313afeSstephan partner in a dedicated SharedArrayBuffer, noting that we'd have 831c5313afeSstephan to encode them... TextEncoder can do that for us. */ 832c5313afeSstephan warn("OPFS xGetLastError() has nothing sensible to return."); 8333961b263Sstephan return 0; 834c5313afeSstephan }, 8358766fd20Sstephan //xSleep is optionally defined below 836c5313afeSstephan xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){ 837f815011aSstephan mTimeStart('xOpen'); 838c5313afeSstephan if(0===zName){ 839c5313afeSstephan zName = randomFilename(); 840c5313afeSstephan }else if('number'===typeof zName){ 841c5313afeSstephan zName = wasm.cstringToJs(zName); 842c5313afeSstephan } 843138647a5Sstephan const fh = Object.create(null); 844138647a5Sstephan fh.fid = pFile; 845138647a5Sstephan fh.filename = zName; 846138647a5Sstephan fh.sab = new SharedArrayBuffer(state.fileBufferSize); 847138647a5Sstephan fh.flags = flags; 848138647a5Sstephan const rc = opRun('xOpen', pFile, zName, flags); 849c5313afeSstephan if(!rc){ 850c5313afeSstephan /* Recall that sqlite3_vfs::xClose() will be called, even on 851c5313afeSstephan error, unless pFile->pMethods is NULL. */ 852138647a5Sstephan if(fh.readOnly){ 853c5313afeSstephan wasm.setMemValue(pOutFlags, capi.SQLITE_OPEN_READONLY, 'i32'); 854c5313afeSstephan } 855138647a5Sstephan __openFiles[pFile] = fh; 856138647a5Sstephan fh.sabView = state.sabFileBufView; 857138647a5Sstephan fh.sq3File = new sqlite3_file(pFile); 858138647a5Sstephan fh.sq3File.$pMethods = opfsIoMethods.pointer; 8599a55773bSstephan fh.lockType = capi.SQLITE_LOCK_NONE; 860c5313afeSstephan } 861f815011aSstephan mTimeEnd(); 862c5313afeSstephan return rc; 863c5313afeSstephan }/*xOpen()*/ 864c5313afeSstephan }/*vfsSyncWrappers*/; 865c5313afeSstephan 8668766fd20Sstephan if(dVfs){ 8678766fd20Sstephan opfsVfs.$xRandomness = dVfs.$xRandomness; 8688766fd20Sstephan opfsVfs.$xSleep = dVfs.$xSleep; 8698766fd20Sstephan } 870c5313afeSstephan if(!opfsVfs.$xRandomness){ 871c5313afeSstephan /* If the default VFS has no xRandomness(), add a basic JS impl... */ 872c5313afeSstephan vfsSyncWrappers.xRandomness = function(pVfs, nOut, pOut){ 8733961b263Sstephan const heap = wasm.heap8u(); 8743961b263Sstephan let i = 0; 8753961b263Sstephan for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF; 8763961b263Sstephan return i; 877c5313afeSstephan }; 878c5313afeSstephan } 879c5313afeSstephan if(!opfsVfs.$xSleep){ 880c5313afeSstephan /* If we can inherit an xSleep() impl from the default VFS then 8818766fd20Sstephan assume it's sane and use it, otherwise install a JS-based 8828766fd20Sstephan one. */ 8838766fd20Sstephan vfsSyncWrappers.xSleep = function(pVfs,ms){ 884c4b87be3Sstephan Atomics.wait(state.sabOPView, state.opIds.xSleep, 0, ms); 8858766fd20Sstephan return 0; 8868766fd20Sstephan }; 8873961b263Sstephan } 8883961b263Sstephan 889c5313afeSstephan /* Install the vfs/io_methods into their C-level shared instances... */ 890278d3fafSstephan for(let k of Object.keys(ioSyncWrappers)){ 891278d3fafSstephan installMethod(opfsIoMethods, k, ioSyncWrappers[k]); 892278d3fafSstephan } 893278d3fafSstephan for(let k of Object.keys(vfsSyncWrappers)){ 894278d3fafSstephan installMethod(opfsVfs, k, vfsSyncWrappers[k]); 895278d3fafSstephan } 8963961b263Sstephan 897f3860120Sstephan /** 89849048b14Sstephan Expects an OPFS file path. It gets resolved, such that ".." 89949048b14Sstephan components are properly expanded, and returned. If the 2nd arg 90049048b14Sstephan is true, the result is returned as an array of path elements, 90149048b14Sstephan else an absolute path string is returned. 902f3860120Sstephan */ 90349048b14Sstephan opfsUtil.getResolvedPath = function(filename,splitIt){ 90449048b14Sstephan const p = new URL(filename, "file://irrelevant").pathname; 90549048b14Sstephan return splitIt ? p.split('/').filter((v)=>!!v) : p; 906f3860120Sstephan }; 90749048b14Sstephan 908f3860120Sstephan /** 90949048b14Sstephan Takes the absolute path to a filesystem element. Returns an 91049048b14Sstephan array of [handleOfContainingDir, filename]. If the 2nd argument 91149048b14Sstephan is truthy then each directory element leading to the file is 91249048b14Sstephan created along the way. Throws if any creation or resolution 91349048b14Sstephan fails. 91449048b14Sstephan */ 91549048b14Sstephan opfsUtil.getDirForFilename = async function f(absFilename, createDirs = false){ 91649048b14Sstephan const path = opfsUtil.getResolvedPath(absFilename, true); 91749048b14Sstephan const filename = path.pop(); 91849048b14Sstephan let dh = opfsUtil.rootDirectory; 91949048b14Sstephan for(const dirName of path){ 92049048b14Sstephan if(dirName){ 92149048b14Sstephan dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs}); 92249048b14Sstephan } 92349048b14Sstephan } 92449048b14Sstephan return [dh, filename]; 92549048b14Sstephan }; 92649048b14Sstephan 92749048b14Sstephan /** 92849048b14Sstephan Creates the given directory name, recursively, in 929f3860120Sstephan the OPFS filesystem. Returns true if it succeeds or the 930f3860120Sstephan directory already exists, else false. 931f3860120Sstephan */ 93249048b14Sstephan opfsUtil.mkdir = async function(absDirName){ 93349048b14Sstephan try { 93449048b14Sstephan await opfsUtil.getDirForFilename(absDirName+"/filepart", true); 93549048b14Sstephan return true; 93649048b14Sstephan }catch(e){ 93749048b14Sstephan //console.warn("mkdir(",absDirName,") failed:",e); 93849048b14Sstephan return false; 93949048b14Sstephan } 940f3860120Sstephan }; 941f3860120Sstephan /** 94249048b14Sstephan Checks whether the given OPFS filesystem entry exists, 943f3860120Sstephan returning true if it does, false if it doesn't. 944f3860120Sstephan */ 94549048b14Sstephan opfsUtil.entryExists = async function(fsEntryName){ 94649048b14Sstephan try { 947f45c3370Sstephan const [dh, fn] = await opfsUtil.getDirForFilename(fsEntryName); 94849048b14Sstephan await dh.getFileHandle(fn); 94949048b14Sstephan return true; 95049048b14Sstephan }catch(e){ 95149048b14Sstephan return false; 95249048b14Sstephan } 953f3860120Sstephan }; 954f3860120Sstephan 955f3860120Sstephan /** 956f3860120Sstephan Generates a random ASCII string, intended for use as a 957f3860120Sstephan temporary file name. Its argument is the length of the string, 958f3860120Sstephan defaulting to 16. 959f3860120Sstephan */ 960f3860120Sstephan opfsUtil.randomFilename = randomFilename; 961f3860120Sstephan 96256fae744Sstephan /** 96356fae744Sstephan Re-registers the OPFS VFS. This is intended only for odd use 96456fae744Sstephan cases which have to call sqlite3_shutdown() as part of their 96556fae744Sstephan initialization process, which will unregister the VFS 96656fae744Sstephan registered by installOpfsVfs(). If passed a truthy value, the 96756fae744Sstephan OPFS VFS is registered as the default VFS, else it is not made 96856fae744Sstephan the default. Returns the result of the the 96956fae744Sstephan sqlite3_vfs_register() call. 97056fae744Sstephan 97156fae744Sstephan Design note: the problem of having to re-register things after 97256fae744Sstephan a shutdown/initialize pair is more general. How to best plug 97356fae744Sstephan that in to the library is unclear. In particular, we cannot 97456fae744Sstephan hook in to any C-side calls to sqlite3_initialize(), so we 97556fae744Sstephan cannot add an after-initialize callback mechanism. 97656fae744Sstephan */ 9773d645484Sstephan opfsUtil.registerVfs = (asDefault=false)=>{ 9788948fbeeSstephan return wasm.exports.sqlite3_vfs_register( 97956fae744Sstephan opfsVfs.pointer, asDefault ? 1 : 0 98056fae744Sstephan ); 98156fae744Sstephan }; 98256fae744Sstephan 9833c272ba3Sstephan /** 98449048b14Sstephan Returns a promise which resolves to an object which represents 98549048b14Sstephan all files and directories in the OPFS tree. The top-most object 98649048b14Sstephan has two properties: `dirs` is an array of directory entries 98749048b14Sstephan (described below) and `files` is a list of file names for all 98849048b14Sstephan files in that directory. 98949048b14Sstephan 99049048b14Sstephan Traversal starts at sqlite3.opfs.rootDirectory. 99149048b14Sstephan 99249048b14Sstephan Each `dirs` entry is an object in this form: 99349048b14Sstephan 99449048b14Sstephan ``` 99549048b14Sstephan { name: directoryName, 99649048b14Sstephan dirs: [...subdirs], 99749048b14Sstephan files: [...file names] 99849048b14Sstephan } 99949048b14Sstephan ``` 100049048b14Sstephan 100149048b14Sstephan The `files` and `subdirs` entries are always set but may be 100249048b14Sstephan empty arrays. 100349048b14Sstephan 100449048b14Sstephan The returned object has the same structure but its `name` is 100549048b14Sstephan an empty string. All returned objects are created with 100649048b14Sstephan Object.create(null), so have no prototype. 100749048b14Sstephan 100849048b14Sstephan Design note: the entries do not contain more information, 100949048b14Sstephan e.g. file sizes, because getting such info is not only 101049048b14Sstephan expensive but is subject to locking-related errors. 10113c272ba3Sstephan */ 101249048b14Sstephan opfsUtil.treeList = async function(){ 101349048b14Sstephan const doDir = async function callee(dirHandle,tgt){ 101449048b14Sstephan tgt.name = dirHandle.name; 101549048b14Sstephan tgt.dirs = []; 101649048b14Sstephan tgt.files = []; 101749048b14Sstephan for await (const handle of dirHandle.values()){ 101849048b14Sstephan if('directory' === handle.kind){ 101949048b14Sstephan const subDir = Object.create(null); 102049048b14Sstephan tgt.dirs.push(subDir); 102149048b14Sstephan await callee(handle, subDir); 102249048b14Sstephan }else{ 102349048b14Sstephan tgt.files.push(handle.name); 102449048b14Sstephan } 102549048b14Sstephan } 102649048b14Sstephan }; 102749048b14Sstephan const root = Object.create(null); 102849048b14Sstephan await doDir(opfsUtil.rootDirectory, root); 102949048b14Sstephan return root; 103049048b14Sstephan }; 103149048b14Sstephan 103249048b14Sstephan /** 103349048b14Sstephan Irrevocably deletes _all_ files in the current origin's OPFS. 103449048b14Sstephan Obviously, this must be used with great caution. It may throw 103549048b14Sstephan an exception if removal of anything fails (e.g. a file is 103649048b14Sstephan locked), but the precise conditions under which it will throw 103749048b14Sstephan are not documented (so we cannot tell you what they are). 103849048b14Sstephan */ 103949048b14Sstephan opfsUtil.rmfr = async function(){ 104049048b14Sstephan const dir = opfsUtil.rootDirectory, opt = {recurse: true}; 104149048b14Sstephan for await (const handle of dir.values()){ 104249048b14Sstephan dir.removeEntry(handle.name, opt); 10433c272ba3Sstephan } 10443c272ba3Sstephan }; 10453c272ba3Sstephan 104649048b14Sstephan /** 104749048b14Sstephan Deletes the given OPFS filesystem entry. As this environment 104849048b14Sstephan has no notion of "current directory", the given name must be an 104949048b14Sstephan absolute path. If the 2nd argument is truthy, deletion is 105049048b14Sstephan recursive (use with caution!). 105149048b14Sstephan 105249048b14Sstephan The returned Promise resolves to true if the deletion was 105349048b14Sstephan successful, else false (but...). The OPFS API reports the 105449048b14Sstephan reason for the failure only in human-readable form, not 105549048b14Sstephan exceptions which can be type-checked to determine the 105649048b14Sstephan failure. Because of that... 105749048b14Sstephan 105849048b14Sstephan If the final argument is truthy then this function will 105949048b14Sstephan propagate any exception on error, rather than returning false. 106049048b14Sstephan */ 106149048b14Sstephan opfsUtil.unlink = async function(fsEntryName, recursive = false, 106249048b14Sstephan throwOnError = false){ 106349048b14Sstephan try { 106449048b14Sstephan const [hDir, filenamePart] = 106549048b14Sstephan await opfsUtil.getDirForFilename(fsEntryName, false); 106649048b14Sstephan await hDir.removeEntry(filenamePart, {recursive}); 106749048b14Sstephan return true; 106849048b14Sstephan }catch(e){ 106949048b14Sstephan if(throwOnError){ 107049048b14Sstephan throw new Error("unlink(",arguments[0],") failed: "+e.message,{ 107149048b14Sstephan cause: e 107249048b14Sstephan }); 107349048b14Sstephan } 107449048b14Sstephan return false; 107549048b14Sstephan } 107649048b14Sstephan }; 107749048b14Sstephan 107849048b14Sstephan /** 107949048b14Sstephan Traverses the OPFS filesystem, calling a callback for each one. 108049048b14Sstephan The argument may be either a callback function or an options object 108149048b14Sstephan with any of the following properties: 108249048b14Sstephan 108349048b14Sstephan - `callback`: function which gets called for each filesystem 108449048b14Sstephan entry. It gets passed 3 arguments: 1) the 108549048b14Sstephan FileSystemFileHandle or FileSystemDirectoryHandle of each 108649048b14Sstephan entry (noting that both are instanceof FileSystemHandle). 2) 108749048b14Sstephan the FileSystemDirectoryHandle of the parent directory. 3) the 108849048b14Sstephan current depth level, with 0 being at the top of the tree 108949048b14Sstephan relative to the starting directory. If the callback returns a 109049048b14Sstephan literal false, as opposed to any other falsy value, traversal 109149048b14Sstephan stops without an error. Any exceptions it throws are 109249048b14Sstephan propagated. Results are undefined if the callback manipulate 109349048b14Sstephan the filesystem (e.g. removing or adding entries) because the 109449048b14Sstephan how OPFS iterators behave in the face of such changes is 109549048b14Sstephan undocumented. 109649048b14Sstephan 109749048b14Sstephan - `recursive` [bool=true]: specifies whether to recurse into 109849048b14Sstephan subdirectories or not. Whether recursion is depth-first or 109949048b14Sstephan breadth-first is unspecified! 110049048b14Sstephan 110149048b14Sstephan - `directory` [FileSystemDirectoryEntry=sqlite3.opfs.rootDirectory] 110249048b14Sstephan specifies the starting directory. 110349048b14Sstephan 110449048b14Sstephan If this function is passed a function, it is assumed to be the 110549048b14Sstephan callback. 110649048b14Sstephan 110749048b14Sstephan Returns a promise because it has to (by virtue of being async) 110849048b14Sstephan but that promise has no specific meaning: the traversal it 110949048b14Sstephan performs is synchronous. The promise must be used to catch any 111049048b14Sstephan exceptions propagated by the callback, however. 111149048b14Sstephan 111249048b14Sstephan TODO: add an option which specifies whether to traverse 111349048b14Sstephan depth-first or breadth-first. We currently do depth-first but 111449048b14Sstephan an incremental file browsing widget would benefit more from 111549048b14Sstephan breadth-first. 111649048b14Sstephan */ 111749048b14Sstephan opfsUtil.traverse = async function(opt){ 111849048b14Sstephan const defaultOpt = { 111949048b14Sstephan recursive: true, 112049048b14Sstephan directory: opfsUtil.rootDirectory 112149048b14Sstephan }; 112249048b14Sstephan if('function'===typeof opt){ 112349048b14Sstephan opt = {callback:opt}; 112449048b14Sstephan } 112549048b14Sstephan opt = Object.assign(defaultOpt, opt||{}); 112649048b14Sstephan const doDir = async function callee(dirHandle, depth){ 112749048b14Sstephan for await (const handle of dirHandle.values()){ 112849048b14Sstephan if(false === opt.callback(handle, dirHandle, depth)) return false; 112949048b14Sstephan else if(opt.recursive && 'directory' === handle.kind){ 113049048b14Sstephan if(false === await callee(handle, depth + 1)) break; 113149048b14Sstephan } 113249048b14Sstephan } 113349048b14Sstephan }; 113449048b14Sstephan doDir(opt.directory, 0); 113549048b14Sstephan }; 113649048b14Sstephan 113749048b14Sstephan //TODO to support fiddle and worker1 db upload: 11383d645484Sstephan //opfsUtil.createFile = function(absName, content=undefined){...} 11393d645484Sstephan 1140f3860120Sstephan if(sqlite3.oo1){ 1141f3860120Sstephan opfsUtil.OpfsDb = function(...args){ 1142e681b651Sstephan const opt = sqlite3.oo1.DB.dbCtorHelper.normalizeArgs(...args); 1143f3860120Sstephan opt.vfs = opfsVfs.$zName; 1144e681b651Sstephan sqlite3.oo1.DB.dbCtorHelper.call(this, opt); 1145f3860120Sstephan }; 1146f3860120Sstephan opfsUtil.OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype); 1147e681b651Sstephan sqlite3.oo1.DB.dbCtorHelper.setVfsPostOpenSql( 11484f5bbedbSstephan opfsVfs.pointer, 1149c7fb48d4Sstephan [ 11504f5bbedbSstephan /* Truncate journal mode is faster than delete or wal for 1151c7fb48d4Sstephan this vfs, per speedtest1. */ 1152ed3182f2Sstephan "pragma journal_mode=truncate;" 1153c7fb48d4Sstephan /* 1154c7fb48d4Sstephan This vfs benefits hugely from cache on moderate/large 1155c7fb48d4Sstephan speedtest1 --size 50 and --size 100 workloads. We currently 1156c7fb48d4Sstephan rely on setting a non-default cache size when building 1157c7fb48d4Sstephan sqlite3.wasm. If that policy changes, the cache can 1158c7fb48d4Sstephan be set here. 1159c7fb48d4Sstephan */ 1160c7fb48d4Sstephan //"pragma cache_size=-8388608;" 1161c7fb48d4Sstephan ].join('') 11624f5bbedbSstephan ); 1163f3860120Sstephan } 1164f3860120Sstephan 1165f3860120Sstephan /** 1166f3860120Sstephan Potential TODOs: 1167f3860120Sstephan 1168f3860120Sstephan - Expose one or both of the Worker objects via opfsUtil and 1169f3860120Sstephan publish an interface for proxying the higher-level OPFS 1170f3860120Sstephan features like getting a directory listing. 1171f3860120Sstephan */ 11725e8bb0aaSstephan const sanityCheck = function(){ 1173c5313afeSstephan const scope = wasm.scopedAllocPush(); 1174c5313afeSstephan const sq3File = new sqlite3_file(); 1175c5313afeSstephan try{ 1176c5313afeSstephan const fid = sq3File.pointer; 1177c5313afeSstephan const openFlags = capi.SQLITE_OPEN_CREATE 1178c5313afeSstephan | capi.SQLITE_OPEN_READWRITE 1179c5313afeSstephan //| capi.SQLITE_OPEN_DELETEONCLOSE 1180c5313afeSstephan | capi.SQLITE_OPEN_MAIN_DB; 1181c5313afeSstephan const pOut = wasm.scopedAlloc(8); 1182b8c8d4e4Sstephan const dbFile = "/sanity/check/file"+randomFilename(8); 1183c5313afeSstephan const zDbFile = wasm.scopedAllocCString(dbFile); 1184c5313afeSstephan let rc; 1185e8afca3fSstephan state.s11n.serialize("This is ä string."); 1186e8afca3fSstephan rc = state.s11n.deserialize(); 1187e8afca3fSstephan log("deserialize() says:",rc); 1188e8afca3fSstephan if("This is ä string."!==rc[0]) toss("String d13n error."); 1189c5313afeSstephan vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut); 1190c5313afeSstephan rc = wasm.getMemValue(pOut,'i32'); 1191c5313afeSstephan log("xAccess(",dbFile,") exists ?=",rc); 1192c5313afeSstephan rc = vfsSyncWrappers.xOpen(opfsVfs.pointer, zDbFile, 1193c5313afeSstephan fid, openFlags, pOut); 1194c4b87be3Sstephan log("open rc =",rc,"state.sabOPView[xOpen] =", 1195c4b87be3Sstephan state.sabOPView[state.opIds.xOpen]); 1196e8afca3fSstephan if(0!==rc){ 1197c5313afeSstephan error("open failed with code",rc); 1198c5313afeSstephan return; 1199c5313afeSstephan } 1200c5313afeSstephan vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut); 1201c5313afeSstephan rc = wasm.getMemValue(pOut,'i32'); 1202c5313afeSstephan if(!rc) toss("xAccess() failed to detect file."); 1203c5313afeSstephan rc = ioSyncWrappers.xSync(sq3File.pointer, 0); 1204c5313afeSstephan if(rc) toss('sync failed w/ rc',rc); 1205c5313afeSstephan rc = ioSyncWrappers.xTruncate(sq3File.pointer, 1024); 1206c5313afeSstephan if(rc) toss('truncate failed w/ rc',rc); 1207c5313afeSstephan wasm.setMemValue(pOut,0,'i64'); 1208c5313afeSstephan rc = ioSyncWrappers.xFileSize(sq3File.pointer, pOut); 1209c5313afeSstephan if(rc) toss('xFileSize failed w/ rc',rc); 1210c5313afeSstephan log("xFileSize says:",wasm.getMemValue(pOut, 'i64')); 1211c5313afeSstephan rc = ioSyncWrappers.xWrite(sq3File.pointer, zDbFile, 10, 1); 1212c5313afeSstephan if(rc) toss("xWrite() failed!"); 1213c5313afeSstephan const readBuf = wasm.scopedAlloc(16); 1214c5313afeSstephan rc = ioSyncWrappers.xRead(sq3File.pointer, readBuf, 6, 2); 1215c5313afeSstephan wasm.setMemValue(readBuf+6,0); 1216c5313afeSstephan let jRead = wasm.cstringToJs(readBuf); 1217c5313afeSstephan log("xRead() got:",jRead); 1218c5313afeSstephan if("sanity"!==jRead) toss("Unexpected xRead() value."); 12198766fd20Sstephan if(vfsSyncWrappers.xSleep){ 1220c5313afeSstephan log("xSleep()ing before close()ing..."); 12218766fd20Sstephan vfsSyncWrappers.xSleep(opfsVfs.pointer,2000); 12228766fd20Sstephan log("waking up from xSleep()"); 12238766fd20Sstephan } 1224c5313afeSstephan rc = ioSyncWrappers.xClose(fid); 1225c4b87be3Sstephan log("xClose rc =",rc,"sabOPView =",state.sabOPView); 1226c5313afeSstephan log("Deleting file:",dbFile); 1227c5313afeSstephan vfsSyncWrappers.xDelete(opfsVfs.pointer, zDbFile, 0x1234); 1228c5313afeSstephan vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut); 1229c5313afeSstephan rc = wasm.getMemValue(pOut,'i32'); 1230c5313afeSstephan if(rc) toss("Expecting 0 from xAccess(",dbFile,") after xDelete()."); 1231c9e2602eSstephan warn("End of OPFS sanity checks."); 1232c5313afeSstephan }finally{ 1233c5313afeSstephan sq3File.dispose(); 1234c5313afeSstephan wasm.scopedAllocPop(scope); 1235c5313afeSstephan } 1236c5313afeSstephan }/*sanityCheck()*/; 1237c5313afeSstephan 1238c5313afeSstephan W.onmessage = function({data}){ 1239c5313afeSstephan //log("Worker.onmessage:",data); 1240c5313afeSstephan switch(data.type){ 1241138647a5Sstephan case 'opfs-async-loaded': 1242e8afca3fSstephan /*Arrives as soon as the asyc proxy finishes loading. 1243e8afca3fSstephan Pass our config and shared state on to the async worker.*/ 12445e8bb0aaSstephan W.postMessage({type: 'opfs-async-init',args: state}); 1245c5313afeSstephan break; 1246138647a5Sstephan case 'opfs-async-inited':{ 1247e8afca3fSstephan /*Indicates that the async partner has received the 'init' 1248e8afca3fSstephan and has finished initializing, so the real work can 1249e8afca3fSstephan begin...*/ 1250c5313afeSstephan try { 12510e0687ccSstephan const rc = capi.sqlite3_vfs_register(opfsVfs.pointer, 0); 12523961b263Sstephan if(rc){ 12533961b263Sstephan toss("sqlite3_vfs_register(OPFS) failed with rc",rc); 12543961b263Sstephan } 1255c5313afeSstephan if(opfsVfs.pointer !== capi.sqlite3_vfs_find("opfs")){ 1256c5313afeSstephan toss("BUG: sqlite3_vfs_find() failed for just-installed OPFS VFS"); 1257c5313afeSstephan } 1258c5313afeSstephan capi.sqlite3_vfs_register.addReference(opfsVfs, opfsIoMethods); 1259c4b87be3Sstephan state.sabOPView = new Int32Array(state.sabOP); 1260138647a5Sstephan state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize); 1261138647a5Sstephan state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize); 1262138647a5Sstephan initS11n(); 1263c5313afeSstephan if(options.sanityChecks){ 1264c5313afeSstephan warn("Running sanity checks because of opfs-sanity-check URL arg..."); 1265c5313afeSstephan sanityCheck(); 1266c5313afeSstephan } 12671f095d48Sstephan navigator.storage.getDirectory().then((d)=>{ 1268f3860120Sstephan W.onerror = W._originalOnError; 1269f3860120Sstephan delete W._originalOnError; 1270f3860120Sstephan sqlite3.opfs = opfsUtil; 12711f095d48Sstephan opfsUtil.rootDirectory = d; 1272c5313afeSstephan log("End of OPFS sqlite3_vfs setup.", opfsVfs); 1273509f4052Sstephan promiseResolve(sqlite3); 12741f095d48Sstephan }); 1275c5313afeSstephan }catch(e){ 1276c5313afeSstephan error(e); 1277c5313afeSstephan promiseReject(e); 1278c5313afeSstephan } 1279c5313afeSstephan break; 1280c5313afeSstephan } 1281c5313afeSstephan default: 1282c5313afeSstephan promiseReject(e); 1283c5313afeSstephan error("Unexpected message from the async worker:",data); 1284c5313afeSstephan break; 1285f861b36bSstephan }/*switch(data.type)*/ 1286f861b36bSstephan }/*W.onmessage()*/; 1287c5313afeSstephan })/*thePromise*/; 1288c5313afeSstephan return thePromise; 1289c5313afeSstephan}/*installOpfsVfs()*/; 12905b9973d8SstephaninstallOpfsVfs.defaultProxyUri = 12915b9973d8Sstephan "sqlite3-opfs-async-proxy.js"; 12929a55773bSstephanself.sqlite3ApiBootstrap.initializersAsync.push(async (sqlite3)=>{ 1293cd0df83cSstephan if(sqlite3.scriptInfo && !sqlite3.scriptInfo.isWorker){ 1294cd0df83cSstephan return; 1295cd0df83cSstephan } 12969a55773bSstephan try{ 1297cd0df83cSstephan let proxyJs = installOpfsVfs.defaultProxyUri; 1298cd0df83cSstephan if(sqlite3.scriptInfo.sqlite3Dir){ 1299cd0df83cSstephan installOpfsVfs.defaultProxyUri = 1300cd0df83cSstephan sqlite3.scriptInfo.sqlite3Dir + proxyJs; 1301cd0df83cSstephan //console.warn("installOpfsVfs.defaultProxyUri =",installOpfsVfs.defaultProxyUri); 1302cd0df83cSstephan } 1303ff891b4eSstephan return installOpfsVfs().catch((e)=>{ 1304cd0df83cSstephan console.warn("Ignoring inability to install OPFS sqlite3_vfs:",e.message); 1305ff891b4eSstephan }); 13069a55773bSstephan }catch(e){ 13079a55773bSstephan console.error("installOpfsVfs() exception:",e); 13089a55773bSstephan throw e; 13099a55773bSstephan } 13109a55773bSstephan}); 1311c5313afeSstephan}/*sqlite3ApiBootstrap.initializers.push()*/); 1312