blob: ffc7669a7f4585bdefeaf4e6b3a10bf2db5e8012 [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 };
131 const wMsg = (type,payload)=>W.postMessage({type,payload});
stephan509f4052022-09-19 09:58:01 +0000132 /**
133 Generic utilities for working with OPFS. This will get filled out
134 by the Promise setup and, on success, installed as sqlite3.opfs.
135 */
136 const opfsUtil = Object.create(null);
stephanf8150112022-09-19 17:09:09 +0000137 /**
138 Not part of the public API. Solely for internal/development
139 use.
140 */
141 opfsUtil.metrics = {
142 dump: function(){
stephanaec046a2022-09-19 18:22:29 +0000143 let k, n = 0, t = 0, w = 0;
144 for(k in state.opIds){
stephanf8150112022-09-19 17:09:09 +0000145 const m = metrics[k];
146 n += m.count;
147 t += m.time;
stephanaec046a2022-09-19 18:22:29 +0000148 w += m.wait;
stephanf8150112022-09-19 17:09:09 +0000149 m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
150 m.avgWait = (m.count && m.wait) ? (m.wait / m.count) : 0;
151 }
stephanaec046a2022-09-19 18:22:29 +0000152 console.log(self.location.href,
153 "metrics for",self.location.href,":",metrics,
154 "\nTotal of",n,"op(s) for",t,
155 "ms (incl. "+w+" ms of waiting on the async side)");
stephanf8150112022-09-19 17:09:09 +0000156 },
157 reset: function(){
158 let k;
159 const r = (m)=>(m.count = m.time = m.wait = 0);
160 for(k in state.opIds){
161 r(metrics[k] = Object.create(null));
162 }
stephanaec046a2022-09-19 18:22:29 +0000163 //[ // timed routines which are not in state.opIds
164 // 'xFileControl'
165 //].forEach((k)=>r(metrics[k] = Object.create(null)));
stephanf8150112022-09-19 17:09:09 +0000166 }
167 }/*metrics*/;
stephanc5313af2022-09-18 02:35:30 +0000168
169 /**
170 State which we send to the async-api Worker or share with it.
171 This object must initially contain only cloneable or sharable
172 objects. After the worker's "inited" message arrives, other types
173 of data may be added to it.
stephanf3860122022-09-18 17:32:35 +0000174
175 For purposes of Atomics.wait() and Atomics.notify(), we use a
176 SharedArrayBuffer with one slot reserved for each of the API
177 proxy's methods. The sync side of the API uses Atomics.wait()
178 on the corresponding slot and the async side uses
179 Atomics.notify() on that slot.
180
181 The approach of using a single SAB to serialize comms for all
182 instances might(?) lead to deadlock situations in multi-db
183 cases. We should probably have one SAB here with a single slot
184 for locking a per-file initialization step and then allocate a
185 separate SAB like the above one for each file. That will
186 require a bit of acrobatics but should be feasible.
stephanc5313af2022-09-18 02:35:30 +0000187 */
188 const state = Object.create(null);
189 state.verbose = options.verbose;
stephanf3860122022-09-18 17:32:35 +0000190 state.fileBufferSize =
stephanc4b87be2022-09-20 01:28:47 +0000191 1024 * 64 /* size of aFileHandle.sab. 64k = max sqlite3 page
192 size. */;
193 state.sabOffsetS11n = state.fileBufferSize;
194 state.sabIO = new SharedArrayBuffer(
195 state.fileBufferSize
196 + 4096/* arg/result serialization */
197 + 8 /* to be removed - porting crutch */
198 );
stephanf3860122022-09-18 17:32:35 +0000199 state.fbInt64Offset =
stephanc4b87be2022-09-20 01:28:47 +0000200 state.sabIO.byteLength - 8 /*spot in fileHandle.sab to store an int64 result.
201 to be removed. Porting crutch. */;
stephanc5313af2022-09-18 02:35:30 +0000202 state.opIds = Object.create(null);
stephanf8150112022-09-19 17:09:09 +0000203 const metrics = Object.create(null);
stephanc5313af2022-09-18 02:35:30 +0000204 {
stephan3961b262022-08-10 11:26:08 +0000205 let i = 0;
stephanc4b87be2022-09-20 01:28:47 +0000206 state.opIds.nothing = i++;
stephanc5313af2022-09-18 02:35:30 +0000207 state.opIds.xAccess = i++;
208 state.opIds.xClose = i++;
209 state.opIds.xDelete = i++;
stephanf3860122022-09-18 17:32:35 +0000210 state.opIds.xDeleteNoWait = i++;
stephanc5313af2022-09-18 02:35:30 +0000211 state.opIds.xFileSize = i++;
212 state.opIds.xOpen = i++;
213 state.opIds.xRead = i++;
214 state.opIds.xSleep = i++;
215 state.opIds.xSync = i++;
216 state.opIds.xTruncate = i++;
217 state.opIds.xWrite = i++;
stephanf3860122022-09-18 17:32:35 +0000218 state.opIds.mkdir = i++;
stephanc4b87be2022-09-20 01:28:47 +0000219 state.sabOP = new SharedArrayBuffer(i * 4/*sizeof int32*/);
stephanaec046a2022-09-19 18:22:29 +0000220 state.opIds.xFileControl = state.opIds.xSync /* special case */;
stephanf8150112022-09-19 17:09:09 +0000221 opfsUtil.metrics.reset();
stephanc5313af2022-09-18 02:35:30 +0000222 }
223
224 state.sq3Codes = Object.create(null);
225 state.sq3Codes._reverse = Object.create(null);
226 [ // SQLITE_xxx constants to export to the async worker counterpart...
227 'SQLITE_ERROR', 'SQLITE_IOERR',
228 'SQLITE_NOTFOUND', 'SQLITE_MISUSE',
229 'SQLITE_IOERR_READ', 'SQLITE_IOERR_SHORT_READ',
230 'SQLITE_IOERR_WRITE', 'SQLITE_IOERR_FSYNC',
231 'SQLITE_IOERR_TRUNCATE', 'SQLITE_IOERR_DELETE',
stephanf3860122022-09-18 17:32:35 +0000232 'SQLITE_IOERR_ACCESS', 'SQLITE_IOERR_CLOSE',
stephanc4b87be2022-09-20 01:28:47 +0000233 'SQLITE_IOERR_DELETE',
234 'SQLITE_OPEN_CREATE', 'SQLITE_OPEN_DELETEONCLOSE',
235 'SQLITE_OPEN_READONLY'
stephanc5313af2022-09-18 02:35:30 +0000236 ].forEach(function(k){
237 state.sq3Codes[k] = capi[k] || toss("Maintenance required: not found:",k);
238 state.sq3Codes._reverse[capi[k]] = k;
stephan3961b262022-08-10 11:26:08 +0000239 });
stephan3961b262022-08-10 11:26:08 +0000240
stephanc5313af2022-09-18 02:35:30 +0000241 const isWorkerErrCode = (n)=>!!state.sq3Codes._reverse[n];
stephan3961b262022-08-10 11:26:08 +0000242
stephanc5313af2022-09-18 02:35:30 +0000243 /**
244 Runs the given operation in the async worker counterpart, waits
245 for its response, and returns the result which the async worker
stephanc4b87be2022-09-20 01:28:47 +0000246 writes to the given op's index in state.sabOPView. The 2nd argument
stephanc5313af2022-09-18 02:35:30 +0000247 must be a single object or primitive value, depending on the
248 given operation's signature in the async API counterpart.
249 */
250 const opRun = (op,args)=>{
stephanf8150112022-09-19 17:09:09 +0000251 const t = performance.now();
stephanc4b87be2022-09-20 01:28:47 +0000252 Atomics.store(state.sabOPView, state.opIds[op], -1);
stephanc5313af2022-09-18 02:35:30 +0000253 wMsg(op, args);
stephanc4b87be2022-09-20 01:28:47 +0000254 Atomics.wait(state.sabOPView, state.opIds[op], -1);
stephanf8150112022-09-19 17:09:09 +0000255 metrics[op].wait += performance.now() - t;
stephanc4b87be2022-09-20 01:28:47 +0000256 return Atomics.load(state.sabOPView, state.opIds[op]);
stephanc5313af2022-09-18 02:35:30 +0000257 };
258
259 /**
260 Generates a random ASCII string len characters long, intended for
261 use as a temporary file name.
262 */
263 const randomFilename = function f(len=16){
264 if(!f._chars){
265 f._chars = "abcdefghijklmnopqrstuvwxyz"+
266 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+
267 "012346789";
268 f._n = f._chars.length;
269 }
270 const a = [];
271 let i = 0;
272 for( ; i < len; ++i){
273 const ndx = Math.random() * (f._n * 64) % f._n | 0;
274 a[i] = f._chars[ndx];
275 }
276 return a.join('');
277 };
278
279 /**
280 Map of sqlite3_file pointers to objects constructed by xOpen().
281 */
282 const __openFiles = Object.create(null);
283
284 const pDVfs = capi.sqlite3_vfs_find(null)/*pointer to default VFS*/;
285 const dVfs = pDVfs
286 ? new sqlite3_vfs(pDVfs)
287 : null /* dVfs will be null when sqlite3 is built with
288 SQLITE_OS_OTHER. Though we cannot currently handle
289 that case, the hope is to eventually be able to. */;
290 const opfsVfs = new sqlite3_vfs();
291 const opfsIoMethods = new sqlite3_io_methods();
292 opfsVfs.$iVersion = 2/*yes, two*/;
293 opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof;
294 opfsVfs.$mxPathname = 1024/*sure, why not?*/;
295 opfsVfs.$zName = wasm.allocCString("opfs");
296 // All C-side memory of opfsVfs is zeroed out, but just to be explicit:
297 opfsVfs.$xDlOpen = opfsVfs.$xDlError = opfsVfs.$xDlSym = opfsVfs.$xDlClose = null;
298 opfsVfs.ondispose = [
299 '$zName', opfsVfs.$zName,
300 'cleanup default VFS wrapper', ()=>(dVfs ? dVfs.dispose() : null),
301 'cleanup opfsIoMethods', ()=>opfsIoMethods.dispose()
302 ];
stephanc5313af2022-09-18 02:35:30 +0000303 /**
304 Pedantic sidebar about opfsVfs.ondispose: the entries in that array
305 are items to clean up when opfsVfs.dispose() is called, but in this
306 environment it will never be called. The VFS instance simply
307 hangs around until the WASM module instance is cleaned up. We
308 "could" _hypothetically_ clean it up by "importing" an
309 sqlite3_os_end() impl into the wasm build, but the shutdown order
310 of the wasm engine and the JS one are undefined so there is no
311 guaranty that the opfsVfs instance would be available in one
312 environment or the other when sqlite3_os_end() is called (_if_ it
313 gets called at all in a wasm build, which is undefined).
314 */
315
316 /**
317 Installs a StructBinder-bound function pointer member of the
318 given name and function in the given StructType target object.
319 It creates a WASM proxy for the given function and arranges for
320 that proxy to be cleaned up when tgt.dispose() is called. Throws
321 on the slightest hint of error (e.g. tgt is-not-a StructType,
322 name does not map to a struct-bound member, etc.).
323
324 Returns a proxy for this function which is bound to tgt and takes
325 2 args (name,func). That function returns the same thing,
326 permitting calls to be chained.
327
328 If called with only 1 arg, it has no side effects but returns a
329 func with the same signature as described above.
330 */
331 const installMethod = function callee(tgt, name, func){
stephanf3860122022-09-18 17:32:35 +0000332 if(!(tgt instanceof sqlite3.StructBinder.StructType)){
stephanc5313af2022-09-18 02:35:30 +0000333 toss("Usage error: target object is-not-a StructType.");
334 }
335 if(1===arguments.length){
336 return (n,f)=>callee(tgt,n,f);
337 }
338 if(!callee.argcProxy){
339 callee.argcProxy = function(func,sig){
340 return function(...args){
341 if(func.length!==arguments.length){
342 toss("Argument mismatch. Native signature is:",sig);
343 }
344 return func.apply(this, args);
345 }
346 };
347 callee.removeFuncList = function(){
348 if(this.ondispose.__removeFuncList){
349 this.ondispose.__removeFuncList.forEach(
350 (v,ndx)=>{
351 if('number'===typeof v){
352 try{wasm.uninstallFunction(v)}
353 catch(e){/*ignore*/}
354 }
355 /* else it's a descriptive label for the next number in
356 the list. */
357 }
358 );
359 delete this.ondispose.__removeFuncList;
360 }
361 };
362 }/*static init*/
363 const sigN = tgt.memberSignature(name);
364 if(sigN.length<2){
365 toss("Member",name," is not a function pointer. Signature =",sigN);
366 }
367 const memKey = tgt.memberKey(name);
368 //log("installMethod",tgt, name, sigN);
369 const fProxy = 1
370 // We can remove this proxy middle-man once the VFS is working
371 ? callee.argcProxy(func, sigN)
372 : func;
373 const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
374 tgt[memKey] = pFunc;
375 if(!tgt.ondispose) tgt.ondispose = [];
376 if(!tgt.ondispose.__removeFuncList){
377 tgt.ondispose.push('ondispose.__removeFuncList handler',
378 callee.removeFuncList);
379 tgt.ondispose.__removeFuncList = [];
380 }
381 tgt.ondispose.__removeFuncList.push(memKey, pFunc);
382 return (n,f)=>callee(tgt, n, f);
383 }/*installMethod*/;
stephanf8150112022-09-19 17:09:09 +0000384
385 const opTimer = Object.create(null);
386 opTimer.op = undefined;
387 opTimer.start = undefined;
388 const mTimeStart = (op)=>{
389 opTimer.start = performance.now();
390 opTimer.op = op;
391 //metrics[op] || toss("Maintenance required: missing metrics for",op);
392 ++metrics[op].count;
393 };
394 const mTimeEnd = ()=>(
395 metrics[opTimer.op].time += performance.now() - opTimer.start
396 );
397
stephanc5313af2022-09-18 02:35:30 +0000398 /**
399 Impls for the sqlite3_io_methods methods. Maintenance reminder:
400 members are in alphabetical order to simplify finding them.
401 */
402 const ioSyncWrappers = {
403 xCheckReservedLock: function(pFile,pOut){
404 // Exclusive lock is automatically acquired when opened
405 //warn("xCheckReservedLock(",arguments,") is a no-op");
406 wasm.setMemValue(pOut,1,'i32');
407 return 0;
408 },
409 xClose: function(pFile){
stephanf8150112022-09-19 17:09:09 +0000410 mTimeStart('xClose');
stephanc5313af2022-09-18 02:35:30 +0000411 let rc = 0;
412 const f = __openFiles[pFile];
413 if(f){
414 delete __openFiles[pFile];
415 rc = opRun('xClose', pFile);
416 if(f.sq3File) f.sq3File.dispose();
417 }
stephanf8150112022-09-19 17:09:09 +0000418 mTimeEnd();
stephanc5313af2022-09-18 02:35:30 +0000419 return rc;
420 },
421 xDeviceCharacteristics: function(pFile){
422 //debug("xDeviceCharacteristics(",pFile,")");
423 return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
424 },
stephanf8150112022-09-19 17:09:09 +0000425 xFileControl: function(pFile, opId, pArg){
426 mTimeStart('xFileControl');
stephanaec046a2022-09-19 18:22:29 +0000427 const rc = (capi.SQLITE_FCNTL_SYNC===opId)
428 ? opRun('xSync', {fid:pFile, flags:0})
429 : capi.SQLITE_NOTFOUND;
stephanf8150112022-09-19 17:09:09 +0000430 mTimeEnd();
stephanaec046a2022-09-19 18:22:29 +0000431 return rc;
stephanc5313af2022-09-18 02:35:30 +0000432 },
433 xFileSize: function(pFile,pSz64){
stephanf8150112022-09-19 17:09:09 +0000434 mTimeStart('xFileSize');
stephanc5313af2022-09-18 02:35:30 +0000435 const rc = opRun('xFileSize', pFile);
436 if(!isWorkerErrCode(rc)){
stephanaec046a2022-09-19 18:22:29 +0000437 wasm.setMemValue(
438 pSz64, __openFiles[pFile].sabViewFileSize.getBigInt64(0,true),
439 'i64'
440 );
stephanc5313af2022-09-18 02:35:30 +0000441 }
stephanf8150112022-09-19 17:09:09 +0000442 mTimeEnd();
stephanc5313af2022-09-18 02:35:30 +0000443 return rc;
444 },
445 xLock: function(pFile,lockType){
446 //2022-09: OPFS handles lock when opened
447 //warn("xLock(",arguments,") is a no-op");
448 return 0;
449 },
450 xRead: function(pFile,pDest,n,offset){
451 /* int (*xRead)(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst) */
stephanf8150112022-09-19 17:09:09 +0000452 mTimeStart('xRead');
stephanc5313af2022-09-18 02:35:30 +0000453 const f = __openFiles[pFile];
454 let rc;
455 try {
456 // FIXME(?): block until we finish copying the xRead result buffer. How?
457 rc = opRun('xRead',{fid:pFile, n, offset});
stephan862281f2022-09-19 09:25:25 +0000458 if(0===rc || capi.SQLITE_IOERR_SHORT_READ===rc){
stephanf8150112022-09-19 17:09:09 +0000459 // set() seems to be the fastest way to copy this...
460 wasm.heap8u().set(f.sabView.subarray(0, n), pDest);
stephan862281f2022-09-19 09:25:25 +0000461 }
stephanc5313af2022-09-18 02:35:30 +0000462 }catch(e){
463 error("xRead(",arguments,") failed:",e,f);
464 rc = capi.SQLITE_IOERR_READ;
465 }
stephanf8150112022-09-19 17:09:09 +0000466 mTimeEnd();
stephanc5313af2022-09-18 02:35:30 +0000467 return rc;
468 },
469 xSync: function(pFile,flags){
stephanaec046a2022-09-19 18:22:29 +0000470 ++metrics.xSync.count;
stephanf8150112022-09-19 17:09:09 +0000471 return 0; // impl'd in xFileControl(). opRun('xSync', {fid:pFile, flags});
stephanc5313af2022-09-18 02:35:30 +0000472 },
473 xTruncate: function(pFile,sz64){
stephanf8150112022-09-19 17:09:09 +0000474 mTimeStart('xTruncate');
475 const rc = opRun('xTruncate', {fid:pFile, size: sz64});
476 mTimeEnd();
477 return rc;
stephanc5313af2022-09-18 02:35:30 +0000478 },
479 xUnlock: function(pFile,lockType){
480 //2022-09: OPFS handles lock when opened
481 //warn("xUnlock(",arguments,") is a no-op");
482 return 0;
483 },
484 xWrite: function(pFile,pSrc,n,offset){
485 /* int (*xWrite)(sqlite3_file*, const void*, int iAmt, sqlite3_int64 iOfst) */
stephanf8150112022-09-19 17:09:09 +0000486 mTimeStart('xWrite');
stephanc5313af2022-09-18 02:35:30 +0000487 const f = __openFiles[pFile];
stephanf8150112022-09-19 17:09:09 +0000488 let rc;
stephanc5313af2022-09-18 02:35:30 +0000489 try {
stephanc5313af2022-09-18 02:35:30 +0000490 // FIXME(?): block from here until we finish the xWrite. How?
stephanf8150112022-09-19 17:09:09 +0000491 f.sabView.set(wasm.heap8u().subarray(pSrc, pSrc+n));
492 rc = opRun('xWrite',{fid:pFile, n, offset});
stephanc5313af2022-09-18 02:35:30 +0000493 }catch(e){
494 error("xWrite(",arguments,") failed:",e,f);
stephanf8150112022-09-19 17:09:09 +0000495 rc = capi.SQLITE_IOERR_WRITE;
stephanc5313af2022-09-18 02:35:30 +0000496 }
stephanf8150112022-09-19 17:09:09 +0000497 mTimeEnd();
498 return rc;
stephanc5313af2022-09-18 02:35:30 +0000499 }
500 }/*ioSyncWrappers*/;
501
502 /**
503 Impls for the sqlite3_vfs methods. Maintenance reminder: members
504 are in alphabetical order to simplify finding them.
505 */
506 const vfsSyncWrappers = {
507 xAccess: function(pVfs,zName,flags,pOut){
stephanf8150112022-09-19 17:09:09 +0000508 mTimeStart('xAccess');
stephanaec046a2022-09-19 18:22:29 +0000509 wasm.setMemValue(
510 pOut, (opRun('xAccess', wasm.cstringToJs(zName)) ? 0 : 1), 'i32'
511 );
stephanf8150112022-09-19 17:09:09 +0000512 mTimeEnd();
stephanc5313af2022-09-18 02:35:30 +0000513 return 0;
514 },
515 xCurrentTime: function(pVfs,pOut){
516 /* If it turns out that we need to adjust for timezone, see:
517 https://stackoverflow.com/a/11760121/1458521 */
518 wasm.setMemValue(pOut, 2440587.5 + (new Date().getTime()/86400000),
519 'double');
520 return 0;
521 },
522 xCurrentTimeInt64: function(pVfs,pOut){
523 // TODO: confirm that this calculation is correct
524 wasm.setMemValue(pOut, (2440587.5 * 86400000) + new Date().getTime(),
525 'i64');
526 return 0;
527 },
528 xDelete: function(pVfs, zName, doSyncDir){
stephanf8150112022-09-19 17:09:09 +0000529 mTimeStart('xDelete');
stephanf3860122022-09-18 17:32:35 +0000530 opRun('xDelete', {filename: wasm.cstringToJs(zName), syncDir: doSyncDir});
531 /* We're ignoring errors because we cannot yet differentiate
532 between harmless and non-harmless failures. */
stephanf8150112022-09-19 17:09:09 +0000533 mTimeEnd();
stephanf3860122022-09-18 17:32:35 +0000534 return 0;
stephanc5313af2022-09-18 02:35:30 +0000535 },
536 xFullPathname: function(pVfs,zName,nOut,pOut){
537 /* Until/unless we have some notion of "current dir"
538 in OPFS, simply copy zName to pOut... */
539 const i = wasm.cstrncpy(pOut, zName, nOut);
540 return i<nOut ? 0 : capi.SQLITE_CANTOPEN
541 /*CANTOPEN is required by the docs but SQLITE_RANGE would be a closer match*/;
542 },
543 xGetLastError: function(pVfs,nOut,pOut){
544 /* TODO: store exception.message values from the async
545 partner in a dedicated SharedArrayBuffer, noting that we'd have
546 to encode them... TextEncoder can do that for us. */
547 warn("OPFS xGetLastError() has nothing sensible to return.");
548 return 0;
549 },
stephan8766fd22022-09-19 05:19:04 +0000550 //xSleep is optionally defined below
stephanc5313af2022-09-18 02:35:30 +0000551 xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){
stephanf8150112022-09-19 17:09:09 +0000552 mTimeStart('xOpen');
stephanc5313af2022-09-18 02:35:30 +0000553 if(!f._){
554 f._ = {
555 fileTypes: {
556 SQLITE_OPEN_MAIN_DB: 'mainDb',
557 SQLITE_OPEN_MAIN_JOURNAL: 'mainJournal',
558 SQLITE_OPEN_TEMP_DB: 'tempDb',
559 SQLITE_OPEN_TEMP_JOURNAL: 'tempJournal',
560 SQLITE_OPEN_TRANSIENT_DB: 'transientDb',
561 SQLITE_OPEN_SUBJOURNAL: 'subjournal',
562 SQLITE_OPEN_SUPER_JOURNAL: 'superJournal',
563 SQLITE_OPEN_WAL: 'wal'
564 },
565 getFileType: function(filename,oflags){
566 const ft = f._.fileTypes;
567 for(let k of Object.keys(ft)){
568 if(oflags & capi[k]) return ft[k];
569 }
570 warn("Cannot determine fileType based on xOpen() flags for file",filename);
571 return '???';
572 }
573 };
574 }
575 if(0===zName){
576 zName = randomFilename();
577 }else if('number'===typeof zName){
578 zName = wasm.cstringToJs(zName);
579 }
580 const args = Object.create(null);
581 args.fid = pFile;
582 args.filename = zName;
583 args.sab = new SharedArrayBuffer(state.fileBufferSize);
stephanc4b87be2022-09-20 01:28:47 +0000584 args.flags = flags;
stephanc5313af2022-09-18 02:35:30 +0000585 const rc = opRun('xOpen', args);
586 if(!rc){
587 /* Recall that sqlite3_vfs::xClose() will be called, even on
588 error, unless pFile->pMethods is NULL. */
589 if(args.readOnly){
590 wasm.setMemValue(pOutFlags, capi.SQLITE_OPEN_READONLY, 'i32');
591 }
592 __openFiles[pFile] = args;
stephanc4b87be2022-09-20 01:28:47 +0000593 args.sabView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
594 args.sabViewFileSize = new DataView(state.sabIO, state.fbInt64Offset, 8);
stephanc5313af2022-09-18 02:35:30 +0000595 args.sq3File = new sqlite3_file(pFile);
596 args.sq3File.$pMethods = opfsIoMethods.pointer;
597 args.ba = new Uint8Array(args.sab);
598 }
stephanf8150112022-09-19 17:09:09 +0000599 mTimeEnd();
stephanc5313af2022-09-18 02:35:30 +0000600 return rc;
601 }/*xOpen()*/
602 }/*vfsSyncWrappers*/;
603
stephan8766fd22022-09-19 05:19:04 +0000604 if(dVfs){
605 opfsVfs.$xRandomness = dVfs.$xRandomness;
606 opfsVfs.$xSleep = dVfs.$xSleep;
607 }
stephanc5313af2022-09-18 02:35:30 +0000608 if(!opfsVfs.$xRandomness){
609 /* If the default VFS has no xRandomness(), add a basic JS impl... */
610 vfsSyncWrappers.xRandomness = function(pVfs, nOut, pOut){
611 const heap = wasm.heap8u();
612 let i = 0;
613 for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF;
614 return i;
615 };
616 }
617 if(!opfsVfs.$xSleep){
618 /* If we can inherit an xSleep() impl from the default VFS then
stephan8766fd22022-09-19 05:19:04 +0000619 assume it's sane and use it, otherwise install a JS-based
620 one. */
621 vfsSyncWrappers.xSleep = function(pVfs,ms){
stephanc4b87be2022-09-20 01:28:47 +0000622 Atomics.wait(state.sabOPView, state.opIds.xSleep, 0, ms);
stephan8766fd22022-09-19 05:19:04 +0000623 return 0;
624 };
stephanc5313af2022-09-18 02:35:30 +0000625 }
626
627 /* Install the vfs/io_methods into their C-level shared instances... */
628 let inst = installMethod(opfsIoMethods);
629 for(let k of Object.keys(ioSyncWrappers)) inst(k, ioSyncWrappers[k]);
630 inst = installMethod(opfsVfs);
631 for(let k of Object.keys(vfsSyncWrappers)) inst(k, vfsSyncWrappers[k]);
stephanf3860122022-09-18 17:32:35 +0000632
stephanf3860122022-09-18 17:32:35 +0000633 /**
634 Syncronously deletes the given OPFS filesystem entry, ignoring
635 any errors. As this environment has no notion of "current
636 directory", the given name must be an absolute path. If the 2nd
637 argument is truthy, deletion is recursive (use with caution!).
638
639 Returns true if the deletion succeeded and fails if it fails,
640 but cannot report the nature of the failure.
641 */
stephan0e0687c2022-09-19 13:44:23 +0000642 opfsUtil.deleteEntry = function(fsEntryName,recursive=false){
stephanf3860122022-09-18 17:32:35 +0000643 return 0===opRun('xDelete', {filename:fsEntryName, recursive});
644 };
645 /**
646 Exactly like deleteEntry() but runs asynchronously.
647 */
stephan0e0687c2022-09-19 13:44:23 +0000648 opfsUtil.deleteEntryAsync = async function(fsEntryName,recursive=false){
stephanf3860122022-09-18 17:32:35 +0000649 wMsg('xDeleteNoWait', {filename: fsEntryName, recursive});
650 };
651 /**
652 Synchronously creates the given directory name, recursively, in
653 the OPFS filesystem. Returns true if it succeeds or the
654 directory already exists, else false.
655 */
656 opfsUtil.mkdir = async function(absDirName){
657 return 0===opRun('mkdir', absDirName);
658 };
659 /**
660 Synchronously checks whether the given OPFS filesystem exists,
661 returning true if it does, false if it doesn't.
662 */
663 opfsUtil.entryExists = function(fsEntryName){
664 return 0===opRun('xAccess', fsEntryName);
665 };
666
667 /**
668 Generates a random ASCII string, intended for use as a
669 temporary file name. Its argument is the length of the string,
670 defaulting to 16.
671 */
672 opfsUtil.randomFilename = randomFilename;
673
674 if(sqlite3.oo1){
675 opfsUtil.OpfsDb = function(...args){
676 const opt = sqlite3.oo1.dbCtorHelper.normalizeArgs(...args);
677 opt.vfs = opfsVfs.$zName;
678 sqlite3.oo1.dbCtorHelper.call(this, opt);
679 };
680 opfsUtil.OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype);
681 }
stephanf8150112022-09-19 17:09:09 +0000682
stephanf3860122022-09-18 17:32:35 +0000683 /**
684 Potential TODOs:
685
686 - Expose one or both of the Worker objects via opfsUtil and
687 publish an interface for proxying the higher-level OPFS
688 features like getting a directory listing.
689 */
stephanc5313af2022-09-18 02:35:30 +0000690
691 const sanityCheck = async function(){
692 const scope = wasm.scopedAllocPush();
693 const sq3File = new sqlite3_file();
694 try{
695 const fid = sq3File.pointer;
696 const openFlags = capi.SQLITE_OPEN_CREATE
697 | capi.SQLITE_OPEN_READWRITE
698 //| capi.SQLITE_OPEN_DELETEONCLOSE
699 | capi.SQLITE_OPEN_MAIN_DB;
700 const pOut = wasm.scopedAlloc(8);
701 const dbFile = "/sanity/check/file";
702 const zDbFile = wasm.scopedAllocCString(dbFile);
703 let rc;
704 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
705 rc = wasm.getMemValue(pOut,'i32');
706 log("xAccess(",dbFile,") exists ?=",rc);
707 rc = vfsSyncWrappers.xOpen(opfsVfs.pointer, zDbFile,
708 fid, openFlags, pOut);
stephanc4b87be2022-09-20 01:28:47 +0000709 log("open rc =",rc,"state.sabOPView[xOpen] =",
710 state.sabOPView[state.opIds.xOpen]);
stephanc5313af2022-09-18 02:35:30 +0000711 if(isWorkerErrCode(rc)){
712 error("open failed with code",rc);
713 return;
714 }
715 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
716 rc = wasm.getMemValue(pOut,'i32');
717 if(!rc) toss("xAccess() failed to detect file.");
718 rc = ioSyncWrappers.xSync(sq3File.pointer, 0);
719 if(rc) toss('sync failed w/ rc',rc);
720 rc = ioSyncWrappers.xTruncate(sq3File.pointer, 1024);
721 if(rc) toss('truncate failed w/ rc',rc);
722 wasm.setMemValue(pOut,0,'i64');
723 rc = ioSyncWrappers.xFileSize(sq3File.pointer, pOut);
724 if(rc) toss('xFileSize failed w/ rc',rc);
725 log("xFileSize says:",wasm.getMemValue(pOut, 'i64'));
726 rc = ioSyncWrappers.xWrite(sq3File.pointer, zDbFile, 10, 1);
727 if(rc) toss("xWrite() failed!");
728 const readBuf = wasm.scopedAlloc(16);
729 rc = ioSyncWrappers.xRead(sq3File.pointer, readBuf, 6, 2);
730 wasm.setMemValue(readBuf+6,0);
731 let jRead = wasm.cstringToJs(readBuf);
732 log("xRead() got:",jRead);
733 if("sanity"!==jRead) toss("Unexpected xRead() value.");
stephan8766fd22022-09-19 05:19:04 +0000734 if(vfsSyncWrappers.xSleep){
735 log("xSleep()ing before close()ing...");
736 vfsSyncWrappers.xSleep(opfsVfs.pointer,2000);
737 log("waking up from xSleep()");
738 }
stephanc5313af2022-09-18 02:35:30 +0000739 rc = ioSyncWrappers.xClose(fid);
stephanc4b87be2022-09-20 01:28:47 +0000740 log("xClose rc =",rc,"sabOPView =",state.sabOPView);
stephanc5313af2022-09-18 02:35:30 +0000741 log("Deleting file:",dbFile);
742 vfsSyncWrappers.xDelete(opfsVfs.pointer, zDbFile, 0x1234);
743 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
744 rc = wasm.getMemValue(pOut,'i32');
745 if(rc) toss("Expecting 0 from xAccess(",dbFile,") after xDelete().");
746 }finally{
747 sq3File.dispose();
748 wasm.scopedAllocPop(scope);
749 }
750 }/*sanityCheck()*/;
751
stephanf8150112022-09-19 17:09:09 +0000752
stephanc5313af2022-09-18 02:35:30 +0000753 W.onmessage = function({data}){
754 //log("Worker.onmessage:",data);
755 switch(data.type){
756 case 'loaded':
757 /*Pass our config and shared state on to the async worker.*/
758 wMsg('init',state);
759 break;
760 case 'inited':{
761 /*Indicates that the async partner has received the 'init',
762 so we now know that the state object is no longer subject to
763 being copied by a pending postMessage() call.*/
764 try {
stephan0e0687c2022-09-19 13:44:23 +0000765 const rc = capi.sqlite3_vfs_register(opfsVfs.pointer, 0);
stephanc5313af2022-09-18 02:35:30 +0000766 if(rc){
767 opfsVfs.dispose();
768 toss("sqlite3_vfs_register(OPFS) failed with rc",rc);
769 }
770 if(opfsVfs.pointer !== capi.sqlite3_vfs_find("opfs")){
771 toss("BUG: sqlite3_vfs_find() failed for just-installed OPFS VFS");
772 }
773 capi.sqlite3_vfs_register.addReference(opfsVfs, opfsIoMethods);
stephanc4b87be2022-09-20 01:28:47 +0000774 state.sabOPView = new Int32Array(state.sabOP);
775 state.sabFileBufView = new Uint8Array(state.sabFileBufView, 0, state.fileBufferSize);
stephanc5313af2022-09-18 02:35:30 +0000776 if(options.sanityChecks){
777 warn("Running sanity checks because of opfs-sanity-check URL arg...");
778 sanityCheck();
779 }
stephanf3860122022-09-18 17:32:35 +0000780 W.onerror = W._originalOnError;
781 delete W._originalOnError;
782 sqlite3.opfs = opfsUtil;
stephanc5313af2022-09-18 02:35:30 +0000783 log("End of OPFS sqlite3_vfs setup.", opfsVfs);
stephan509f4052022-09-19 09:58:01 +0000784 promiseResolve(sqlite3);
stephanc5313af2022-09-18 02:35:30 +0000785 }catch(e){
786 error(e);
787 promiseReject(e);
788 }
789 break;
790 }
791 default:
792 promiseReject(e);
793 error("Unexpected message from the async worker:",data);
794 break;
795 }
796 };
797 })/*thePromise*/;
798 return thePromise;
799}/*installOpfsVfs()*/;
800sqlite3.installOpfsVfs.defaultProxyUri = "sqlite3-opfs-async-proxy.js";
801}/*sqlite3ApiBootstrap.initializers.push()*/);