blob: a0630d8e760c99fc4636c578ad31c9d9498cb7ca [file] [log] [blame]
stephan3961b262022-08-10 11:26:08 +00001/*
stephanc5313af2022-09-18 02:35:30 +00002 2022-09-18
stephan3961b262022-08-10 11:26:08 +00003
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
stephanc5313af2022-09-18 02:35:30 +000013 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
stephan3961b262022-08-10 11:26:08 +000037
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
stephanc5313af2022-09-18 02:35:30 +000044 - The OPFS features used here are only available in dedicated Worker
stephanf3860122022-09-18 17:32:35 +000045 threads. This file tries to detect that case, resulting in a
46 rejected Promise if those features do not seem to be available.
stephanc5313af2022-09-18 02:35:30 +000047
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
stephanf3860122022-09-18 17:32:35 +000075 object and that object gets a new object installed in its
76 `opfs` property, containing several OPFS-specific utilities.
stephan3961b262022-08-10 11:26:08 +000077*/
stephanc5313af2022-09-18 02:35:30 +000078sqlite3.installOpfsVfs = function callee(asyncProxyUri = callee.defaultProxyUri){
stephan509f4052022-09-19 09:58:01 +000079 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 }
stephanc5313af2022-09-18 02:35:30 +000091 const options = (asyncProxyUri && 'object'===asyncProxyUri) ? asyncProxyUri : {
92 proxyUri: asyncProxyUri
stephan3961b262022-08-10 11:26:08 +000093 };
stephan509f4052022-09-19 09:58:01 +000094 const urlParams = new URL(self.location.href).searchParams;
stephanc5313af2022-09-18 02:35:30 +000095 if(undefined===options.verbose){
stephan509f4052022-09-19 09:58:01 +000096 options.verbose = urlParams.has('opfs-verbose') ? 3 : 2;
stephan3961b262022-08-10 11:26:08 +000097 }
stephanc5313af2022-09-18 02:35:30 +000098 if(undefined===options.sanityChecks){
stephan509f4052022-09-19 09:58:01 +000099 options.sanityChecks = urlParams.has('opfs-sanity-check');
stephanc5313af2022-09-18 02:35:30 +0000100 }
101 if(undefined===options.proxyUri){
102 options.proxyUri = callee.defaultProxyUri;
103 }
stephanf3860122022-09-18 17:32:35 +0000104
stephanc5313af2022-09-18 02:35:30 +0000105 const thePromise = new Promise(function(promiseResolve, promiseReject){
stephan509f4052022-09-19 09:58:01 +0000106 const loggers = {
107 0:console.error.bind(console),
108 1:console.warn.bind(console),
109 2:console.log.bind(console)
stephanc5313af2022-09-18 02:35:30 +0000110 };
stephan509f4052022-09-19 09:58:01 +0000111 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);
stephanc5313af2022-09-18 02:35:30 +0000117 warn("The OPFS VFS feature is very much experimental and under construction.");
118 const toss = function(...args){throw new Error(args.join(' '))};
stephanc5313af2022-09-18 02:35:30 +0000119 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;
stephanc5313af2022-09-18 02:35:30 +0000124 const W = new Worker(options.proxyUri);
stephanf3860122022-09-18 17:32:35 +0000125 W._originalOnError = W.onerror /* will be restored later */;
stephanc5313af2022-09-18 02:35:30 +0000126 W.onerror = function(err){
stephan509f4052022-09-19 09:58:01 +0000127 // The error object doesn't contain any useful info when the
128 // failure is, e.g., that the remote script is 404.
stephanc5313af2022-09-18 02:35:30 +0000129 promiseReject(new Error("Loading OPFS async Worker failed for unknown reasons."));
130 };
stephan509f4052022-09-19 09:58:01 +0000131 /**
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);
stephanf8150112022-09-19 17:09:09 +0000136 /**
137 Not part of the public API. Solely for internal/development
138 use.
139 */
140 opfsUtil.metrics = {
141 dump: function(){
stephanaec046a2022-09-19 18:22:29 +0000142 let k, n = 0, t = 0, w = 0;
143 for(k in state.opIds){
stephanf8150112022-09-19 17:09:09 +0000144 const m = metrics[k];
145 n += m.count;
146 t += m.time;
stephanaec046a2022-09-19 18:22:29 +0000147 w += m.wait;
stephanf8150112022-09-19 17:09:09 +0000148 m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
149 m.avgWait = (m.count && m.wait) ? (m.wait / m.count) : 0;
150 }
stephanaec046a2022-09-19 18:22:29 +0000151 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)");
stephanf8150112022-09-19 17:09:09 +0000155 },
156 reset: function(){
157 let k;
158 const r = (m)=>(m.count = m.time = m.wait = 0);
159 for(k in state.opIds){
160 r(metrics[k] = Object.create(null));
161 }
stephanaec046a2022-09-19 18:22:29 +0000162 //[ // timed routines which are not in state.opIds
163 // 'xFileControl'
164 //].forEach((k)=>r(metrics[k] = Object.create(null)));
stephanf8150112022-09-19 17:09:09 +0000165 }
166 }/*metrics*/;
stephanc5313af2022-09-18 02:35:30 +0000167
stephanc9e26022022-09-20 10:11:52 +0000168 const pDVfs = capi.sqlite3_vfs_find(null)/*pointer to default VFS*/;
169 const dVfs = pDVfs
170 ? new sqlite3_vfs(pDVfs)
171 : null /* dVfs will be null when sqlite3 is built with
172 SQLITE_OS_OTHER. Though we cannot currently handle
173 that case, the hope is to eventually be able to. */;
174 const opfsVfs = new sqlite3_vfs();
175 const opfsIoMethods = new sqlite3_io_methods();
176 opfsVfs.$iVersion = 2/*yes, two*/;
177 opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof;
178 opfsVfs.$mxPathname = 1024/*sure, why not?*/;
179 opfsVfs.$zName = wasm.allocCString("opfs");
180 // All C-side memory of opfsVfs is zeroed out, but just to be explicit:
181 opfsVfs.$xDlOpen = opfsVfs.$xDlError = opfsVfs.$xDlSym = opfsVfs.$xDlClose = null;
182 opfsVfs.ondispose = [
183 '$zName', opfsVfs.$zName,
184 'cleanup default VFS wrapper', ()=>(dVfs ? dVfs.dispose() : null),
185 'cleanup opfsIoMethods', ()=>opfsIoMethods.dispose()
186 ];
187 /**
188 Pedantic sidebar about opfsVfs.ondispose: the entries in that array
189 are items to clean up when opfsVfs.dispose() is called, but in this
190 environment it will never be called. The VFS instance simply
191 hangs around until the WASM module instance is cleaned up. We
192 "could" _hypothetically_ clean it up by "importing" an
193 sqlite3_os_end() impl into the wasm build, but the shutdown order
194 of the wasm engine and the JS one are undefined so there is no
195 guaranty that the opfsVfs instance would be available in one
196 environment or the other when sqlite3_os_end() is called (_if_ it
197 gets called at all in a wasm build, which is undefined).
198 */
199
stephanc5313af2022-09-18 02:35:30 +0000200 /**
201 State which we send to the async-api Worker or share with it.
202 This object must initially contain only cloneable or sharable
203 objects. After the worker's "inited" message arrives, other types
204 of data may be added to it.
stephanf3860122022-09-18 17:32:35 +0000205
206 For purposes of Atomics.wait() and Atomics.notify(), we use a
207 SharedArrayBuffer with one slot reserved for each of the API
208 proxy's methods. The sync side of the API uses Atomics.wait()
209 on the corresponding slot and the async side uses
210 Atomics.notify() on that slot.
211
212 The approach of using a single SAB to serialize comms for all
213 instances might(?) lead to deadlock situations in multi-db
214 cases. We should probably have one SAB here with a single slot
215 for locking a per-file initialization step and then allocate a
216 separate SAB like the above one for each file. That will
217 require a bit of acrobatics but should be feasible.
stephanc5313af2022-09-18 02:35:30 +0000218 */
219 const state = Object.create(null);
stephan138647a2022-09-20 03:31:02 +0000220 state.littleEndian = true;
stephanc5313af2022-09-18 02:35:30 +0000221 state.verbose = options.verbose;
stephanc9e26022022-09-20 10:11:52 +0000222 /* Size of file I/O buffer block. 64k = max sqlite3 page size. */
stephanf3860122022-09-18 17:32:35 +0000223 state.fileBufferSize =
stephanc9e26022022-09-20 10:11:52 +0000224 1024 * 64;
stephan138647a2022-09-20 03:31:02 +0000225 state.sabS11nOffset = state.fileBufferSize;
stephanc9e26022022-09-20 10:11:52 +0000226 /**
227 The size of the block in our SAB for serializing arguments and
228 result values. Need to be large enough to hold serialized
229 values of any of the proxied APIs. Filenames are the largest
230 part but are limited to opfsVfs.$mxPathname bytes.
231 */
232 state.sabS11nSize = opfsVfs.$mxPathname * 2;
233 /**
234 The SAB used for all data I/O (files and arg/result s11n).
235 */
stephanc4b87be2022-09-20 01:28:47 +0000236 state.sabIO = new SharedArrayBuffer(
stephanc9e26022022-09-20 10:11:52 +0000237 state.fileBufferSize/* file i/o block */
238 + state.sabS11nSize/* argument/result serialization block */
stephanc4b87be2022-09-20 01:28:47 +0000239 );
stephanc5313af2022-09-18 02:35:30 +0000240 state.opIds = Object.create(null);
stephanf8150112022-09-19 17:09:09 +0000241 const metrics = Object.create(null);
stephanc5313af2022-09-18 02:35:30 +0000242 {
stephanc9e26022022-09-20 10:11:52 +0000243 /* Indexes for use in our SharedArrayBuffer... */
stephan3961b262022-08-10 11:26:08 +0000244 let i = 0;
stephanc9e26022022-09-20 10:11:52 +0000245 /* SAB slot used to communicate which operation is desired
246 between both workers. This worker writes to it and the other
247 listens for changes. */
stephan138647a2022-09-20 03:31:02 +0000248 state.opIds.whichOp = i++;
stephanc9e26022022-09-20 10:11:52 +0000249 /* Slot for storing return values. This work listens to that
250 slot and the other worker writes to it. */
251 state.opIds.rc = i++;
252 /* Each function gets an ID which this worker writes to
253 the whichOp slot. The async-api worker uses Atomic.wait()
254 on the whichOp slot to figure out which operation to run
255 next. */
stephanc5313af2022-09-18 02:35:30 +0000256 state.opIds.xAccess = i++;
257 state.opIds.xClose = i++;
258 state.opIds.xDelete = i++;
stephanf3860122022-09-18 17:32:35 +0000259 state.opIds.xDeleteNoWait = i++;
stephanc5313af2022-09-18 02:35:30 +0000260 state.opIds.xFileSize = i++;
261 state.opIds.xOpen = i++;
262 state.opIds.xRead = i++;
263 state.opIds.xSleep = i++;
264 state.opIds.xSync = i++;
265 state.opIds.xTruncate = i++;
266 state.opIds.xWrite = i++;
stephanf3860122022-09-18 17:32:35 +0000267 state.opIds.mkdir = i++;
stephan5e8bb0a2022-09-20 08:27:57 +0000268 state.opIds.xFileControl = i++;
stephanc4b87be2022-09-20 01:28:47 +0000269 state.sabOP = new SharedArrayBuffer(i * 4/*sizeof int32*/);
stephanf8150112022-09-19 17:09:09 +0000270 opfsUtil.metrics.reset();
stephanc5313af2022-09-18 02:35:30 +0000271 }
272
stephanc9e26022022-09-20 10:11:52 +0000273 /**
274 SQLITE_xxx constants to export to the async worker
275 counterpart...
276 */
stephanc5313af2022-09-18 02:35:30 +0000277 state.sq3Codes = Object.create(null);
278 state.sq3Codes._reverse = Object.create(null);
stephanc9e26022022-09-20 10:11:52 +0000279 [
stephanc5313af2022-09-18 02:35:30 +0000280 'SQLITE_ERROR', 'SQLITE_IOERR',
281 'SQLITE_NOTFOUND', 'SQLITE_MISUSE',
282 'SQLITE_IOERR_READ', 'SQLITE_IOERR_SHORT_READ',
283 'SQLITE_IOERR_WRITE', 'SQLITE_IOERR_FSYNC',
284 'SQLITE_IOERR_TRUNCATE', 'SQLITE_IOERR_DELETE',
stephanf3860122022-09-18 17:32:35 +0000285 'SQLITE_IOERR_ACCESS', 'SQLITE_IOERR_CLOSE',
stephanc4b87be2022-09-20 01:28:47 +0000286 'SQLITE_IOERR_DELETE',
287 'SQLITE_OPEN_CREATE', 'SQLITE_OPEN_DELETEONCLOSE',
288 'SQLITE_OPEN_READONLY'
stephanc5313af2022-09-18 02:35:30 +0000289 ].forEach(function(k){
290 state.sq3Codes[k] = capi[k] || toss("Maintenance required: not found:",k);
291 state.sq3Codes._reverse[capi[k]] = k;
stephan3961b262022-08-10 11:26:08 +0000292 });
stephan3961b262022-08-10 11:26:08 +0000293
stephanc5313af2022-09-18 02:35:30 +0000294 const isWorkerErrCode = (n)=>!!state.sq3Codes._reverse[n];
stephan3961b262022-08-10 11:26:08 +0000295
stephanc5313af2022-09-18 02:35:30 +0000296 /**
stephanc9e26022022-09-20 10:11:52 +0000297 Runs the given operation (by name) in the async worker
298 counterpart, waits for its response, and returns the result
299 which the async worker writes to SAB[state.opIds.rc]. The
300 2nd and subsequent arguments must be the aruguments for the
301 async op.
stephanc5313af2022-09-18 02:35:30 +0000302 */
stephan138647a2022-09-20 03:31:02 +0000303 const opRun = (op,...args)=>{
stephan5e8bb0a2022-09-20 08:27:57 +0000304 const opNdx = state.opIds[op] || toss("Invalid op ID:",op);
305 state.s11n.serialize(...args);
stephanc9e26022022-09-20 10:11:52 +0000306 Atomics.store(state.sabOPView, state.opIds.rc, -1);
stephan5e8bb0a2022-09-20 08:27:57 +0000307 Atomics.store(state.sabOPView, state.opIds.whichOp, opNdx);
308 Atomics.notify(state.sabOPView, state.opIds.whichOp) /* async thread will take over here */;
stephanf8150112022-09-19 17:09:09 +0000309 const t = performance.now();
stephanc9e26022022-09-20 10:11:52 +0000310 Atomics.wait(state.sabOPView, state.opIds.rc, -1);
311 const rc = Atomics.load(state.sabOPView, state.opIds.rc);
stephanf8150112022-09-19 17:09:09 +0000312 metrics[op].wait += performance.now() - t;
stephan5e8bb0a2022-09-20 08:27:57 +0000313 return rc;
stephanc5313af2022-09-18 02:35:30 +0000314 };
315
stephan138647a2022-09-20 03:31:02 +0000316 const initS11n = ()=>{
317 // Achtung: this code is 100% duplicated in the other half of this proxy!
318 if(state.s11n) return state.s11n;
319 const jsonDecoder = new TextDecoder(),
320 jsonEncoder = new TextEncoder('utf-8'),
321 viewSz = new DataView(state.sabIO, state.sabS11nOffset, 4),
322 viewJson = new Uint8Array(state.sabIO, state.sabS11nOffset+4, state.sabS11nSize-4);
323 state.s11n = Object.create(null);
324 /**
325 Returns an array of the state serialized by the most recent
326 serialize() operation (here or in the counterpart thread), or
327 null if the serialization buffer is empty.
328 */
329 state.s11n.deserialize = function(){
330 const sz = viewSz.getInt32(0, state.littleEndian);
331 const json = sz ? jsonDecoder.decode(
332 viewJson.slice(0, sz)
333 /* slice() (copy) needed, instead of subarray() (reference),
334 because TextDecoder throws if asked to decode from an
335 SAB. */
336 ) : null;
337 return JSON.parse(json);
338 }
339 /**
340 Serializes all arguments to the shared buffer for consumption
341 by the counterpart thread. This impl currently uses JSON for
342 serialization for simplicy of implementation, but if that
343 proves imperformant then a lower-level approach will be
344 created.
stephan5e8bb0a2022-09-20 08:27:57 +0000345
346 If passed "too much data" (more that the shared buffer size
347 it will either throw or truncate the data (not certain
348 which)). This routine is only intended for serializing OPFS
349 VFS arguments and (in at least one special case) result
350 values, and the buffer is sized to be able to comfortably
351 handle those.
352
353 If passed no arguments then it zeroes out the serialization
354 state.
stephan138647a2022-09-20 03:31:02 +0000355 */
356 state.s11n.serialize = function(...args){
stephan5e8bb0a2022-09-20 08:27:57 +0000357 if(args.length){
358 const json = jsonEncoder.encode(JSON.stringify(args));
359 viewSz.setInt32(0, json.byteLength, state.littleEndian);
360 viewJson.set(json);
361 }else{
362 viewSz.setInt32(0, 0, state.littleEndian);
363 }
stephan138647a2022-09-20 03:31:02 +0000364 };
365 return state.s11n;
366 };
367
stephanc5313af2022-09-18 02:35:30 +0000368 /**
369 Generates a random ASCII string len characters long, intended for
370 use as a temporary file name.
371 */
372 const randomFilename = function f(len=16){
373 if(!f._chars){
374 f._chars = "abcdefghijklmnopqrstuvwxyz"+
375 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+
376 "012346789";
377 f._n = f._chars.length;
378 }
379 const a = [];
380 let i = 0;
381 for( ; i < len; ++i){
382 const ndx = Math.random() * (f._n * 64) % f._n | 0;
383 a[i] = f._chars[ndx];
384 }
385 return a.join('');
386 };
387
388 /**
389 Map of sqlite3_file pointers to objects constructed by xOpen().
390 */
391 const __openFiles = Object.create(null);
stephanc5313af2022-09-18 02:35:30 +0000392
393 /**
394 Installs a StructBinder-bound function pointer member of the
395 given name and function in the given StructType target object.
396 It creates a WASM proxy for the given function and arranges for
397 that proxy to be cleaned up when tgt.dispose() is called. Throws
398 on the slightest hint of error (e.g. tgt is-not-a StructType,
399 name does not map to a struct-bound member, etc.).
400
401 Returns a proxy for this function which is bound to tgt and takes
402 2 args (name,func). That function returns the same thing,
403 permitting calls to be chained.
404
405 If called with only 1 arg, it has no side effects but returns a
406 func with the same signature as described above.
407 */
408 const installMethod = function callee(tgt, name, func){
stephanf3860122022-09-18 17:32:35 +0000409 if(!(tgt instanceof sqlite3.StructBinder.StructType)){
stephanc5313af2022-09-18 02:35:30 +0000410 toss("Usage error: target object is-not-a StructType.");
411 }
412 if(1===arguments.length){
413 return (n,f)=>callee(tgt,n,f);
414 }
415 if(!callee.argcProxy){
416 callee.argcProxy = function(func,sig){
417 return function(...args){
418 if(func.length!==arguments.length){
419 toss("Argument mismatch. Native signature is:",sig);
420 }
421 return func.apply(this, args);
422 }
423 };
424 callee.removeFuncList = function(){
425 if(this.ondispose.__removeFuncList){
426 this.ondispose.__removeFuncList.forEach(
427 (v,ndx)=>{
428 if('number'===typeof v){
429 try{wasm.uninstallFunction(v)}
430 catch(e){/*ignore*/}
431 }
432 /* else it's a descriptive label for the next number in
433 the list. */
434 }
435 );
436 delete this.ondispose.__removeFuncList;
437 }
438 };
439 }/*static init*/
440 const sigN = tgt.memberSignature(name);
441 if(sigN.length<2){
442 toss("Member",name," is not a function pointer. Signature =",sigN);
443 }
444 const memKey = tgt.memberKey(name);
445 //log("installMethod",tgt, name, sigN);
446 const fProxy = 1
447 // We can remove this proxy middle-man once the VFS is working
448 ? callee.argcProxy(func, sigN)
449 : func;
450 const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
451 tgt[memKey] = pFunc;
452 if(!tgt.ondispose) tgt.ondispose = [];
453 if(!tgt.ondispose.__removeFuncList){
454 tgt.ondispose.push('ondispose.__removeFuncList handler',
455 callee.removeFuncList);
456 tgt.ondispose.__removeFuncList = [];
457 }
458 tgt.ondispose.__removeFuncList.push(memKey, pFunc);
459 return (n,f)=>callee(tgt, n, f);
460 }/*installMethod*/;
stephanf8150112022-09-19 17:09:09 +0000461
462 const opTimer = Object.create(null);
463 opTimer.op = undefined;
464 opTimer.start = undefined;
465 const mTimeStart = (op)=>{
466 opTimer.start = performance.now();
467 opTimer.op = op;
468 //metrics[op] || toss("Maintenance required: missing metrics for",op);
469 ++metrics[op].count;
470 };
471 const mTimeEnd = ()=>(
472 metrics[opTimer.op].time += performance.now() - opTimer.start
473 );
474
stephanc5313af2022-09-18 02:35:30 +0000475 /**
476 Impls for the sqlite3_io_methods methods. Maintenance reminder:
477 members are in alphabetical order to simplify finding them.
478 */
479 const ioSyncWrappers = {
480 xCheckReservedLock: function(pFile,pOut){
481 // Exclusive lock is automatically acquired when opened
482 //warn("xCheckReservedLock(",arguments,") is a no-op");
483 wasm.setMemValue(pOut,1,'i32');
484 return 0;
485 },
486 xClose: function(pFile){
stephanf8150112022-09-19 17:09:09 +0000487 mTimeStart('xClose');
stephanc5313af2022-09-18 02:35:30 +0000488 let rc = 0;
489 const f = __openFiles[pFile];
490 if(f){
491 delete __openFiles[pFile];
492 rc = opRun('xClose', pFile);
493 if(f.sq3File) f.sq3File.dispose();
494 }
stephanf8150112022-09-19 17:09:09 +0000495 mTimeEnd();
stephanc5313af2022-09-18 02:35:30 +0000496 return rc;
497 },
498 xDeviceCharacteristics: function(pFile){
499 //debug("xDeviceCharacteristics(",pFile,")");
500 return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
501 },
stephanf8150112022-09-19 17:09:09 +0000502 xFileControl: function(pFile, opId, pArg){
503 mTimeStart('xFileControl');
stephanaec046a2022-09-19 18:22:29 +0000504 const rc = (capi.SQLITE_FCNTL_SYNC===opId)
stephan138647a2022-09-20 03:31:02 +0000505 ? opRun('xSync', pFile, 0)
stephanaec046a2022-09-19 18:22:29 +0000506 : capi.SQLITE_NOTFOUND;
stephanf8150112022-09-19 17:09:09 +0000507 mTimeEnd();
stephanaec046a2022-09-19 18:22:29 +0000508 return rc;
stephanc5313af2022-09-18 02:35:30 +0000509 },
510 xFileSize: function(pFile,pSz64){
stephanf8150112022-09-19 17:09:09 +0000511 mTimeStart('xFileSize');
stephanc5313af2022-09-18 02:35:30 +0000512 const rc = opRun('xFileSize', pFile);
513 if(!isWorkerErrCode(rc)){
stephan138647a2022-09-20 03:31:02 +0000514 const sz = state.s11n.deserialize()[0];
515 wasm.setMemValue(pSz64, BigInt(sz), 'i64');
stephanc5313af2022-09-18 02:35:30 +0000516 }
stephanf8150112022-09-19 17:09:09 +0000517 mTimeEnd();
stephanc5313af2022-09-18 02:35:30 +0000518 return rc;
519 },
520 xLock: function(pFile,lockType){
521 //2022-09: OPFS handles lock when opened
522 //warn("xLock(",arguments,") is a no-op");
523 return 0;
524 },
stephan138647a2022-09-20 03:31:02 +0000525 xRead: function(pFile,pDest,n,offset64){
stephanc5313af2022-09-18 02:35:30 +0000526 /* int (*xRead)(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst) */
stephanf8150112022-09-19 17:09:09 +0000527 mTimeStart('xRead');
stephanc5313af2022-09-18 02:35:30 +0000528 const f = __openFiles[pFile];
529 let rc;
530 try {
stephan138647a2022-09-20 03:31:02 +0000531 rc = opRun('xRead',pFile, n, Number(offset64));
stephan862281f2022-09-19 09:25:25 +0000532 if(0===rc || capi.SQLITE_IOERR_SHORT_READ===rc){
stephanf8150112022-09-19 17:09:09 +0000533 // set() seems to be the fastest way to copy this...
534 wasm.heap8u().set(f.sabView.subarray(0, n), pDest);
stephan862281f2022-09-19 09:25:25 +0000535 }
stephanc5313af2022-09-18 02:35:30 +0000536 }catch(e){
537 error("xRead(",arguments,") failed:",e,f);
538 rc = capi.SQLITE_IOERR_READ;
539 }
stephanf8150112022-09-19 17:09:09 +0000540 mTimeEnd();
stephanc5313af2022-09-18 02:35:30 +0000541 return rc;
542 },
543 xSync: function(pFile,flags){
stephanaec046a2022-09-19 18:22:29 +0000544 ++metrics.xSync.count;
stephan138647a2022-09-20 03:31:02 +0000545 return 0; // impl'd in xFileControl()
stephanc5313af2022-09-18 02:35:30 +0000546 },
547 xTruncate: function(pFile,sz64){
stephanf8150112022-09-19 17:09:09 +0000548 mTimeStart('xTruncate');
stephan138647a2022-09-20 03:31:02 +0000549 const rc = opRun('xTruncate', pFile, Number(sz64));
stephanf8150112022-09-19 17:09:09 +0000550 mTimeEnd();
551 return rc;
stephanc5313af2022-09-18 02:35:30 +0000552 },
553 xUnlock: function(pFile,lockType){
554 //2022-09: OPFS handles lock when opened
555 //warn("xUnlock(",arguments,") is a no-op");
556 return 0;
557 },
stephan138647a2022-09-20 03:31:02 +0000558 xWrite: function(pFile,pSrc,n,offset64){
stephanc5313af2022-09-18 02:35:30 +0000559 /* int (*xWrite)(sqlite3_file*, const void*, int iAmt, sqlite3_int64 iOfst) */
stephanf8150112022-09-19 17:09:09 +0000560 mTimeStart('xWrite');
stephanc5313af2022-09-18 02:35:30 +0000561 const f = __openFiles[pFile];
stephanf8150112022-09-19 17:09:09 +0000562 let rc;
stephanc5313af2022-09-18 02:35:30 +0000563 try {
stephanf8150112022-09-19 17:09:09 +0000564 f.sabView.set(wasm.heap8u().subarray(pSrc, pSrc+n));
stephan138647a2022-09-20 03:31:02 +0000565 rc = opRun('xWrite', pFile, n, Number(offset64));
stephanc5313af2022-09-18 02:35:30 +0000566 }catch(e){
567 error("xWrite(",arguments,") failed:",e,f);
stephanf8150112022-09-19 17:09:09 +0000568 rc = capi.SQLITE_IOERR_WRITE;
stephanc5313af2022-09-18 02:35:30 +0000569 }
stephanf8150112022-09-19 17:09:09 +0000570 mTimeEnd();
571 return rc;
stephanc5313af2022-09-18 02:35:30 +0000572 }
573 }/*ioSyncWrappers*/;
574
575 /**
576 Impls for the sqlite3_vfs methods. Maintenance reminder: members
577 are in alphabetical order to simplify finding them.
578 */
579 const vfsSyncWrappers = {
580 xAccess: function(pVfs,zName,flags,pOut){
stephanf8150112022-09-19 17:09:09 +0000581 mTimeStart('xAccess');
stephan5e8bb0a2022-09-20 08:27:57 +0000582 const rc = opRun('xAccess', wasm.cstringToJs(zName));
583 wasm.setMemValue( pOut, (rc ? 0 : 1), 'i32' );
stephanf8150112022-09-19 17:09:09 +0000584 mTimeEnd();
stephanc5313af2022-09-18 02:35:30 +0000585 return 0;
586 },
587 xCurrentTime: function(pVfs,pOut){
588 /* If it turns out that we need to adjust for timezone, see:
589 https://stackoverflow.com/a/11760121/1458521 */
590 wasm.setMemValue(pOut, 2440587.5 + (new Date().getTime()/86400000),
591 'double');
592 return 0;
593 },
594 xCurrentTimeInt64: function(pVfs,pOut){
595 // TODO: confirm that this calculation is correct
596 wasm.setMemValue(pOut, (2440587.5 * 86400000) + new Date().getTime(),
597 'i64');
598 return 0;
599 },
600 xDelete: function(pVfs, zName, doSyncDir){
stephanf8150112022-09-19 17:09:09 +0000601 mTimeStart('xDelete');
stephan138647a2022-09-20 03:31:02 +0000602 opRun('xDelete', wasm.cstringToJs(zName), doSyncDir, false);
stephanf3860122022-09-18 17:32:35 +0000603 /* We're ignoring errors because we cannot yet differentiate
604 between harmless and non-harmless failures. */
stephanf8150112022-09-19 17:09:09 +0000605 mTimeEnd();
stephanf3860122022-09-18 17:32:35 +0000606 return 0;
stephanc5313af2022-09-18 02:35:30 +0000607 },
608 xFullPathname: function(pVfs,zName,nOut,pOut){
609 /* Until/unless we have some notion of "current dir"
610 in OPFS, simply copy zName to pOut... */
611 const i = wasm.cstrncpy(pOut, zName, nOut);
612 return i<nOut ? 0 : capi.SQLITE_CANTOPEN
613 /*CANTOPEN is required by the docs but SQLITE_RANGE would be a closer match*/;
614 },
615 xGetLastError: function(pVfs,nOut,pOut){
616 /* TODO: store exception.message values from the async
617 partner in a dedicated SharedArrayBuffer, noting that we'd have
618 to encode them... TextEncoder can do that for us. */
619 warn("OPFS xGetLastError() has nothing sensible to return.");
620 return 0;
621 },
stephan8766fd22022-09-19 05:19:04 +0000622 //xSleep is optionally defined below
stephanc5313af2022-09-18 02:35:30 +0000623 xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){
stephanf8150112022-09-19 17:09:09 +0000624 mTimeStart('xOpen');
stephanc5313af2022-09-18 02:35:30 +0000625 if(!f._){
626 f._ = {
627 fileTypes: {
628 SQLITE_OPEN_MAIN_DB: 'mainDb',
629 SQLITE_OPEN_MAIN_JOURNAL: 'mainJournal',
630 SQLITE_OPEN_TEMP_DB: 'tempDb',
631 SQLITE_OPEN_TEMP_JOURNAL: 'tempJournal',
632 SQLITE_OPEN_TRANSIENT_DB: 'transientDb',
633 SQLITE_OPEN_SUBJOURNAL: 'subjournal',
634 SQLITE_OPEN_SUPER_JOURNAL: 'superJournal',
635 SQLITE_OPEN_WAL: 'wal'
636 },
637 getFileType: function(filename,oflags){
638 const ft = f._.fileTypes;
639 for(let k of Object.keys(ft)){
640 if(oflags & capi[k]) return ft[k];
641 }
642 warn("Cannot determine fileType based on xOpen() flags for file",filename);
643 return '???';
644 }
645 };
646 }
647 if(0===zName){
648 zName = randomFilename();
649 }else if('number'===typeof zName){
650 zName = wasm.cstringToJs(zName);
651 }
stephan138647a2022-09-20 03:31:02 +0000652 const fh = Object.create(null);
653 fh.fid = pFile;
654 fh.filename = zName;
655 fh.sab = new SharedArrayBuffer(state.fileBufferSize);
656 fh.flags = flags;
657 const rc = opRun('xOpen', pFile, zName, flags);
stephanc5313af2022-09-18 02:35:30 +0000658 if(!rc){
659 /* Recall that sqlite3_vfs::xClose() will be called, even on
660 error, unless pFile->pMethods is NULL. */
stephan138647a2022-09-20 03:31:02 +0000661 if(fh.readOnly){
stephanc5313af2022-09-18 02:35:30 +0000662 wasm.setMemValue(pOutFlags, capi.SQLITE_OPEN_READONLY, 'i32');
663 }
stephan138647a2022-09-20 03:31:02 +0000664 __openFiles[pFile] = fh;
665 fh.sabView = state.sabFileBufView;
666 fh.sq3File = new sqlite3_file(pFile);
667 fh.sq3File.$pMethods = opfsIoMethods.pointer;
stephanc5313af2022-09-18 02:35:30 +0000668 }
stephanf8150112022-09-19 17:09:09 +0000669 mTimeEnd();
stephanc5313af2022-09-18 02:35:30 +0000670 return rc;
671 }/*xOpen()*/
672 }/*vfsSyncWrappers*/;
673
stephan8766fd22022-09-19 05:19:04 +0000674 if(dVfs){
675 opfsVfs.$xRandomness = dVfs.$xRandomness;
676 opfsVfs.$xSleep = dVfs.$xSleep;
677 }
stephanc5313af2022-09-18 02:35:30 +0000678 if(!opfsVfs.$xRandomness){
679 /* If the default VFS has no xRandomness(), add a basic JS impl... */
680 vfsSyncWrappers.xRandomness = function(pVfs, nOut, pOut){
681 const heap = wasm.heap8u();
682 let i = 0;
683 for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF;
684 return i;
685 };
686 }
687 if(!opfsVfs.$xSleep){
688 /* If we can inherit an xSleep() impl from the default VFS then
stephan8766fd22022-09-19 05:19:04 +0000689 assume it's sane and use it, otherwise install a JS-based
690 one. */
691 vfsSyncWrappers.xSleep = function(pVfs,ms){
stephanc4b87be2022-09-20 01:28:47 +0000692 Atomics.wait(state.sabOPView, state.opIds.xSleep, 0, ms);
stephan8766fd22022-09-19 05:19:04 +0000693 return 0;
694 };
stephanc5313af2022-09-18 02:35:30 +0000695 }
696
697 /* Install the vfs/io_methods into their C-level shared instances... */
698 let inst = installMethod(opfsIoMethods);
699 for(let k of Object.keys(ioSyncWrappers)) inst(k, ioSyncWrappers[k]);
700 inst = installMethod(opfsVfs);
701 for(let k of Object.keys(vfsSyncWrappers)) inst(k, vfsSyncWrappers[k]);
stephanf3860122022-09-18 17:32:35 +0000702
stephanf3860122022-09-18 17:32:35 +0000703 /**
704 Syncronously deletes the given OPFS filesystem entry, ignoring
705 any errors. As this environment has no notion of "current
706 directory", the given name must be an absolute path. If the 2nd
707 argument is truthy, deletion is recursive (use with caution!).
708
709 Returns true if the deletion succeeded and fails if it fails,
710 but cannot report the nature of the failure.
711 */
stephan0e0687c2022-09-19 13:44:23 +0000712 opfsUtil.deleteEntry = function(fsEntryName,recursive=false){
stephan138647a2022-09-20 03:31:02 +0000713 return 0===opRun('xDelete', fsEntryName, 0, recursive);
stephanf3860122022-09-18 17:32:35 +0000714 };
715 /**
stephanf3860122022-09-18 17:32:35 +0000716 Synchronously creates the given directory name, recursively, in
717 the OPFS filesystem. Returns true if it succeeds or the
718 directory already exists, else false.
719 */
stephan5e8bb0a2022-09-20 08:27:57 +0000720 opfsUtil.mkdir = function(absDirName){
stephanf3860122022-09-18 17:32:35 +0000721 return 0===opRun('mkdir', absDirName);
722 };
723 /**
724 Synchronously checks whether the given OPFS filesystem exists,
725 returning true if it does, false if it doesn't.
726 */
727 opfsUtil.entryExists = function(fsEntryName){
728 return 0===opRun('xAccess', fsEntryName);
729 };
730
731 /**
732 Generates a random ASCII string, intended for use as a
733 temporary file name. Its argument is the length of the string,
734 defaulting to 16.
735 */
736 opfsUtil.randomFilename = randomFilename;
737
738 if(sqlite3.oo1){
739 opfsUtil.OpfsDb = function(...args){
740 const opt = sqlite3.oo1.dbCtorHelper.normalizeArgs(...args);
741 opt.vfs = opfsVfs.$zName;
742 sqlite3.oo1.dbCtorHelper.call(this, opt);
743 };
744 opfsUtil.OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype);
745 }
stephanf8150112022-09-19 17:09:09 +0000746
stephanf3860122022-09-18 17:32:35 +0000747 /**
748 Potential TODOs:
749
750 - Expose one or both of the Worker objects via opfsUtil and
751 publish an interface for proxying the higher-level OPFS
752 features like getting a directory listing.
753 */
stephanc5313af2022-09-18 02:35:30 +0000754
stephan5e8bb0a2022-09-20 08:27:57 +0000755 const sanityCheck = function(){
stephanc5313af2022-09-18 02:35:30 +0000756 const scope = wasm.scopedAllocPush();
757 const sq3File = new sqlite3_file();
758 try{
759 const fid = sq3File.pointer;
760 const openFlags = capi.SQLITE_OPEN_CREATE
761 | capi.SQLITE_OPEN_READWRITE
762 //| capi.SQLITE_OPEN_DELETEONCLOSE
763 | capi.SQLITE_OPEN_MAIN_DB;
764 const pOut = wasm.scopedAlloc(8);
765 const dbFile = "/sanity/check/file";
766 const zDbFile = wasm.scopedAllocCString(dbFile);
767 let rc;
768 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
769 rc = wasm.getMemValue(pOut,'i32');
770 log("xAccess(",dbFile,") exists ?=",rc);
771 rc = vfsSyncWrappers.xOpen(opfsVfs.pointer, zDbFile,
772 fid, openFlags, pOut);
stephanc4b87be2022-09-20 01:28:47 +0000773 log("open rc =",rc,"state.sabOPView[xOpen] =",
774 state.sabOPView[state.opIds.xOpen]);
stephanc5313af2022-09-18 02:35:30 +0000775 if(isWorkerErrCode(rc)){
776 error("open failed with code",rc);
777 return;
778 }
779 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
780 rc = wasm.getMemValue(pOut,'i32');
781 if(!rc) toss("xAccess() failed to detect file.");
782 rc = ioSyncWrappers.xSync(sq3File.pointer, 0);
783 if(rc) toss('sync failed w/ rc',rc);
784 rc = ioSyncWrappers.xTruncate(sq3File.pointer, 1024);
785 if(rc) toss('truncate failed w/ rc',rc);
786 wasm.setMemValue(pOut,0,'i64');
787 rc = ioSyncWrappers.xFileSize(sq3File.pointer, pOut);
788 if(rc) toss('xFileSize failed w/ rc',rc);
789 log("xFileSize says:",wasm.getMemValue(pOut, 'i64'));
790 rc = ioSyncWrappers.xWrite(sq3File.pointer, zDbFile, 10, 1);
791 if(rc) toss("xWrite() failed!");
792 const readBuf = wasm.scopedAlloc(16);
793 rc = ioSyncWrappers.xRead(sq3File.pointer, readBuf, 6, 2);
794 wasm.setMemValue(readBuf+6,0);
795 let jRead = wasm.cstringToJs(readBuf);
796 log("xRead() got:",jRead);
797 if("sanity"!==jRead) toss("Unexpected xRead() value.");
stephan8766fd22022-09-19 05:19:04 +0000798 if(vfsSyncWrappers.xSleep){
799 log("xSleep()ing before close()ing...");
800 vfsSyncWrappers.xSleep(opfsVfs.pointer,2000);
801 log("waking up from xSleep()");
802 }
stephanc5313af2022-09-18 02:35:30 +0000803 rc = ioSyncWrappers.xClose(fid);
stephanc4b87be2022-09-20 01:28:47 +0000804 log("xClose rc =",rc,"sabOPView =",state.sabOPView);
stephanc5313af2022-09-18 02:35:30 +0000805 log("Deleting file:",dbFile);
806 vfsSyncWrappers.xDelete(opfsVfs.pointer, zDbFile, 0x1234);
807 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
808 rc = wasm.getMemValue(pOut,'i32');
809 if(rc) toss("Expecting 0 from xAccess(",dbFile,") after xDelete().");
stephanc9e26022022-09-20 10:11:52 +0000810 warn("End of OPFS sanity checks.");
stephanc5313af2022-09-18 02:35:30 +0000811 }finally{
812 sq3File.dispose();
813 wasm.scopedAllocPop(scope);
814 }
815 }/*sanityCheck()*/;
816
stephanf8150112022-09-19 17:09:09 +0000817
stephanc5313af2022-09-18 02:35:30 +0000818 W.onmessage = function({data}){
819 //log("Worker.onmessage:",data);
820 switch(data.type){
stephan138647a2022-09-20 03:31:02 +0000821 case 'opfs-async-loaded':
stephanc5313af2022-09-18 02:35:30 +0000822 /*Pass our config and shared state on to the async worker.*/
stephan5e8bb0a2022-09-20 08:27:57 +0000823 W.postMessage({type: 'opfs-async-init',args: state});
stephanc5313af2022-09-18 02:35:30 +0000824 break;
stephan138647a2022-09-20 03:31:02 +0000825 case 'opfs-async-inited':{
stephanc5313af2022-09-18 02:35:30 +0000826 /*Indicates that the async partner has received the 'init',
827 so we now know that the state object is no longer subject to
828 being copied by a pending postMessage() call.*/
829 try {
stephan0e0687c2022-09-19 13:44:23 +0000830 const rc = capi.sqlite3_vfs_register(opfsVfs.pointer, 0);
stephanc5313af2022-09-18 02:35:30 +0000831 if(rc){
832 opfsVfs.dispose();
833 toss("sqlite3_vfs_register(OPFS) failed with rc",rc);
834 }
835 if(opfsVfs.pointer !== capi.sqlite3_vfs_find("opfs")){
836 toss("BUG: sqlite3_vfs_find() failed for just-installed OPFS VFS");
837 }
838 capi.sqlite3_vfs_register.addReference(opfsVfs, opfsIoMethods);
stephanc4b87be2022-09-20 01:28:47 +0000839 state.sabOPView = new Int32Array(state.sabOP);
stephan138647a2022-09-20 03:31:02 +0000840 state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
841 state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
842 initS11n();
stephanc5313af2022-09-18 02:35:30 +0000843 if(options.sanityChecks){
844 warn("Running sanity checks because of opfs-sanity-check URL arg...");
845 sanityCheck();
846 }
stephanf3860122022-09-18 17:32:35 +0000847 W.onerror = W._originalOnError;
848 delete W._originalOnError;
849 sqlite3.opfs = opfsUtil;
stephanc5313af2022-09-18 02:35:30 +0000850 log("End of OPFS sqlite3_vfs setup.", opfsVfs);
stephan509f4052022-09-19 09:58:01 +0000851 promiseResolve(sqlite3);
stephanc5313af2022-09-18 02:35:30 +0000852 }catch(e){
853 error(e);
854 promiseReject(e);
855 }
856 break;
857 }
858 default:
859 promiseReject(e);
860 error("Unexpected message from the async worker:",data);
861 break;
862 }
863 };
864 })/*thePromise*/;
865 return thePromise;
866}/*installOpfsVfs()*/;
867sqlite3.installOpfsVfs.defaultProxyUri = "sqlite3-opfs-async-proxy.js";
868}/*sqlite3ApiBootstrap.initializers.push()*/);