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