blob: 3f27941e8b9d26f803cabe8e5aea7c5c51e3874d [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
stephanf861b362022-10-25 08:06:17 +000018 after sqlite3-api-oo1.js and before sqlite3-api-cleanup.js.
stephanc5313af2022-09-18 02:35:30 +000019*/
stephanc5313af2022-09-18 02:35:30 +000020'use strict';
21self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){
22/**
stephanf861b362022-10-25 08:06:17 +000023 installOpfsVfs() returns a Promise which, on success, installs an
24 sqlite3_vfs named "opfs", suitable for use with all sqlite3 APIs
25 which accept a VFS. It is intended to be called via
26 sqlite3ApiBootstrap.initializersAsync or an equivalent mechanism.
27
28 The installed VFS uses the Origin-Private FileSystem API for
stephanc5313af2022-09-18 02:35:30 +000029 all file storage. On error it is rejected with an exception
30 explaining the problem. Reasons for rejection include, but are
31 not limited to:
32
33 - The counterpart Worker (see below) could not be loaded.
34
35 - The environment does not support OPFS. That includes when
36 this function is called from the main window thread.
37
stephan3961b262022-08-10 11:26:08 +000038 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
stephanf861b362022-10-25 08:06:17 +000054 - This function may only be called a single time. When called, this
55 function removes itself from the sqlite3 object.
stephanc5313af2022-09-18 02:35:30 +000056
stephanf861b362022-10-25 08:06:17 +000057 All arguments to this function are for internal/development purposes
58 only. They do not constitute a public API and may change at any
59 time.
stephanc5313af2022-09-18 02:35:30 +000060
stephanf861b362022-10-25 08:06:17 +000061 The argument may optionally be a plain object with the following
62 configuration options:
stephanc5313af2022-09-18 02:35:30 +000063
stephanf861b362022-10-25 08:06:17 +000064 - proxyUri: as described above
stephanc5313af2022-09-18 02:35:30 +000065
stephanf861b362022-10-25 08:06:17 +000066 - 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.
stephanc5313af2022-09-18 02:35:30 +000069
stephanf861b362022-10-25 08:06:17 +000070 - sanityChecks (=false): if true, some basic sanity tests are
71 run on the OPFS VFS API after it's initialized, before the
72 returned Promise resolves.
73
74 On success, the Promise resolves to the top-most sqlite3 namespace
75 object and that object gets a new object installed in its
76 `opfs` property, containing several OPFS-specific utilities.
stephan3961b262022-08-10 11:26:08 +000077*/
stephanf861b362022-10-25 08:06:17 +000078const installOpfsVfs = function callee(options){
stephan4cffb642022-09-20 16:20:35 +000079 if(!self.SharedArrayBuffer ||
stephanf6c686c2022-09-30 11:01:44 +000080 !self.Atomics ||
stephan509f4052022-09-19 09:58:01 +000081 !self.FileSystemHandle ||
82 !self.FileSystemDirectoryHandle ||
83 !self.FileSystemFileHandle ||
84 !self.FileSystemFileHandle.prototype.createSyncAccessHandle ||
85 !navigator.storage.getDirectory){
86 return Promise.reject(
87 new Error("This environment does not have OPFS support.")
88 );
89 }
stephanf861b362022-10-25 08:06:17 +000090 if(!options || 'object'!==typeof options){
91 options = Object.create(null);
92 }
stephan509f4052022-09-19 09:58:01 +000093 const urlParams = new URL(self.location.href).searchParams;
stephanc5313af2022-09-18 02:35:30 +000094 if(undefined===options.verbose){
stephan509f4052022-09-19 09:58:01 +000095 options.verbose = urlParams.has('opfs-verbose') ? 3 : 2;
stephan3961b262022-08-10 11:26:08 +000096 }
stephanc5313af2022-09-18 02:35:30 +000097 if(undefined===options.sanityChecks){
stephan509f4052022-09-19 09:58:01 +000098 options.sanityChecks = urlParams.has('opfs-sanity-check');
stephanc5313af2022-09-18 02:35:30 +000099 }
100 if(undefined===options.proxyUri){
101 options.proxyUri = callee.defaultProxyUri;
102 }
stephanf3860122022-09-18 17:32:35 +0000103
stephancd0df832022-10-19 04:44:58 +0000104 if('function' === typeof options.proxyUri){
105 options.proxyUri = options.proxyUri();
106 }
stephane8afca32022-09-21 14:02:47 +0000107 const thePromise = new Promise(function(promiseResolve, promiseReject_){
stephan509f4052022-09-19 09:58:01 +0000108 const loggers = {
109 0:console.error.bind(console),
110 1:console.warn.bind(console),
111 2:console.log.bind(console)
stephanc5313af2022-09-18 02:35:30 +0000112 };
stephan509f4052022-09-19 09:58:01 +0000113 const logImpl = (level,...args)=>{
114 if(options.verbose>level) loggers[level]("OPFS syncer:",...args);
115 };
116 const log = (...args)=>logImpl(2, ...args);
117 const warn = (...args)=>logImpl(1, ...args);
118 const error = (...args)=>logImpl(0, ...args);
stephanc5313af2022-09-18 02:35:30 +0000119 const toss = function(...args){throw new Error(args.join(' '))};
stephanc5313af2022-09-18 02:35:30 +0000120 const capi = sqlite3.capi;
121 const wasm = capi.wasm;
122 const sqlite3_vfs = capi.sqlite3_vfs;
123 const sqlite3_file = capi.sqlite3_file;
124 const sqlite3_io_methods = capi.sqlite3_io_methods;
stephan509f4052022-09-19 09:58:01 +0000125 /**
126 Generic utilities for working with OPFS. This will get filled out
127 by the Promise setup and, on success, installed as sqlite3.opfs.
128 */
129 const opfsUtil = Object.create(null);
stephanf8150112022-09-19 17:09:09 +0000130 /**
131 Not part of the public API. Solely for internal/development
132 use.
133 */
134 opfsUtil.metrics = {
135 dump: function(){
stephanaec046a2022-09-19 18:22:29 +0000136 let k, n = 0, t = 0, w = 0;
137 for(k in state.opIds){
stephanf8150112022-09-19 17:09:09 +0000138 const m = metrics[k];
139 n += m.count;
140 t += m.time;
stephanaec046a2022-09-19 18:22:29 +0000141 w += m.wait;
stephanf8150112022-09-19 17:09:09 +0000142 m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
143 m.avgWait = (m.count && m.wait) ? (m.wait / m.count) : 0;
144 }
stephanaec046a2022-09-19 18:22:29 +0000145 console.log(self.location.href,
146 "metrics for",self.location.href,":",metrics,
147 "\nTotal of",n,"op(s) for",t,
148 "ms (incl. "+w+" ms of waiting on the async side)");
stephan56fae742022-09-24 10:12:19 +0000149 console.log("Serialization metrics:",metrics.s11n);
stephan3c272ba2022-10-04 00:54:00 +0000150 W.postMessage({type:'opfs-async-metrics'});
stephanf8150112022-09-19 17:09:09 +0000151 },
152 reset: function(){
153 let k;
154 const r = (m)=>(m.count = m.time = m.wait = 0);
155 for(k in state.opIds){
156 r(metrics[k] = Object.create(null));
157 }
stephanb8c8d4e2022-09-20 13:25:39 +0000158 let s = metrics.s11n = Object.create(null);
159 s = s.serialize = Object.create(null);
160 s.count = s.time = 0;
161 s = metrics.s11n.deserialize = Object.create(null);
162 s.count = s.time = 0;
stephanf8150112022-09-19 17:09:09 +0000163 }
164 }/*metrics*/;
stephan56fae742022-09-24 10:12:19 +0000165 const promiseReject = function(err){
166 opfsVfs.dispose();
167 return promiseReject_(err);
168 };
169 const W = new Worker(options.proxyUri);
170 W._originalOnError = W.onerror /* will be restored later */;
171 W.onerror = function(err){
172 // The error object doesn't contain any useful info when the
173 // failure is, e.g., that the remote script is 404.
stephan9a557732022-10-04 17:06:51 +0000174 error("Error initializing OPFS asyncer:",err);
stephan56fae742022-09-24 10:12:19 +0000175 promiseReject(new Error("Loading OPFS async Worker failed for unknown reasons."));
176 };
stephanc9e26022022-09-20 10:11:52 +0000177 const pDVfs = capi.sqlite3_vfs_find(null)/*pointer to default VFS*/;
178 const dVfs = pDVfs
179 ? new sqlite3_vfs(pDVfs)
180 : null /* dVfs will be null when sqlite3 is built with
181 SQLITE_OS_OTHER. Though we cannot currently handle
182 that case, the hope is to eventually be able to. */;
183 const opfsVfs = new sqlite3_vfs();
184 const opfsIoMethods = new sqlite3_io_methods();
185 opfsVfs.$iVersion = 2/*yes, two*/;
186 opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof;
187 opfsVfs.$mxPathname = 1024/*sure, why not?*/;
188 opfsVfs.$zName = wasm.allocCString("opfs");
189 // All C-side memory of opfsVfs is zeroed out, but just to be explicit:
190 opfsVfs.$xDlOpen = opfsVfs.$xDlError = opfsVfs.$xDlSym = opfsVfs.$xDlClose = null;
191 opfsVfs.ondispose = [
192 '$zName', opfsVfs.$zName,
193 'cleanup default VFS wrapper', ()=>(dVfs ? dVfs.dispose() : null),
194 'cleanup opfsIoMethods', ()=>opfsIoMethods.dispose()
195 ];
196 /**
197 Pedantic sidebar about opfsVfs.ondispose: the entries in that array
198 are items to clean up when opfsVfs.dispose() is called, but in this
199 environment it will never be called. The VFS instance simply
200 hangs around until the WASM module instance is cleaned up. We
201 "could" _hypothetically_ clean it up by "importing" an
202 sqlite3_os_end() impl into the wasm build, but the shutdown order
203 of the wasm engine and the JS one are undefined so there is no
204 guaranty that the opfsVfs instance would be available in one
205 environment or the other when sqlite3_os_end() is called (_if_ it
206 gets called at all in a wasm build, which is undefined).
207 */
stephanc5313af2022-09-18 02:35:30 +0000208 /**
209 State which we send to the async-api Worker or share with it.
210 This object must initially contain only cloneable or sharable
211 objects. After the worker's "inited" message arrives, other types
212 of data may be added to it.
stephanf3860122022-09-18 17:32:35 +0000213
214 For purposes of Atomics.wait() and Atomics.notify(), we use a
215 SharedArrayBuffer with one slot reserved for each of the API
216 proxy's methods. The sync side of the API uses Atomics.wait()
217 on the corresponding slot and the async side uses
218 Atomics.notify() on that slot.
219
220 The approach of using a single SAB to serialize comms for all
221 instances might(?) lead to deadlock situations in multi-db
222 cases. We should probably have one SAB here with a single slot
223 for locking a per-file initialization step and then allocate a
224 separate SAB like the above one for each file. That will
stephanf861b362022-10-25 08:06:17 +0000225 require a bit of acrobatics but should be feasible. The most
226 problematic part is that xOpen() would have to use
227 postMessage() to communicate its SharedArrayBuffer, and mixing
228 that approach with Atomics.wait/notify() gets a bit messy.
stephanc5313af2022-09-18 02:35:30 +0000229 */
230 const state = Object.create(null);
231 state.verbose = options.verbose;
stephan9a557732022-10-04 17:06:51 +0000232 state.littleEndian = (()=>{
233 const buffer = new ArrayBuffer(2);
stephanf861b362022-10-25 08:06:17 +0000234 new DataView(buffer).setInt16(0, 256, true /* ==>littleEndian */);
stephan9a557732022-10-04 17:06:51 +0000235 // Int16Array uses the platform's endianness.
236 return new Int16Array(buffer)[0] === 256;
237 })();
stephanf861b362022-10-25 08:06:17 +0000238 /**
239 Whether the async counterpart should log exceptions to
240 the serialization channel. That produces a great deal of
241 noise for seemingly innocuous things like xAccess() checks
242 for missing files, so this option may have one of 3 values:
stephan56fae742022-09-24 10:12:19 +0000243
stephanf861b362022-10-25 08:06:17 +0000244 0 = no exception logging
stephan56fae742022-09-24 10:12:19 +0000245
stephanf861b362022-10-25 08:06:17 +0000246 1 = only log exceptions for "significant" ops like xOpen(),
247 xRead(), and xWrite().
stephan56fae742022-09-24 10:12:19 +0000248
stephanf861b362022-10-25 08:06:17 +0000249 2 = log all exceptions.
stephan56fae742022-09-24 10:12:19 +0000250 */
251 state.asyncS11nExceptions = 1;
stephanf861b362022-10-25 08:06:17 +0000252 /* Size of file I/O buffer block. 64k = max sqlite3 page size, and
253 xRead/xWrite() will never deal in blocks larger than that. */
254 state.fileBufferSize = 1024 * 64;
stephan138647a2022-09-20 03:31:02 +0000255 state.sabS11nOffset = state.fileBufferSize;
stephanc9e26022022-09-20 10:11:52 +0000256 /**
257 The size of the block in our SAB for serializing arguments and
stephane8afca32022-09-21 14:02:47 +0000258 result values. Needs to be large enough to hold serialized
stephanc9e26022022-09-20 10:11:52 +0000259 values of any of the proxied APIs. Filenames are the largest
260 part but are limited to opfsVfs.$mxPathname bytes.
261 */
262 state.sabS11nSize = opfsVfs.$mxPathname * 2;
263 /**
stephanf861b362022-10-25 08:06:17 +0000264 The SAB used for all data I/O between the synchronous and
265 async halves (file i/o and arg/result s11n).
stephanc9e26022022-09-20 10:11:52 +0000266 */
stephanc4b87be2022-09-20 01:28:47 +0000267 state.sabIO = new SharedArrayBuffer(
stephanc9e26022022-09-20 10:11:52 +0000268 state.fileBufferSize/* file i/o block */
269 + state.sabS11nSize/* argument/result serialization block */
stephanc4b87be2022-09-20 01:28:47 +0000270 );
stephanc5313af2022-09-18 02:35:30 +0000271 state.opIds = Object.create(null);
stephanf8150112022-09-19 17:09:09 +0000272 const metrics = Object.create(null);
stephanc5313af2022-09-18 02:35:30 +0000273 {
stephanc9e26022022-09-20 10:11:52 +0000274 /* Indexes for use in our SharedArrayBuffer... */
stephan3961b262022-08-10 11:26:08 +0000275 let i = 0;
stephanc9e26022022-09-20 10:11:52 +0000276 /* SAB slot used to communicate which operation is desired
277 between both workers. This worker writes to it and the other
278 listens for changes. */
stephan138647a2022-09-20 03:31:02 +0000279 state.opIds.whichOp = i++;
stephan9a557732022-10-04 17:06:51 +0000280 /* Slot for storing return values. This worker listens to that
stephanc9e26022022-09-20 10:11:52 +0000281 slot and the other worker writes to it. */
282 state.opIds.rc = i++;
283 /* Each function gets an ID which this worker writes to
284 the whichOp slot. The async-api worker uses Atomic.wait()
285 on the whichOp slot to figure out which operation to run
286 next. */
stephanc5313af2022-09-18 02:35:30 +0000287 state.opIds.xAccess = i++;
288 state.opIds.xClose = i++;
289 state.opIds.xDelete = i++;
stephanf3860122022-09-18 17:32:35 +0000290 state.opIds.xDeleteNoWait = i++;
stephan56fae742022-09-24 10:12:19 +0000291 state.opIds.xFileControl = i++;
stephanc5313af2022-09-18 02:35:30 +0000292 state.opIds.xFileSize = i++;
stephan9a557732022-10-04 17:06:51 +0000293 state.opIds.xLock = i++;
stephanc5313af2022-09-18 02:35:30 +0000294 state.opIds.xOpen = i++;
295 state.opIds.xRead = i++;
296 state.opIds.xSleep = i++;
297 state.opIds.xSync = i++;
298 state.opIds.xTruncate = i++;
stephan9a557732022-10-04 17:06:51 +0000299 state.opIds.xUnlock = i++;
stephanc5313af2022-09-18 02:35:30 +0000300 state.opIds.xWrite = i++;
stephanf3860122022-09-18 17:32:35 +0000301 state.opIds.mkdir = i++;
stephan3c272ba2022-10-04 00:54:00 +0000302 state.opIds['opfs-async-metrics'] = i++;
303 state.opIds['opfs-async-shutdown'] = i++;
stephanf861b362022-10-25 08:06:17 +0000304 /* The retry slot is used by the async part for wait-and-retry
305 semantics. Though we could hypothetically use the xSleep slot
306 for that, doing so might lead to undesired side effects. */
307 state.opIds.retry = i++;
308 state.sabOP = new SharedArrayBuffer(
309 i * 4/* ==sizeof int32, noting that Atomics.wait() and friends
310 can only function on Int32Array views of an SAB. */);
stephanf8150112022-09-19 17:09:09 +0000311 opfsUtil.metrics.reset();
stephanc5313af2022-09-18 02:35:30 +0000312 }
stephanc9e26022022-09-20 10:11:52 +0000313 /**
314 SQLITE_xxx constants to export to the async worker
315 counterpart...
316 */
stephanc5313af2022-09-18 02:35:30 +0000317 state.sq3Codes = Object.create(null);
stephanc9e26022022-09-20 10:11:52 +0000318 [
stephanc5313af2022-09-18 02:35:30 +0000319 'SQLITE_ERROR', 'SQLITE_IOERR',
320 'SQLITE_NOTFOUND', 'SQLITE_MISUSE',
321 'SQLITE_IOERR_READ', 'SQLITE_IOERR_SHORT_READ',
322 'SQLITE_IOERR_WRITE', 'SQLITE_IOERR_FSYNC',
323 'SQLITE_IOERR_TRUNCATE', 'SQLITE_IOERR_DELETE',
stephanf3860122022-09-18 17:32:35 +0000324 'SQLITE_IOERR_ACCESS', 'SQLITE_IOERR_CLOSE',
stephanc4b87be2022-09-20 01:28:47 +0000325 'SQLITE_IOERR_DELETE',
stephan9a557732022-10-04 17:06:51 +0000326 'SQLITE_LOCK_NONE',
327 'SQLITE_LOCK_SHARED',
328 'SQLITE_LOCK_RESERVED',
329 'SQLITE_LOCK_PENDING',
330 'SQLITE_LOCK_EXCLUSIVE',
stephanc4b87be2022-09-20 01:28:47 +0000331 'SQLITE_OPEN_CREATE', 'SQLITE_OPEN_DELETEONCLOSE',
332 'SQLITE_OPEN_READONLY'
stephan9a557732022-10-04 17:06:51 +0000333 ].forEach((k)=>{
334 if(undefined === (state.sq3Codes[k] = capi[k])){
335 toss("Maintenance required: not found:",k);
336 }
stephan3961b262022-08-10 11:26:08 +0000337 });
stephan3961b262022-08-10 11:26:08 +0000338
stephanc5313af2022-09-18 02:35:30 +0000339 /**
stephanc9e26022022-09-20 10:11:52 +0000340 Runs the given operation (by name) in the async worker
341 counterpart, waits for its response, and returns the result
342 which the async worker writes to SAB[state.opIds.rc]. The
343 2nd and subsequent arguments must be the aruguments for the
344 async op.
stephanc5313af2022-09-18 02:35:30 +0000345 */
stephan138647a2022-09-20 03:31:02 +0000346 const opRun = (op,...args)=>{
stephan5e8bb0a2022-09-20 08:27:57 +0000347 const opNdx = state.opIds[op] || toss("Invalid op ID:",op);
348 state.s11n.serialize(...args);
stephanc9e26022022-09-20 10:11:52 +0000349 Atomics.store(state.sabOPView, state.opIds.rc, -1);
stephan5e8bb0a2022-09-20 08:27:57 +0000350 Atomics.store(state.sabOPView, state.opIds.whichOp, opNdx);
stephanf861b362022-10-25 08:06:17 +0000351 Atomics.notify(state.sabOPView, state.opIds.whichOp)
352 /* async thread will take over here */;
stephanf8150112022-09-19 17:09:09 +0000353 const t = performance.now();
stephanf861b362022-10-25 08:06:17 +0000354 Atomics.wait(state.sabOPView, state.opIds.rc, -1)
355 /* When this wait() call returns, the async half will have
356 completed the operation and reported its results. */;
stephanc9e26022022-09-20 10:11:52 +0000357 const rc = Atomics.load(state.sabOPView, state.opIds.rc);
stephane8afca32022-09-21 14:02:47 +0000358 metrics[op].wait += performance.now() - t;
359 if(rc && state.asyncS11nExceptions){
stephan72ab4002022-09-21 12:27:35 +0000360 const err = state.s11n.deserialize();
361 if(err) error(op+"() async error:",...err);
362 }
stephan5e8bb0a2022-09-20 08:27:57 +0000363 return rc;
stephanc5313af2022-09-18 02:35:30 +0000364 };
365
stephan138647a2022-09-20 03:31:02 +0000366 const initS11n = ()=>{
stephanb8c8d4e2022-09-20 13:25:39 +0000367 /**
stephanf861b362022-10-25 08:06:17 +0000368 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
369 ACHTUNG: this code is 100% duplicated in the other half of
370 this proxy! The documentation is maintained in the
371 "synchronous half".
372 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
stephanb8c8d4e2022-09-20 13:25:39 +0000373
stephan72ab4002022-09-21 12:27:35 +0000374 This proxy de/serializes cross-thread function arguments and
375 output-pointer values via the state.sabIO SharedArrayBuffer,
376 using the region defined by (state.sabS11nOffset,
377 state.sabS11nOffset]. Only one dataset is recorded at a time.
378
stephanf861b362022-10-25 08:06:17 +0000379 This is not a general-purpose format. It only supports the
380 range of operations, and data sizes, needed by the
381 sqlite3_vfs and sqlite3_io_methods operations. Serialized
382 data are transient and this serialization algorithm may
383 change at any time.
stephan72ab4002022-09-21 12:27:35 +0000384
385 The data format can be succinctly summarized as:
386
387 Nt...Td...D
388
389 Where:
390
391 - N = number of entries (1 byte)
392
393 - t = type ID of first argument (1 byte)
394
395 - ...T = type IDs of the 2nd and subsequent arguments (1 byte
396 each).
397
398 - d = raw bytes of first argument (per-type size).
399
400 - ...D = raw bytes of the 2nd and subsequent arguments (per-type
401 size).
402
403 All types except strings have fixed sizes. Strings are stored
404 using their TextEncoder/TextDecoder representations. It would
405 arguably make more sense to store them as Int16Arrays of
406 their JS character values, but how best/fastest to get that
stephanf861b362022-10-25 08:06:17 +0000407 in and out of string form is an open point. Initial
408 experimentation with that approach did not gain us any speed.
stephan72ab4002022-09-21 12:27:35 +0000409
410 Historical note: this impl was initially about 1% this size by
411 using using JSON.stringify/parse(), but using fit-to-purpose
412 serialization saves considerable runtime.
stephanb8c8d4e2022-09-20 13:25:39 +0000413 */
stephan138647a2022-09-20 03:31:02 +0000414 if(state.s11n) return state.s11n;
stephanb8c8d4e2022-09-20 13:25:39 +0000415 const textDecoder = new TextDecoder(),
stephan72ab4002022-09-21 12:27:35 +0000416 textEncoder = new TextEncoder('utf-8'),
417 viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize),
418 viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
stephan138647a2022-09-20 03:31:02 +0000419 state.s11n = Object.create(null);
stephan72ab4002022-09-21 12:27:35 +0000420 /* Only arguments and return values of these types may be
421 serialized. This covers the whole range of types needed by the
422 sqlite3_vfs API. */
stephanb8c8d4e2022-09-20 13:25:39 +0000423 const TypeIds = Object.create(null);
424 TypeIds.number = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' };
425 TypeIds.bigint = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' };
426 TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' };
427 TypeIds.string = { id: 4 };
stephan72ab4002022-09-21 12:27:35 +0000428
429 const getTypeId = (v)=>(
430 TypeIds[typeof v]
431 || toss("Maintenance required: this value type cannot be serialized.",v)
432 );
stephanb8c8d4e2022-09-20 13:25:39 +0000433 const getTypeIdById = (tid)=>{
434 switch(tid){
stephan72ab4002022-09-21 12:27:35 +0000435 case TypeIds.number.id: return TypeIds.number;
436 case TypeIds.bigint.id: return TypeIds.bigint;
437 case TypeIds.boolean.id: return TypeIds.boolean;
438 case TypeIds.string.id: return TypeIds.string;
439 default: toss("Invalid type ID:",tid);
stephanb8c8d4e2022-09-20 13:25:39 +0000440 }
441 };
stephan72ab4002022-09-21 12:27:35 +0000442
stephan138647a2022-09-20 03:31:02 +0000443 /**
stephan72ab4002022-09-21 12:27:35 +0000444 Returns an array of the deserialized state stored by the most
445 recent serialize() operation (from from this thread or the
446 counterpart thread), or null if the serialization buffer is empty.
stephan138647a2022-09-20 03:31:02 +0000447 */
448 state.s11n.deserialize = function(){
stephanb8c8d4e2022-09-20 13:25:39 +0000449 ++metrics.s11n.deserialize.count;
450 const t = performance.now();
stephanb8c8d4e2022-09-20 13:25:39 +0000451 const argc = viewU8[0];
stephan72ab4002022-09-21 12:27:35 +0000452 const rc = argc ? [] : null;
stephanb8c8d4e2022-09-20 13:25:39 +0000453 if(argc){
stephan72ab4002022-09-21 12:27:35 +0000454 const typeIds = [];
455 let offset = 1, i, n, v;
stephanb8c8d4e2022-09-20 13:25:39 +0000456 for(i = 0; i < argc; ++i, ++offset){
457 typeIds.push(getTypeIdById(viewU8[offset]));
458 }
459 for(i = 0; i < argc; ++i){
460 const t = typeIds[i];
461 if(t.getter){
462 v = viewDV[t.getter](offset, state.littleEndian);
463 offset += t.size;
stephan72ab4002022-09-21 12:27:35 +0000464 }else{/*String*/
stephanb8c8d4e2022-09-20 13:25:39 +0000465 n = viewDV.getInt32(offset, state.littleEndian);
466 offset += 4;
467 v = textDecoder.decode(viewU8.slice(offset, offset+n));
468 offset += n;
469 }
470 rc.push(v);
471 }
472 }
473 //log("deserialize:",argc, rc);
474 metrics.s11n.deserialize.time += performance.now() - t;
475 return rc;
476 };
stephan72ab4002022-09-21 12:27:35 +0000477
stephan138647a2022-09-20 03:31:02 +0000478 /**
479 Serializes all arguments to the shared buffer for consumption
stephanb8c8d4e2022-09-20 13:25:39 +0000480 by the counterpart thread.
stephan5e8bb0a2022-09-20 08:27:57 +0000481
stephanb8c8d4e2022-09-20 13:25:39 +0000482 This routine is only intended for serializing OPFS VFS
483 arguments and (in at least one special case) result values,
484 and the buffer is sized to be able to comfortably handle
485 those.
stephan5e8bb0a2022-09-20 08:27:57 +0000486
487 If passed no arguments then it zeroes out the serialization
488 state.
stephan138647a2022-09-20 03:31:02 +0000489 */
490 state.s11n.serialize = function(...args){
stephanb8c8d4e2022-09-20 13:25:39 +0000491 const t = performance.now();
stephan72ab4002022-09-21 12:27:35 +0000492 ++metrics.s11n.serialize.count;
stephan5e8bb0a2022-09-20 08:27:57 +0000493 if(args.length){
stephanb8c8d4e2022-09-20 13:25:39 +0000494 //log("serialize():",args);
stephan72ab4002022-09-21 12:27:35 +0000495 const typeIds = [];
496 let i = 0, offset = 1;
497 viewU8[0] = args.length & 0xff /* header = # of args */;
stephanb8c8d4e2022-09-20 13:25:39 +0000498 for(; i < args.length; ++i, ++offset){
stephan72ab4002022-09-21 12:27:35 +0000499 /* Write the TypeIds.id value into the next args.length
500 bytes. */
stephanb8c8d4e2022-09-20 13:25:39 +0000501 typeIds.push(getTypeId(args[i]));
502 viewU8[offset] = typeIds[i].id;
503 }
504 for(i = 0; i < args.length; ++i) {
stephan72ab4002022-09-21 12:27:35 +0000505 /* Deserialize the following bytes based on their
506 corresponding TypeIds.id from the header. */
stephanb8c8d4e2022-09-20 13:25:39 +0000507 const t = typeIds[i];
508 if(t.setter){
509 viewDV[t.setter](offset, args[i], state.littleEndian);
510 offset += t.size;
stephan72ab4002022-09-21 12:27:35 +0000511 }else{/*String*/
stephanb8c8d4e2022-09-20 13:25:39 +0000512 const s = textEncoder.encode(args[i]);
513 viewDV.setInt32(offset, s.byteLength, state.littleEndian);
514 offset += 4;
515 viewU8.set(s, offset);
516 offset += s.byteLength;
517 }
518 }
519 //log("serialize() result:",viewU8.slice(0,offset));
stephan5e8bb0a2022-09-20 08:27:57 +0000520 }else{
stephanb8c8d4e2022-09-20 13:25:39 +0000521 viewU8[0] = 0;
stephan5e8bb0a2022-09-20 08:27:57 +0000522 }
stephanb8c8d4e2022-09-20 13:25:39 +0000523 metrics.s11n.serialize.time += performance.now() - t;
stephan138647a2022-09-20 03:31:02 +0000524 };
525 return state.s11n;
stephanb8c8d4e2022-09-20 13:25:39 +0000526 }/*initS11n()*/;
stephan138647a2022-09-20 03:31:02 +0000527
stephanc5313af2022-09-18 02:35:30 +0000528 /**
529 Generates a random ASCII string len characters long, intended for
530 use as a temporary file name.
531 */
532 const randomFilename = function f(len=16){
533 if(!f._chars){
534 f._chars = "abcdefghijklmnopqrstuvwxyz"+
535 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+
536 "012346789";
537 f._n = f._chars.length;
538 }
539 const a = [];
540 let i = 0;
541 for( ; i < len; ++i){
542 const ndx = Math.random() * (f._n * 64) % f._n | 0;
543 a[i] = f._chars[ndx];
544 }
545 return a.join('');
546 };
547
548 /**
549 Map of sqlite3_file pointers to objects constructed by xOpen().
550 */
551 const __openFiles = Object.create(null);
stephanc5313af2022-09-18 02:35:30 +0000552
553 /**
554 Installs a StructBinder-bound function pointer member of the
555 given name and function in the given StructType target object.
556 It creates a WASM proxy for the given function and arranges for
557 that proxy to be cleaned up when tgt.dispose() is called. Throws
558 on the slightest hint of error (e.g. tgt is-not-a StructType,
559 name does not map to a struct-bound member, etc.).
560
561 Returns a proxy for this function which is bound to tgt and takes
562 2 args (name,func). That function returns the same thing,
563 permitting calls to be chained.
564
565 If called with only 1 arg, it has no side effects but returns a
566 func with the same signature as described above.
567 */
568 const installMethod = function callee(tgt, name, func){
stephanf3860122022-09-18 17:32:35 +0000569 if(!(tgt instanceof sqlite3.StructBinder.StructType)){
stephanc5313af2022-09-18 02:35:30 +0000570 toss("Usage error: target object is-not-a StructType.");
571 }
572 if(1===arguments.length){
573 return (n,f)=>callee(tgt,n,f);
574 }
575 if(!callee.argcProxy){
576 callee.argcProxy = function(func,sig){
577 return function(...args){
578 if(func.length!==arguments.length){
579 toss("Argument mismatch. Native signature is:",sig);
580 }
581 return func.apply(this, args);
582 }
583 };
584 callee.removeFuncList = function(){
585 if(this.ondispose.__removeFuncList){
586 this.ondispose.__removeFuncList.forEach(
587 (v,ndx)=>{
588 if('number'===typeof v){
589 try{wasm.uninstallFunction(v)}
590 catch(e){/*ignore*/}
591 }
592 /* else it's a descriptive label for the next number in
593 the list. */
594 }
595 );
596 delete this.ondispose.__removeFuncList;
597 }
598 };
599 }/*static init*/
600 const sigN = tgt.memberSignature(name);
601 if(sigN.length<2){
602 toss("Member",name," is not a function pointer. Signature =",sigN);
603 }
604 const memKey = tgt.memberKey(name);
stephanc2ccd672022-09-20 10:47:36 +0000605 const fProxy = 0
stephanf861b362022-10-25 08:06:17 +0000606 /** This middle-man proxy is only for use during development, to
607 confirm that we always pass the proper number of
608 arguments. We know that the C-level code will always use the
609 correct argument count. */
stephanc5313af2022-09-18 02:35:30 +0000610 ? callee.argcProxy(func, sigN)
611 : func;
612 const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
613 tgt[memKey] = pFunc;
614 if(!tgt.ondispose) tgt.ondispose = [];
615 if(!tgt.ondispose.__removeFuncList){
616 tgt.ondispose.push('ondispose.__removeFuncList handler',
617 callee.removeFuncList);
618 tgt.ondispose.__removeFuncList = [];
619 }
620 tgt.ondispose.__removeFuncList.push(memKey, pFunc);
621 return (n,f)=>callee(tgt, n, f);
622 }/*installMethod*/;
stephanf8150112022-09-19 17:09:09 +0000623
624 const opTimer = Object.create(null);
625 opTimer.op = undefined;
626 opTimer.start = undefined;
627 const mTimeStart = (op)=>{
628 opTimer.start = performance.now();
629 opTimer.op = op;
stephanf8150112022-09-19 17:09:09 +0000630 ++metrics[op].count;
631 };
632 const mTimeEnd = ()=>(
633 metrics[opTimer.op].time += performance.now() - opTimer.start
634 );
635
stephanc5313af2022-09-18 02:35:30 +0000636 /**
637 Impls for the sqlite3_io_methods methods. Maintenance reminder:
638 members are in alphabetical order to simplify finding them.
639 */
640 const ioSyncWrappers = {
641 xCheckReservedLock: function(pFile,pOut){
stephanf861b362022-10-25 08:06:17 +0000642 /**
643 As of late 2022, only a single lock can be held on an OPFS
644 file. We have no way of checking whether any _other_ db
645 connection has a lock except by trying to obtain and (on
646 success) release a sync-handle for it, but doing so would
647 involve an inherent race condition. For the time being,
648 pending a better solution, we simply report whether the
649 given pFile instance has a lock.
650 */
stephan9a557732022-10-04 17:06:51 +0000651 const f = __openFiles[pFile];
652 wasm.setMemValue(pOut, f.lockMode ? 1 : 0, 'i32');
stephanc5313af2022-09-18 02:35:30 +0000653 return 0;
654 },
655 xClose: function(pFile){
stephanf8150112022-09-19 17:09:09 +0000656 mTimeStart('xClose');
stephanc5313af2022-09-18 02:35:30 +0000657 let rc = 0;
658 const f = __openFiles[pFile];
659 if(f){
660 delete __openFiles[pFile];
661 rc = opRun('xClose', pFile);
662 if(f.sq3File) f.sq3File.dispose();
663 }
stephanf8150112022-09-19 17:09:09 +0000664 mTimeEnd();
stephanc5313af2022-09-18 02:35:30 +0000665 return rc;
666 },
667 xDeviceCharacteristics: function(pFile){
668 //debug("xDeviceCharacteristics(",pFile,")");
669 return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
670 },
stephanf8150112022-09-19 17:09:09 +0000671 xFileControl: function(pFile, opId, pArg){
672 mTimeStart('xFileControl');
stephanaec046a2022-09-19 18:22:29 +0000673 const rc = (capi.SQLITE_FCNTL_SYNC===opId)
stephan138647a2022-09-20 03:31:02 +0000674 ? opRun('xSync', pFile, 0)
stephanaec046a2022-09-19 18:22:29 +0000675 : capi.SQLITE_NOTFOUND;
stephanf8150112022-09-19 17:09:09 +0000676 mTimeEnd();
stephanaec046a2022-09-19 18:22:29 +0000677 return rc;
stephanc5313af2022-09-18 02:35:30 +0000678 },
679 xFileSize: function(pFile,pSz64){
stephanf8150112022-09-19 17:09:09 +0000680 mTimeStart('xFileSize');
stephanc5313af2022-09-18 02:35:30 +0000681 const rc = opRun('xFileSize', pFile);
stephane8afca32022-09-21 14:02:47 +0000682 if(0==rc){
stephan138647a2022-09-20 03:31:02 +0000683 const sz = state.s11n.deserialize()[0];
stephan278d3fa2022-09-26 13:55:10 +0000684 wasm.setMemValue(pSz64, sz, 'i64');
stephanc5313af2022-09-18 02:35:30 +0000685 }
stephanf8150112022-09-19 17:09:09 +0000686 mTimeEnd();
stephanc5313af2022-09-18 02:35:30 +0000687 return rc;
688 },
689 xLock: function(pFile,lockType){
stephan9a557732022-10-04 17:06:51 +0000690 mTimeStart('xLock');
691 const f = __openFiles[pFile];
692 let rc = 0;
693 if( capi.SQLITE_LOCK_NONE === f.lockType ) {
694 rc = opRun('xLock', pFile, lockType);
695 if( 0===rc ) f.lockType = lockType;
696 }else{
697 f.lockType = lockType;
698 }
699 mTimeEnd();
700 return rc;
stephanc5313af2022-09-18 02:35:30 +0000701 },
stephan138647a2022-09-20 03:31:02 +0000702 xRead: function(pFile,pDest,n,offset64){
stephanf8150112022-09-19 17:09:09 +0000703 mTimeStart('xRead');
stephanc5313af2022-09-18 02:35:30 +0000704 const f = __openFiles[pFile];
705 let rc;
706 try {
stephan138647a2022-09-20 03:31:02 +0000707 rc = opRun('xRead',pFile, n, Number(offset64));
stephanf861b362022-10-25 08:06:17 +0000708 if(0===rc || capi.SQLITE_IOERR_SHORT_READ===rc){
709 /**
710 Results get written to the SharedArrayBuffer f.sabView.
711 Because the heap is _not_ a SharedArrayBuffer, we have
712 to copy the results. TypedArray.set() seems to be the
713 fastest way to copy this. */
stephanf8150112022-09-19 17:09:09 +0000714 wasm.heap8u().set(f.sabView.subarray(0, n), pDest);
stephan862281f2022-09-19 09:25:25 +0000715 }
stephanc5313af2022-09-18 02:35:30 +0000716 }catch(e){
717 error("xRead(",arguments,") failed:",e,f);
718 rc = capi.SQLITE_IOERR_READ;
719 }
stephanf8150112022-09-19 17:09:09 +0000720 mTimeEnd();
stephanc5313af2022-09-18 02:35:30 +0000721 return rc;
722 },
723 xSync: function(pFile,flags){
stephanaec046a2022-09-19 18:22:29 +0000724 ++metrics.xSync.count;
stephan138647a2022-09-20 03:31:02 +0000725 return 0; // impl'd in xFileControl()
stephanc5313af2022-09-18 02:35:30 +0000726 },
727 xTruncate: function(pFile,sz64){
stephanf8150112022-09-19 17:09:09 +0000728 mTimeStart('xTruncate');
stephan138647a2022-09-20 03:31:02 +0000729 const rc = opRun('xTruncate', pFile, Number(sz64));
stephanf8150112022-09-19 17:09:09 +0000730 mTimeEnd();
731 return rc;
stephanc5313af2022-09-18 02:35:30 +0000732 },
733 xUnlock: function(pFile,lockType){
stephan9a557732022-10-04 17:06:51 +0000734 mTimeStart('xUnlock');
735 const f = __openFiles[pFile];
736 let rc = 0;
737 if( capi.SQLITE_LOCK_NONE === lockType
738 && f.lockType ){
739 rc = opRun('xUnlock', pFile, lockType);
740 }
741 if( 0===rc ) f.lockType = lockType;
742 mTimeEnd();
743 return rc;
stephanc5313af2022-09-18 02:35:30 +0000744 },
stephan138647a2022-09-20 03:31:02 +0000745 xWrite: function(pFile,pSrc,n,offset64){
stephanf8150112022-09-19 17:09:09 +0000746 mTimeStart('xWrite');
stephanc5313af2022-09-18 02:35:30 +0000747 const f = __openFiles[pFile];
stephanf8150112022-09-19 17:09:09 +0000748 let rc;
stephanc5313af2022-09-18 02:35:30 +0000749 try {
stephanf8150112022-09-19 17:09:09 +0000750 f.sabView.set(wasm.heap8u().subarray(pSrc, pSrc+n));
stephan138647a2022-09-20 03:31:02 +0000751 rc = opRun('xWrite', pFile, n, Number(offset64));
stephanc5313af2022-09-18 02:35:30 +0000752 }catch(e){
753 error("xWrite(",arguments,") failed:",e,f);
stephanf8150112022-09-19 17:09:09 +0000754 rc = capi.SQLITE_IOERR_WRITE;
stephanc5313af2022-09-18 02:35:30 +0000755 }
stephanf8150112022-09-19 17:09:09 +0000756 mTimeEnd();
757 return rc;
stephanc5313af2022-09-18 02:35:30 +0000758 }
759 }/*ioSyncWrappers*/;
stephan9a557732022-10-04 17:06:51 +0000760
stephanc5313af2022-09-18 02:35:30 +0000761 /**
762 Impls for the sqlite3_vfs methods. Maintenance reminder: members
763 are in alphabetical order to simplify finding them.
764 */
765 const vfsSyncWrappers = {
766 xAccess: function(pVfs,zName,flags,pOut){
stephanf8150112022-09-19 17:09:09 +0000767 mTimeStart('xAccess');
stephan5e8bb0a2022-09-20 08:27:57 +0000768 const rc = opRun('xAccess', wasm.cstringToJs(zName));
769 wasm.setMemValue( pOut, (rc ? 0 : 1), 'i32' );
stephanf8150112022-09-19 17:09:09 +0000770 mTimeEnd();
stephanc5313af2022-09-18 02:35:30 +0000771 return 0;
772 },
773 xCurrentTime: function(pVfs,pOut){
774 /* If it turns out that we need to adjust for timezone, see:
775 https://stackoverflow.com/a/11760121/1458521 */
776 wasm.setMemValue(pOut, 2440587.5 + (new Date().getTime()/86400000),
777 'double');
778 return 0;
779 },
780 xCurrentTimeInt64: function(pVfs,pOut){
781 // TODO: confirm that this calculation is correct
782 wasm.setMemValue(pOut, (2440587.5 * 86400000) + new Date().getTime(),
783 'i64');
784 return 0;
785 },
786 xDelete: function(pVfs, zName, doSyncDir){
stephanf8150112022-09-19 17:09:09 +0000787 mTimeStart('xDelete');
stephan138647a2022-09-20 03:31:02 +0000788 opRun('xDelete', wasm.cstringToJs(zName), doSyncDir, false);
stephanf3860122022-09-18 17:32:35 +0000789 /* We're ignoring errors because we cannot yet differentiate
790 between harmless and non-harmless failures. */
stephanf8150112022-09-19 17:09:09 +0000791 mTimeEnd();
stephanf3860122022-09-18 17:32:35 +0000792 return 0;
stephanc5313af2022-09-18 02:35:30 +0000793 },
794 xFullPathname: function(pVfs,zName,nOut,pOut){
795 /* Until/unless we have some notion of "current dir"
796 in OPFS, simply copy zName to pOut... */
797 const i = wasm.cstrncpy(pOut, zName, nOut);
798 return i<nOut ? 0 : capi.SQLITE_CANTOPEN
799 /*CANTOPEN is required by the docs but SQLITE_RANGE would be a closer match*/;
800 },
801 xGetLastError: function(pVfs,nOut,pOut){
802 /* TODO: store exception.message values from the async
803 partner in a dedicated SharedArrayBuffer, noting that we'd have
804 to encode them... TextEncoder can do that for us. */
805 warn("OPFS xGetLastError() has nothing sensible to return.");
806 return 0;
807 },
stephan8766fd22022-09-19 05:19:04 +0000808 //xSleep is optionally defined below
stephanc5313af2022-09-18 02:35:30 +0000809 xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){
stephanf8150112022-09-19 17:09:09 +0000810 mTimeStart('xOpen');
stephanc5313af2022-09-18 02:35:30 +0000811 if(0===zName){
812 zName = randomFilename();
813 }else if('number'===typeof zName){
814 zName = wasm.cstringToJs(zName);
815 }
stephan138647a2022-09-20 03:31:02 +0000816 const fh = Object.create(null);
817 fh.fid = pFile;
818 fh.filename = zName;
819 fh.sab = new SharedArrayBuffer(state.fileBufferSize);
820 fh.flags = flags;
821 const rc = opRun('xOpen', pFile, zName, flags);
stephanc5313af2022-09-18 02:35:30 +0000822 if(!rc){
823 /* Recall that sqlite3_vfs::xClose() will be called, even on
824 error, unless pFile->pMethods is NULL. */
stephan138647a2022-09-20 03:31:02 +0000825 if(fh.readOnly){
stephanc5313af2022-09-18 02:35:30 +0000826 wasm.setMemValue(pOutFlags, capi.SQLITE_OPEN_READONLY, 'i32');
827 }
stephan138647a2022-09-20 03:31:02 +0000828 __openFiles[pFile] = fh;
829 fh.sabView = state.sabFileBufView;
830 fh.sq3File = new sqlite3_file(pFile);
831 fh.sq3File.$pMethods = opfsIoMethods.pointer;
stephan9a557732022-10-04 17:06:51 +0000832 fh.lockType = capi.SQLITE_LOCK_NONE;
stephanc5313af2022-09-18 02:35:30 +0000833 }
stephanf8150112022-09-19 17:09:09 +0000834 mTimeEnd();
stephanc5313af2022-09-18 02:35:30 +0000835 return rc;
836 }/*xOpen()*/
837 }/*vfsSyncWrappers*/;
838
stephan8766fd22022-09-19 05:19:04 +0000839 if(dVfs){
840 opfsVfs.$xRandomness = dVfs.$xRandomness;
841 opfsVfs.$xSleep = dVfs.$xSleep;
842 }
stephanc5313af2022-09-18 02:35:30 +0000843 if(!opfsVfs.$xRandomness){
844 /* If the default VFS has no xRandomness(), add a basic JS impl... */
845 vfsSyncWrappers.xRandomness = function(pVfs, nOut, pOut){
846 const heap = wasm.heap8u();
847 let i = 0;
848 for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF;
849 return i;
850 };
851 }
852 if(!opfsVfs.$xSleep){
853 /* If we can inherit an xSleep() impl from the default VFS then
stephan8766fd22022-09-19 05:19:04 +0000854 assume it's sane and use it, otherwise install a JS-based
855 one. */
856 vfsSyncWrappers.xSleep = function(pVfs,ms){
stephanc4b87be2022-09-20 01:28:47 +0000857 Atomics.wait(state.sabOPView, state.opIds.xSleep, 0, ms);
stephan8766fd22022-09-19 05:19:04 +0000858 return 0;
859 };
stephanc5313af2022-09-18 02:35:30 +0000860 }
861
862 /* Install the vfs/io_methods into their C-level shared instances... */
stephan278d3fa2022-09-26 13:55:10 +0000863 for(let k of Object.keys(ioSyncWrappers)){
864 installMethod(opfsIoMethods, k, ioSyncWrappers[k]);
865 }
866 for(let k of Object.keys(vfsSyncWrappers)){
867 installMethod(opfsVfs, k, vfsSyncWrappers[k]);
868 }
stephanf3860122022-09-18 17:32:35 +0000869
stephanf3860122022-09-18 17:32:35 +0000870 /**
871 Syncronously deletes the given OPFS filesystem entry, ignoring
872 any errors. As this environment has no notion of "current
873 directory", the given name must be an absolute path. If the 2nd
874 argument is truthy, deletion is recursive (use with caution!).
875
stephan278d3fa2022-09-26 13:55:10 +0000876 Returns true if the deletion succeeded and false if it fails,
stephanf3860122022-09-18 17:32:35 +0000877 but cannot report the nature of the failure.
878 */
stephan0e0687c2022-09-19 13:44:23 +0000879 opfsUtil.deleteEntry = function(fsEntryName,recursive=false){
stephan72ab4002022-09-21 12:27:35 +0000880 mTimeStart('xDelete');
881 const rc = opRun('xDelete', fsEntryName, 0, recursive);
882 mTimeEnd();
883 return 0===rc;
stephanf3860122022-09-18 17:32:35 +0000884 };
885 /**
stephanf3860122022-09-18 17:32:35 +0000886 Synchronously creates the given directory name, recursively, in
887 the OPFS filesystem. Returns true if it succeeds or the
888 directory already exists, else false.
889 */
stephan5e8bb0a2022-09-20 08:27:57 +0000890 opfsUtil.mkdir = function(absDirName){
stephan72ab4002022-09-21 12:27:35 +0000891 mTimeStart('mkdir');
892 const rc = opRun('mkdir', absDirName);
893 mTimeEnd();
894 return 0===rc;
stephanf3860122022-09-18 17:32:35 +0000895 };
896 /**
897 Synchronously checks whether the given OPFS filesystem exists,
898 returning true if it does, false if it doesn't.
899 */
900 opfsUtil.entryExists = function(fsEntryName){
901 return 0===opRun('xAccess', fsEntryName);
902 };
903
904 /**
905 Generates a random ASCII string, intended for use as a
906 temporary file name. Its argument is the length of the string,
907 defaulting to 16.
908 */
909 opfsUtil.randomFilename = randomFilename;
stephan56fae742022-09-24 10:12:19 +0000910
911 /**
912 Re-registers the OPFS VFS. This is intended only for odd use
913 cases which have to call sqlite3_shutdown() as part of their
914 initialization process, which will unregister the VFS
915 registered by installOpfsVfs(). If passed a truthy value, the
916 OPFS VFS is registered as the default VFS, else it is not made
917 the default. Returns the result of the the
918 sqlite3_vfs_register() call.
919
920 Design note: the problem of having to re-register things after
921 a shutdown/initialize pair is more general. How to best plug
922 that in to the library is unclear. In particular, we cannot
923 hook in to any C-side calls to sqlite3_initialize(), so we
924 cannot add an after-initialize callback mechanism.
925 */
stephan3d645482022-09-27 09:17:37 +0000926 opfsUtil.registerVfs = (asDefault=false)=>{
stephan56fae742022-09-24 10:12:19 +0000927 return capi.wasm.exports.sqlite3_vfs_register(
928 opfsVfs.pointer, asDefault ? 1 : 0
929 );
930 };
stephan3d645482022-09-27 09:17:37 +0000931
stephan3c272ba2022-10-04 00:54:00 +0000932 /**
933 Only for test/development use.
934 */
935 opfsUtil.debug = {
936 asyncShutdown: ()=>{
937 warn("Shutting down OPFS async listener. OPFS will no longer work.");
938 opRun('opfs-async-shutdown');
939 },
940 asyncRestart: ()=>{
941 warn("Attempting to restart OPFS async listener. Might work, might not.");
942 W.postMessage({type: 'opfs-async-restart'});
943 }
944 };
945
stephan3d645482022-09-27 09:17:37 +0000946 //TODO to support fiddle db upload:
947 //opfsUtil.createFile = function(absName, content=undefined){...}
948
stephanf3860122022-09-18 17:32:35 +0000949 if(sqlite3.oo1){
950 opfsUtil.OpfsDb = function(...args){
951 const opt = sqlite3.oo1.dbCtorHelper.normalizeArgs(...args);
952 opt.vfs = opfsVfs.$zName;
953 sqlite3.oo1.dbCtorHelper.call(this, opt);
954 };
955 opfsUtil.OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype);
stephan4f5bbed2022-10-03 13:03:41 +0000956 sqlite3.oo1.dbCtorHelper.setVfsPostOpenSql(
957 opfsVfs.pointer,
stephanc7fb48d2022-10-04 09:12:05 +0000958 [
959 /* Truncate journal mode is faster than delete or wal for
960 this vfs, per speedtest1. */
stephaned3182f2022-10-04 11:14:23 +0000961 "pragma journal_mode=truncate;"
stephanc7fb48d2022-10-04 09:12:05 +0000962 /*
963 This vfs benefits hugely from cache on moderate/large
964 speedtest1 --size 50 and --size 100 workloads. We currently
965 rely on setting a non-default cache size when building
966 sqlite3.wasm. If that policy changes, the cache can
967 be set here.
968 */
969 //"pragma cache_size=-8388608;"
970 ].join('')
stephan4f5bbed2022-10-03 13:03:41 +0000971 );
stephanf3860122022-09-18 17:32:35 +0000972 }
stephan4f5bbed2022-10-03 13:03:41 +0000973
stephanf3860122022-09-18 17:32:35 +0000974 /**
975 Potential TODOs:
976
977 - Expose one or both of the Worker objects via opfsUtil and
978 publish an interface for proxying the higher-level OPFS
979 features like getting a directory listing.
980 */
stephan5e8bb0a2022-09-20 08:27:57 +0000981 const sanityCheck = function(){
stephanc5313af2022-09-18 02:35:30 +0000982 const scope = wasm.scopedAllocPush();
983 const sq3File = new sqlite3_file();
984 try{
985 const fid = sq3File.pointer;
986 const openFlags = capi.SQLITE_OPEN_CREATE
987 | capi.SQLITE_OPEN_READWRITE
988 //| capi.SQLITE_OPEN_DELETEONCLOSE
989 | capi.SQLITE_OPEN_MAIN_DB;
990 const pOut = wasm.scopedAlloc(8);
stephanb8c8d4e2022-09-20 13:25:39 +0000991 const dbFile = "/sanity/check/file"+randomFilename(8);
stephanc5313af2022-09-18 02:35:30 +0000992 const zDbFile = wasm.scopedAllocCString(dbFile);
993 let rc;
stephane8afca32022-09-21 14:02:47 +0000994 state.s11n.serialize("This is ä string.");
995 rc = state.s11n.deserialize();
996 log("deserialize() says:",rc);
997 if("This is ä string."!==rc[0]) toss("String d13n error.");
stephanc5313af2022-09-18 02:35:30 +0000998 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
999 rc = wasm.getMemValue(pOut,'i32');
1000 log("xAccess(",dbFile,") exists ?=",rc);
1001 rc = vfsSyncWrappers.xOpen(opfsVfs.pointer, zDbFile,
1002 fid, openFlags, pOut);
stephanc4b87be2022-09-20 01:28:47 +00001003 log("open rc =",rc,"state.sabOPView[xOpen] =",
1004 state.sabOPView[state.opIds.xOpen]);
stephane8afca32022-09-21 14:02:47 +00001005 if(0!==rc){
stephanc5313af2022-09-18 02:35:30 +00001006 error("open failed with code",rc);
1007 return;
1008 }
1009 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
1010 rc = wasm.getMemValue(pOut,'i32');
1011 if(!rc) toss("xAccess() failed to detect file.");
1012 rc = ioSyncWrappers.xSync(sq3File.pointer, 0);
1013 if(rc) toss('sync failed w/ rc',rc);
1014 rc = ioSyncWrappers.xTruncate(sq3File.pointer, 1024);
1015 if(rc) toss('truncate failed w/ rc',rc);
1016 wasm.setMemValue(pOut,0,'i64');
1017 rc = ioSyncWrappers.xFileSize(sq3File.pointer, pOut);
1018 if(rc) toss('xFileSize failed w/ rc',rc);
1019 log("xFileSize says:",wasm.getMemValue(pOut, 'i64'));
1020 rc = ioSyncWrappers.xWrite(sq3File.pointer, zDbFile, 10, 1);
1021 if(rc) toss("xWrite() failed!");
1022 const readBuf = wasm.scopedAlloc(16);
1023 rc = ioSyncWrappers.xRead(sq3File.pointer, readBuf, 6, 2);
1024 wasm.setMemValue(readBuf+6,0);
1025 let jRead = wasm.cstringToJs(readBuf);
1026 log("xRead() got:",jRead);
1027 if("sanity"!==jRead) toss("Unexpected xRead() value.");
stephan8766fd22022-09-19 05:19:04 +00001028 if(vfsSyncWrappers.xSleep){
1029 log("xSleep()ing before close()ing...");
1030 vfsSyncWrappers.xSleep(opfsVfs.pointer,2000);
1031 log("waking up from xSleep()");
1032 }
stephanc5313af2022-09-18 02:35:30 +00001033 rc = ioSyncWrappers.xClose(fid);
stephanc4b87be2022-09-20 01:28:47 +00001034 log("xClose rc =",rc,"sabOPView =",state.sabOPView);
stephanc5313af2022-09-18 02:35:30 +00001035 log("Deleting file:",dbFile);
1036 vfsSyncWrappers.xDelete(opfsVfs.pointer, zDbFile, 0x1234);
1037 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
1038 rc = wasm.getMemValue(pOut,'i32');
1039 if(rc) toss("Expecting 0 from xAccess(",dbFile,") after xDelete().");
stephanc9e26022022-09-20 10:11:52 +00001040 warn("End of OPFS sanity checks.");
stephanc5313af2022-09-18 02:35:30 +00001041 }finally{
1042 sq3File.dispose();
1043 wasm.scopedAllocPop(scope);
1044 }
1045 }/*sanityCheck()*/;
stephan6559e0a2022-09-27 14:31:34 +00001046
stephanc5313af2022-09-18 02:35:30 +00001047 W.onmessage = function({data}){
1048 //log("Worker.onmessage:",data);
1049 switch(data.type){
stephan138647a2022-09-20 03:31:02 +00001050 case 'opfs-async-loaded':
stephane8afca32022-09-21 14:02:47 +00001051 /*Arrives as soon as the asyc proxy finishes loading.
1052 Pass our config and shared state on to the async worker.*/
stephan5e8bb0a2022-09-20 08:27:57 +00001053 W.postMessage({type: 'opfs-async-init',args: state});
stephanc5313af2022-09-18 02:35:30 +00001054 break;
stephan138647a2022-09-20 03:31:02 +00001055 case 'opfs-async-inited':{
stephane8afca32022-09-21 14:02:47 +00001056 /*Indicates that the async partner has received the 'init'
1057 and has finished initializing, so the real work can
1058 begin...*/
stephanc5313af2022-09-18 02:35:30 +00001059 try {
stephan0e0687c2022-09-19 13:44:23 +00001060 const rc = capi.sqlite3_vfs_register(opfsVfs.pointer, 0);
stephanc5313af2022-09-18 02:35:30 +00001061 if(rc){
stephanc5313af2022-09-18 02:35:30 +00001062 toss("sqlite3_vfs_register(OPFS) failed with rc",rc);
1063 }
1064 if(opfsVfs.pointer !== capi.sqlite3_vfs_find("opfs")){
1065 toss("BUG: sqlite3_vfs_find() failed for just-installed OPFS VFS");
1066 }
1067 capi.sqlite3_vfs_register.addReference(opfsVfs, opfsIoMethods);
stephanc4b87be2022-09-20 01:28:47 +00001068 state.sabOPView = new Int32Array(state.sabOP);
stephan138647a2022-09-20 03:31:02 +00001069 state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
1070 state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
1071 initS11n();
stephanc5313af2022-09-18 02:35:30 +00001072 if(options.sanityChecks){
1073 warn("Running sanity checks because of opfs-sanity-check URL arg...");
1074 sanityCheck();
1075 }
stephan1f095d42022-09-26 11:38:58 +00001076 navigator.storage.getDirectory().then((d)=>{
1077 W.onerror = W._originalOnError;
1078 delete W._originalOnError;
1079 sqlite3.opfs = opfsUtil;
1080 opfsUtil.rootDirectory = d;
1081 log("End of OPFS sqlite3_vfs setup.", opfsVfs);
1082 promiseResolve(sqlite3);
1083 });
stephanc5313af2022-09-18 02:35:30 +00001084 }catch(e){
1085 error(e);
1086 promiseReject(e);
1087 }
1088 break;
1089 }
1090 default:
1091 promiseReject(e);
1092 error("Unexpected message from the async worker:",data);
1093 break;
stephanf861b362022-10-25 08:06:17 +00001094 }/*switch(data.type)*/
1095 }/*W.onmessage()*/;
stephanc5313af2022-09-18 02:35:30 +00001096 })/*thePromise*/;
1097 return thePromise;
1098}/*installOpfsVfs()*/;
stephan5b9973d2022-09-27 13:40:12 +00001099installOpfsVfs.defaultProxyUri =
stephan5b9973d2022-09-27 13:40:12 +00001100 "sqlite3-opfs-async-proxy.js";
stephan9a557732022-10-04 17:06:51 +00001101self.sqlite3ApiBootstrap.initializersAsync.push(async (sqlite3)=>{
stephancd0df832022-10-19 04:44:58 +00001102 if(sqlite3.scriptInfo && !sqlite3.scriptInfo.isWorker){
1103 return;
1104 }
stephan9a557732022-10-04 17:06:51 +00001105 try{
stephancd0df832022-10-19 04:44:58 +00001106 let proxyJs = installOpfsVfs.defaultProxyUri;
1107 if(sqlite3.scriptInfo.sqlite3Dir){
1108 installOpfsVfs.defaultProxyUri =
1109 sqlite3.scriptInfo.sqlite3Dir + proxyJs;
1110 //console.warn("installOpfsVfs.defaultProxyUri =",installOpfsVfs.defaultProxyUri);
1111 }
stephanff891b42022-10-09 15:12:37 +00001112 return installOpfsVfs().catch((e)=>{
stephancd0df832022-10-19 04:44:58 +00001113 console.warn("Ignoring inability to install OPFS sqlite3_vfs:",e.message);
stephanff891b42022-10-09 15:12:37 +00001114 });
stephan9a557732022-10-04 17:06:51 +00001115 }catch(e){
1116 console.error("installOpfsVfs() exception:",e);
1117 throw e;
1118 }
1119});
stephanc5313af2022-09-18 02:35:30 +00001120}/*sqlite3ApiBootstrap.initializers.push()*/);