blob: e0554e8c0ca2cc543f32ee465398a31840034647 [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);
stephanc5313af2022-09-18 02:35:30 +0000137
138 /**
139 State which we send to the async-api Worker or share with it.
140 This object must initially contain only cloneable or sharable
141 objects. After the worker's "inited" message arrives, other types
142 of data may be added to it.
stephanf3860122022-09-18 17:32:35 +0000143
144 For purposes of Atomics.wait() and Atomics.notify(), we use a
145 SharedArrayBuffer with one slot reserved for each of the API
146 proxy's methods. The sync side of the API uses Atomics.wait()
147 on the corresponding slot and the async side uses
148 Atomics.notify() on that slot.
149
150 The approach of using a single SAB to serialize comms for all
151 instances might(?) lead to deadlock situations in multi-db
152 cases. We should probably have one SAB here with a single slot
153 for locking a per-file initialization step and then allocate a
154 separate SAB like the above one for each file. That will
155 require a bit of acrobatics but should be feasible.
stephanc5313af2022-09-18 02:35:30 +0000156 */
157 const state = Object.create(null);
158 state.verbose = options.verbose;
stephanf3860122022-09-18 17:32:35 +0000159 state.fileBufferSize =
160 1024 * 64 + 8 /* size of aFileHandle.sab. 64k = max sqlite3 page
161 size. The additional bytes are space for
162 holding BigInt results, since we cannot store
163 those via the Atomics API (which only works on
164 an Int32Array). */;
165 state.fbInt64Offset =
166 state.fileBufferSize - 8 /*spot in fileHandle.sab to store an int64 result */;
stephanc5313af2022-09-18 02:35:30 +0000167 state.opIds = Object.create(null);
168 {
stephan3961b262022-08-10 11:26:08 +0000169 let i = 0;
stephanc5313af2022-09-18 02:35:30 +0000170 state.opIds.xAccess = i++;
171 state.opIds.xClose = i++;
172 state.opIds.xDelete = i++;
stephanf3860122022-09-18 17:32:35 +0000173 state.opIds.xDeleteNoWait = i++;
stephanc5313af2022-09-18 02:35:30 +0000174 state.opIds.xFileSize = i++;
175 state.opIds.xOpen = i++;
176 state.opIds.xRead = i++;
177 state.opIds.xSleep = i++;
178 state.opIds.xSync = i++;
179 state.opIds.xTruncate = i++;
180 state.opIds.xWrite = i++;
stephanf3860122022-09-18 17:32:35 +0000181 state.opIds.mkdir = i++;
stephanc5313af2022-09-18 02:35:30 +0000182 state.opSAB = new SharedArrayBuffer(i * 4/*sizeof int32*/);
stephanc5313af2022-09-18 02:35:30 +0000183 }
184
185 state.sq3Codes = Object.create(null);
186 state.sq3Codes._reverse = Object.create(null);
187 [ // SQLITE_xxx constants to export to the async worker counterpart...
188 'SQLITE_ERROR', 'SQLITE_IOERR',
189 'SQLITE_NOTFOUND', 'SQLITE_MISUSE',
190 'SQLITE_IOERR_READ', 'SQLITE_IOERR_SHORT_READ',
191 'SQLITE_IOERR_WRITE', 'SQLITE_IOERR_FSYNC',
192 'SQLITE_IOERR_TRUNCATE', 'SQLITE_IOERR_DELETE',
stephanf3860122022-09-18 17:32:35 +0000193 'SQLITE_IOERR_ACCESS', 'SQLITE_IOERR_CLOSE',
194 'SQLITE_IOERR_DELETE'
stephanc5313af2022-09-18 02:35:30 +0000195 ].forEach(function(k){
196 state.sq3Codes[k] = capi[k] || toss("Maintenance required: not found:",k);
197 state.sq3Codes._reverse[capi[k]] = k;
stephan3961b262022-08-10 11:26:08 +0000198 });
stephan3961b262022-08-10 11:26:08 +0000199
stephanc5313af2022-09-18 02:35:30 +0000200 const isWorkerErrCode = (n)=>!!state.sq3Codes._reverse[n];
stephan3961b262022-08-10 11:26:08 +0000201
stephanc5313af2022-09-18 02:35:30 +0000202 /**
203 Runs the given operation in the async worker counterpart, waits
204 for its response, and returns the result which the async worker
205 writes to the given op's index in state.opSABView. The 2nd argument
206 must be a single object or primitive value, depending on the
207 given operation's signature in the async API counterpart.
208 */
209 const opRun = (op,args)=>{
stephanf3860122022-09-18 17:32:35 +0000210 Atomics.store(state.opSABView, state.opIds[op], -1);
stephanc5313af2022-09-18 02:35:30 +0000211 wMsg(op, args);
stephanf3860122022-09-18 17:32:35 +0000212 Atomics.wait(state.opSABView, state.opIds[op], -1);
stephanc5313af2022-09-18 02:35:30 +0000213 return Atomics.load(state.opSABView, state.opIds[op]);
214 };
215
216 /**
217 Generates a random ASCII string len characters long, intended for
218 use as a temporary file name.
219 */
220 const randomFilename = function f(len=16){
221 if(!f._chars){
222 f._chars = "abcdefghijklmnopqrstuvwxyz"+
223 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+
224 "012346789";
225 f._n = f._chars.length;
226 }
227 const a = [];
228 let i = 0;
229 for( ; i < len; ++i){
230 const ndx = Math.random() * (f._n * 64) % f._n | 0;
231 a[i] = f._chars[ndx];
232 }
233 return a.join('');
234 };
235
236 /**
237 Map of sqlite3_file pointers to objects constructed by xOpen().
238 */
239 const __openFiles = Object.create(null);
240
241 const pDVfs = capi.sqlite3_vfs_find(null)/*pointer to default VFS*/;
242 const dVfs = pDVfs
243 ? new sqlite3_vfs(pDVfs)
244 : null /* dVfs will be null when sqlite3 is built with
245 SQLITE_OS_OTHER. Though we cannot currently handle
246 that case, the hope is to eventually be able to. */;
247 const opfsVfs = new sqlite3_vfs();
248 const opfsIoMethods = new sqlite3_io_methods();
249 opfsVfs.$iVersion = 2/*yes, two*/;
250 opfsVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof;
251 opfsVfs.$mxPathname = 1024/*sure, why not?*/;
252 opfsVfs.$zName = wasm.allocCString("opfs");
253 // All C-side memory of opfsVfs is zeroed out, but just to be explicit:
254 opfsVfs.$xDlOpen = opfsVfs.$xDlError = opfsVfs.$xDlSym = opfsVfs.$xDlClose = null;
255 opfsVfs.ondispose = [
256 '$zName', opfsVfs.$zName,
257 'cleanup default VFS wrapper', ()=>(dVfs ? dVfs.dispose() : null),
258 'cleanup opfsIoMethods', ()=>opfsIoMethods.dispose()
259 ];
stephanc5313af2022-09-18 02:35:30 +0000260 /**
261 Pedantic sidebar about opfsVfs.ondispose: the entries in that array
262 are items to clean up when opfsVfs.dispose() is called, but in this
263 environment it will never be called. The VFS instance simply
264 hangs around until the WASM module instance is cleaned up. We
265 "could" _hypothetically_ clean it up by "importing" an
266 sqlite3_os_end() impl into the wasm build, but the shutdown order
267 of the wasm engine and the JS one are undefined so there is no
268 guaranty that the opfsVfs instance would be available in one
269 environment or the other when sqlite3_os_end() is called (_if_ it
270 gets called at all in a wasm build, which is undefined).
271 */
272
273 /**
274 Installs a StructBinder-bound function pointer member of the
275 given name and function in the given StructType target object.
276 It creates a WASM proxy for the given function and arranges for
277 that proxy to be cleaned up when tgt.dispose() is called. Throws
278 on the slightest hint of error (e.g. tgt is-not-a StructType,
279 name does not map to a struct-bound member, etc.).
280
281 Returns a proxy for this function which is bound to tgt and takes
282 2 args (name,func). That function returns the same thing,
283 permitting calls to be chained.
284
285 If called with only 1 arg, it has no side effects but returns a
286 func with the same signature as described above.
287 */
288 const installMethod = function callee(tgt, name, func){
stephanf3860122022-09-18 17:32:35 +0000289 if(!(tgt instanceof sqlite3.StructBinder.StructType)){
stephanc5313af2022-09-18 02:35:30 +0000290 toss("Usage error: target object is-not-a StructType.");
291 }
292 if(1===arguments.length){
293 return (n,f)=>callee(tgt,n,f);
294 }
295 if(!callee.argcProxy){
296 callee.argcProxy = function(func,sig){
297 return function(...args){
298 if(func.length!==arguments.length){
299 toss("Argument mismatch. Native signature is:",sig);
300 }
301 return func.apply(this, args);
302 }
303 };
304 callee.removeFuncList = function(){
305 if(this.ondispose.__removeFuncList){
306 this.ondispose.__removeFuncList.forEach(
307 (v,ndx)=>{
308 if('number'===typeof v){
309 try{wasm.uninstallFunction(v)}
310 catch(e){/*ignore*/}
311 }
312 /* else it's a descriptive label for the next number in
313 the list. */
314 }
315 );
316 delete this.ondispose.__removeFuncList;
317 }
318 };
319 }/*static init*/
320 const sigN = tgt.memberSignature(name);
321 if(sigN.length<2){
322 toss("Member",name," is not a function pointer. Signature =",sigN);
323 }
324 const memKey = tgt.memberKey(name);
325 //log("installMethod",tgt, name, sigN);
326 const fProxy = 1
327 // We can remove this proxy middle-man once the VFS is working
328 ? callee.argcProxy(func, sigN)
329 : func;
330 const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
331 tgt[memKey] = pFunc;
332 if(!tgt.ondispose) tgt.ondispose = [];
333 if(!tgt.ondispose.__removeFuncList){
334 tgt.ondispose.push('ondispose.__removeFuncList handler',
335 callee.removeFuncList);
336 tgt.ondispose.__removeFuncList = [];
337 }
338 tgt.ondispose.__removeFuncList.push(memKey, pFunc);
339 return (n,f)=>callee(tgt, n, f);
340 }/*installMethod*/;
341
342 /**
343 Impls for the sqlite3_io_methods methods. Maintenance reminder:
344 members are in alphabetical order to simplify finding them.
345 */
346 const ioSyncWrappers = {
347 xCheckReservedLock: function(pFile,pOut){
348 // Exclusive lock is automatically acquired when opened
349 //warn("xCheckReservedLock(",arguments,") is a no-op");
350 wasm.setMemValue(pOut,1,'i32');
351 return 0;
352 },
353 xClose: function(pFile){
354 let rc = 0;
355 const f = __openFiles[pFile];
356 if(f){
357 delete __openFiles[pFile];
358 rc = opRun('xClose', pFile);
359 if(f.sq3File) f.sq3File.dispose();
360 }
361 return rc;
362 },
363 xDeviceCharacteristics: function(pFile){
364 //debug("xDeviceCharacteristics(",pFile,")");
365 return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
366 },
367 xFileControl: function(pFile,op,pArg){
368 //debug("xFileControl(",arguments,") is a no-op");
369 return capi.SQLITE_NOTFOUND;
370 },
371 xFileSize: function(pFile,pSz64){
372 const rc = opRun('xFileSize', pFile);
373 if(!isWorkerErrCode(rc)){
374 const f = __openFiles[pFile];
375 wasm.setMemValue(pSz64, f.sabViewFileSize.getBigInt64(0) ,'i64');
376 }
377 return rc;
378 },
379 xLock: function(pFile,lockType){
380 //2022-09: OPFS handles lock when opened
381 //warn("xLock(",arguments,") is a no-op");
382 return 0;
383 },
384 xRead: function(pFile,pDest,n,offset){
385 /* int (*xRead)(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst) */
386 const f = __openFiles[pFile];
387 let rc;
388 try {
389 // FIXME(?): block until we finish copying the xRead result buffer. How?
390 rc = opRun('xRead',{fid:pFile, n, offset});
stephan862281f2022-09-19 09:25:25 +0000391 if(0===rc || capi.SQLITE_IOERR_SHORT_READ===rc){
392 let i = 0;
393 for(; i < n; ++i) wasm.setMemValue(pDest + i, f.sabView[i]);
394 }
stephanc5313af2022-09-18 02:35:30 +0000395 }catch(e){
396 error("xRead(",arguments,") failed:",e,f);
397 rc = capi.SQLITE_IOERR_READ;
398 }
399 return rc;
400 },
401 xSync: function(pFile,flags){
402 return opRun('xSync', {fid:pFile, flags});
403 },
404 xTruncate: function(pFile,sz64){
405 return opRun('xTruncate', {fid:pFile, size: sz64});
406 },
407 xUnlock: function(pFile,lockType){
408 //2022-09: OPFS handles lock when opened
409 //warn("xUnlock(",arguments,") is a no-op");
410 return 0;
411 },
412 xWrite: function(pFile,pSrc,n,offset){
413 /* int (*xWrite)(sqlite3_file*, const void*, int iAmt, sqlite3_int64 iOfst) */
414 const f = __openFiles[pFile];
415 try {
416 let i = 0;
417 // FIXME(?): block from here until we finish the xWrite. How?
418 for(; i < n; ++i) f.sabView[i] = wasm.getMemValue(pSrc+i);
419 return opRun('xWrite',{fid:pFile, n, offset});
420 }catch(e){
421 error("xWrite(",arguments,") failed:",e,f);
422 return capi.SQLITE_IOERR_WRITE;
423 }
424 }
425 }/*ioSyncWrappers*/;
426
427 /**
428 Impls for the sqlite3_vfs methods. Maintenance reminder: members
429 are in alphabetical order to simplify finding them.
430 */
431 const vfsSyncWrappers = {
432 xAccess: function(pVfs,zName,flags,pOut){
433 const rc = opRun('xAccess', wasm.cstringToJs(zName));
434 wasm.setMemValue(pOut, rc ? 0 : 1, 'i32');
435 return 0;
436 },
437 xCurrentTime: function(pVfs,pOut){
438 /* If it turns out that we need to adjust for timezone, see:
439 https://stackoverflow.com/a/11760121/1458521 */
440 wasm.setMemValue(pOut, 2440587.5 + (new Date().getTime()/86400000),
441 'double');
442 return 0;
443 },
444 xCurrentTimeInt64: function(pVfs,pOut){
445 // TODO: confirm that this calculation is correct
446 wasm.setMemValue(pOut, (2440587.5 * 86400000) + new Date().getTime(),
447 'i64');
448 return 0;
449 },
450 xDelete: function(pVfs, zName, doSyncDir){
stephanf3860122022-09-18 17:32:35 +0000451 opRun('xDelete', {filename: wasm.cstringToJs(zName), syncDir: doSyncDir});
452 /* We're ignoring errors because we cannot yet differentiate
453 between harmless and non-harmless failures. */
454 return 0;
stephanc5313af2022-09-18 02:35:30 +0000455 },
456 xFullPathname: function(pVfs,zName,nOut,pOut){
457 /* Until/unless we have some notion of "current dir"
458 in OPFS, simply copy zName to pOut... */
459 const i = wasm.cstrncpy(pOut, zName, nOut);
460 return i<nOut ? 0 : capi.SQLITE_CANTOPEN
461 /*CANTOPEN is required by the docs but SQLITE_RANGE would be a closer match*/;
462 },
463 xGetLastError: function(pVfs,nOut,pOut){
464 /* TODO: store exception.message values from the async
465 partner in a dedicated SharedArrayBuffer, noting that we'd have
466 to encode them... TextEncoder can do that for us. */
467 warn("OPFS xGetLastError() has nothing sensible to return.");
468 return 0;
469 },
stephan8766fd22022-09-19 05:19:04 +0000470 //xSleep is optionally defined below
stephanc5313af2022-09-18 02:35:30 +0000471 xOpen: function f(pVfs, zName, pFile, flags, pOutFlags){
472 if(!f._){
473 f._ = {
474 fileTypes: {
475 SQLITE_OPEN_MAIN_DB: 'mainDb',
476 SQLITE_OPEN_MAIN_JOURNAL: 'mainJournal',
477 SQLITE_OPEN_TEMP_DB: 'tempDb',
478 SQLITE_OPEN_TEMP_JOURNAL: 'tempJournal',
479 SQLITE_OPEN_TRANSIENT_DB: 'transientDb',
480 SQLITE_OPEN_SUBJOURNAL: 'subjournal',
481 SQLITE_OPEN_SUPER_JOURNAL: 'superJournal',
482 SQLITE_OPEN_WAL: 'wal'
483 },
484 getFileType: function(filename,oflags){
485 const ft = f._.fileTypes;
486 for(let k of Object.keys(ft)){
487 if(oflags & capi[k]) return ft[k];
488 }
489 warn("Cannot determine fileType based on xOpen() flags for file",filename);
490 return '???';
491 }
492 };
493 }
494 if(0===zName){
495 zName = randomFilename();
496 }else if('number'===typeof zName){
497 zName = wasm.cstringToJs(zName);
498 }
499 const args = Object.create(null);
500 args.fid = pFile;
501 args.filename = zName;
502 args.sab = new SharedArrayBuffer(state.fileBufferSize);
503 args.fileType = f._.getFileType(args.filename, flags);
504 args.create = !!(flags & capi.SQLITE_OPEN_CREATE);
505 args.deleteOnClose = !!(flags & capi.SQLITE_OPEN_DELETEONCLOSE);
506 args.readOnly = !!(flags & capi.SQLITE_OPEN_READONLY);
507 const rc = opRun('xOpen', args);
508 if(!rc){
509 /* Recall that sqlite3_vfs::xClose() will be called, even on
510 error, unless pFile->pMethods is NULL. */
511 if(args.readOnly){
512 wasm.setMemValue(pOutFlags, capi.SQLITE_OPEN_READONLY, 'i32');
513 }
514 __openFiles[pFile] = args;
515 args.sabView = new Uint8Array(args.sab);
516 args.sabViewFileSize = new DataView(args.sab, state.fbInt64Offset, 8);
517 args.sq3File = new sqlite3_file(pFile);
518 args.sq3File.$pMethods = opfsIoMethods.pointer;
519 args.ba = new Uint8Array(args.sab);
520 }
521 return rc;
522 }/*xOpen()*/
523 }/*vfsSyncWrappers*/;
524
stephan8766fd22022-09-19 05:19:04 +0000525 if(dVfs){
526 opfsVfs.$xRandomness = dVfs.$xRandomness;
527 opfsVfs.$xSleep = dVfs.$xSleep;
528 }
stephanc5313af2022-09-18 02:35:30 +0000529 if(!opfsVfs.$xRandomness){
530 /* If the default VFS has no xRandomness(), add a basic JS impl... */
531 vfsSyncWrappers.xRandomness = function(pVfs, nOut, pOut){
532 const heap = wasm.heap8u();
533 let i = 0;
534 for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF;
535 return i;
536 };
537 }
538 if(!opfsVfs.$xSleep){
539 /* If we can inherit an xSleep() impl from the default VFS then
stephan8766fd22022-09-19 05:19:04 +0000540 assume it's sane and use it, otherwise install a JS-based
541 one. */
542 vfsSyncWrappers.xSleep = function(pVfs,ms){
543 Atomics.wait(state.opSABView, state.opIds.xSleep, 0, ms);
544 return 0;
545 };
stephanc5313af2022-09-18 02:35:30 +0000546 }
547
548 /* Install the vfs/io_methods into their C-level shared instances... */
549 let inst = installMethod(opfsIoMethods);
550 for(let k of Object.keys(ioSyncWrappers)) inst(k, ioSyncWrappers[k]);
551 inst = installMethod(opfsVfs);
552 for(let k of Object.keys(vfsSyncWrappers)) inst(k, vfsSyncWrappers[k]);
stephanf3860122022-09-18 17:32:35 +0000553
stephanf3860122022-09-18 17:32:35 +0000554 /**
555 Syncronously deletes the given OPFS filesystem entry, ignoring
556 any errors. As this environment has no notion of "current
557 directory", the given name must be an absolute path. If the 2nd
558 argument is truthy, deletion is recursive (use with caution!).
559
560 Returns true if the deletion succeeded and fails if it fails,
561 but cannot report the nature of the failure.
562 */
563 opfsUtil.deleteEntry = function(fsEntryName,recursive){
564 return 0===opRun('xDelete', {filename:fsEntryName, recursive});
565 };
566 /**
567 Exactly like deleteEntry() but runs asynchronously.
568 */
569 opfsUtil.deleteEntryAsync = async function(fsEntryName,recursive){
570 wMsg('xDeleteNoWait', {filename: fsEntryName, recursive});
571 };
572 /**
573 Synchronously creates the given directory name, recursively, in
574 the OPFS filesystem. Returns true if it succeeds or the
575 directory already exists, else false.
576 */
577 opfsUtil.mkdir = async function(absDirName){
578 return 0===opRun('mkdir', absDirName);
579 };
580 /**
581 Synchronously checks whether the given OPFS filesystem exists,
582 returning true if it does, false if it doesn't.
583 */
584 opfsUtil.entryExists = function(fsEntryName){
585 return 0===opRun('xAccess', fsEntryName);
586 };
587
588 /**
589 Generates a random ASCII string, intended for use as a
590 temporary file name. Its argument is the length of the string,
591 defaulting to 16.
592 */
593 opfsUtil.randomFilename = randomFilename;
594
595 if(sqlite3.oo1){
596 opfsUtil.OpfsDb = function(...args){
597 const opt = sqlite3.oo1.dbCtorHelper.normalizeArgs(...args);
598 opt.vfs = opfsVfs.$zName;
599 sqlite3.oo1.dbCtorHelper.call(this, opt);
600 };
601 opfsUtil.OpfsDb.prototype = Object.create(sqlite3.oo1.DB.prototype);
602 }
603
604 /**
605 Potential TODOs:
606
607 - Expose one or both of the Worker objects via opfsUtil and
608 publish an interface for proxying the higher-level OPFS
609 features like getting a directory listing.
610 */
stephanc5313af2022-09-18 02:35:30 +0000611
612 const sanityCheck = async function(){
613 const scope = wasm.scopedAllocPush();
614 const sq3File = new sqlite3_file();
615 try{
616 const fid = sq3File.pointer;
617 const openFlags = capi.SQLITE_OPEN_CREATE
618 | capi.SQLITE_OPEN_READWRITE
619 //| capi.SQLITE_OPEN_DELETEONCLOSE
620 | capi.SQLITE_OPEN_MAIN_DB;
621 const pOut = wasm.scopedAlloc(8);
622 const dbFile = "/sanity/check/file";
623 const zDbFile = wasm.scopedAllocCString(dbFile);
624 let rc;
625 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
626 rc = wasm.getMemValue(pOut,'i32');
627 log("xAccess(",dbFile,") exists ?=",rc);
628 rc = vfsSyncWrappers.xOpen(opfsVfs.pointer, zDbFile,
629 fid, openFlags, pOut);
stephan509f4052022-09-19 09:58:01 +0000630 log("open rc =",rc,"state.opSABView[xOpen] =",
631 state.opSABView[state.opIds.xOpen]);
stephanc5313af2022-09-18 02:35:30 +0000632 if(isWorkerErrCode(rc)){
633 error("open failed with code",rc);
634 return;
635 }
636 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
637 rc = wasm.getMemValue(pOut,'i32');
638 if(!rc) toss("xAccess() failed to detect file.");
639 rc = ioSyncWrappers.xSync(sq3File.pointer, 0);
640 if(rc) toss('sync failed w/ rc',rc);
641 rc = ioSyncWrappers.xTruncate(sq3File.pointer, 1024);
642 if(rc) toss('truncate failed w/ rc',rc);
643 wasm.setMemValue(pOut,0,'i64');
644 rc = ioSyncWrappers.xFileSize(sq3File.pointer, pOut);
645 if(rc) toss('xFileSize failed w/ rc',rc);
646 log("xFileSize says:",wasm.getMemValue(pOut, 'i64'));
647 rc = ioSyncWrappers.xWrite(sq3File.pointer, zDbFile, 10, 1);
648 if(rc) toss("xWrite() failed!");
649 const readBuf = wasm.scopedAlloc(16);
650 rc = ioSyncWrappers.xRead(sq3File.pointer, readBuf, 6, 2);
651 wasm.setMemValue(readBuf+6,0);
652 let jRead = wasm.cstringToJs(readBuf);
653 log("xRead() got:",jRead);
654 if("sanity"!==jRead) toss("Unexpected xRead() value.");
stephan8766fd22022-09-19 05:19:04 +0000655 if(vfsSyncWrappers.xSleep){
656 log("xSleep()ing before close()ing...");
657 vfsSyncWrappers.xSleep(opfsVfs.pointer,2000);
658 log("waking up from xSleep()");
659 }
stephanc5313af2022-09-18 02:35:30 +0000660 rc = ioSyncWrappers.xClose(fid);
661 log("xClose rc =",rc,"opSABView =",state.opSABView);
662 log("Deleting file:",dbFile);
663 vfsSyncWrappers.xDelete(opfsVfs.pointer, zDbFile, 0x1234);
664 vfsSyncWrappers.xAccess(opfsVfs.pointer, zDbFile, 0, pOut);
665 rc = wasm.getMemValue(pOut,'i32');
666 if(rc) toss("Expecting 0 from xAccess(",dbFile,") after xDelete().");
667 }finally{
668 sq3File.dispose();
669 wasm.scopedAllocPop(scope);
670 }
671 }/*sanityCheck()*/;
672
673 W.onmessage = function({data}){
674 //log("Worker.onmessage:",data);
675 switch(data.type){
676 case 'loaded':
677 /*Pass our config and shared state on to the async worker.*/
678 wMsg('init',state);
679 break;
680 case 'inited':{
681 /*Indicates that the async partner has received the 'init',
682 so we now know that the state object is no longer subject to
683 being copied by a pending postMessage() call.*/
684 try {
685 const rc = capi.sqlite3_vfs_register(opfsVfs.pointer, opfsVfs.$zName);
686 if(rc){
687 opfsVfs.dispose();
688 toss("sqlite3_vfs_register(OPFS) failed with rc",rc);
689 }
690 if(opfsVfs.pointer !== capi.sqlite3_vfs_find("opfs")){
691 toss("BUG: sqlite3_vfs_find() failed for just-installed OPFS VFS");
692 }
693 capi.sqlite3_vfs_register.addReference(opfsVfs, opfsIoMethods);
694 state.opSABView = new Int32Array(state.opSAB);
695 if(options.sanityChecks){
696 warn("Running sanity checks because of opfs-sanity-check URL arg...");
697 sanityCheck();
698 }
stephanf3860122022-09-18 17:32:35 +0000699 W.onerror = W._originalOnError;
700 delete W._originalOnError;
701 sqlite3.opfs = opfsUtil;
stephanc5313af2022-09-18 02:35:30 +0000702 log("End of OPFS sqlite3_vfs setup.", opfsVfs);
stephan509f4052022-09-19 09:58:01 +0000703 promiseResolve(sqlite3);
stephanc5313af2022-09-18 02:35:30 +0000704 }catch(e){
705 error(e);
706 promiseReject(e);
707 }
708 break;
709 }
710 default:
711 promiseReject(e);
712 error("Unexpected message from the async worker:",data);
713 break;
714 }
715 };
716 })/*thePromise*/;
717 return thePromise;
718}/*installOpfsVfs()*/;
719sqlite3.installOpfsVfs.defaultProxyUri = "sqlite3-opfs-async-proxy.js";
720}/*sqlite3ApiBootstrap.initializers.push()*/);