blob: 8e0d6152b2c209734f3c690ca260023befdbb20c [file] [log] [blame]
stephan132a87b2022-09-17 15:08:22 +00001/*
2 2022-09-16
3
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
stephanf861b362022-10-25 08:06:17 +000013 A Worker which manages asynchronous OPFS handles on behalf of a
stephane72ddfd2022-10-14 15:52:29 +000014 synchronous API which controls it via a combination of Worker
15 messages, SharedArrayBuffer, and Atomics. It is the asynchronous
16 counterpart of the API defined in sqlite3-api-opfs.js.
stephan132a87b2022-09-17 15:08:22 +000017
18 Highly indebted to:
19
20 https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/OriginPrivateFileSystemVFS.js
21
22 for demonstrating how to use the OPFS APIs.
stephan07315542022-09-17 20:50:12 +000023
24 This file is to be loaded as a Worker. It does not have any direct
25 access to the sqlite3 JS/WASM bits, so any bits which it needs (most
26 notably SQLITE_xxx integer codes) have to be imported into it via an
27 initialization process.
stephanf861b362022-10-25 08:06:17 +000028
29 This file represents an implementation detail of a larger piece of
30 code, and not a public interface. Its details may change at any time
31 and are not intended to be used by any client-level code.
stephan132a87b2022-09-17 15:08:22 +000032*/
stephanf861b362022-10-25 08:06:17 +000033"use strict";
stephan07315542022-09-17 20:50:12 +000034const toss = function(...args){throw new Error(args.join(' '))};
35if(self.window === self){
36 toss("This code cannot run from the main thread.",
37 "Load it as a Worker from a separate Worker.");
38}else if(!navigator.storage.getDirectory){
39 toss("This API requires navigator.storage.getDirectory.");
40}
stephan9a557732022-10-04 17:06:51 +000041
stephan07315542022-09-17 20:50:12 +000042/**
43 Will hold state copied to this object from the syncronous side of
44 this API.
45*/
46const state = Object.create(null);
47/**
48 verbose:
49
50 0 = no logging output
51 1 = only errors
52 2 = warnings and errors
53 3 = debug, warnings, and errors
54*/
55state.verbose = 2;
56
stephan509f4052022-09-19 09:58:01 +000057const loggers = {
58 0:console.error.bind(console),
59 1:console.warn.bind(console),
60 2:console.log.bind(console)
stephan07315542022-09-17 20:50:12 +000061};
stephan509f4052022-09-19 09:58:01 +000062const logImpl = (level,...args)=>{
63 if(state.verbose>level) loggers[level]("OPFS asyncer:",...args);
stephan07315542022-09-17 20:50:12 +000064};
stephan509f4052022-09-19 09:58:01 +000065const log = (...args)=>logImpl(2, ...args);
66const warn = (...args)=>logImpl(1, ...args);
67const error = (...args)=>logImpl(0, ...args);
stephanf8150112022-09-19 17:09:09 +000068const metrics = Object.create(null);
stephanaec046a2022-09-19 18:22:29 +000069metrics.reset = ()=>{
70 let k;
stephan5e8bb0a2022-09-20 08:27:57 +000071 const r = (m)=>(m.count = m.time = m.wait = 0);
stephanaec046a2022-09-19 18:22:29 +000072 for(k in state.opIds){
73 r(metrics[k] = Object.create(null));
74 }
stephanb8c8d4e2022-09-20 13:25:39 +000075 let s = metrics.s11n = Object.create(null);
76 s = s.serialize = Object.create(null);
77 s.count = s.time = 0;
78 s = metrics.s11n.deserialize = Object.create(null);
79 s.count = s.time = 0;
stephanaec046a2022-09-19 18:22:29 +000080};
81metrics.dump = ()=>{
82 let k, n = 0, t = 0, w = 0;
83 for(k in state.opIds){
84 const m = metrics[k];
85 n += m.count;
86 t += m.time;
stephan5e8bb0a2022-09-20 08:27:57 +000087 w += m.wait;
stephanaec046a2022-09-19 18:22:29 +000088 m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
89 }
90 console.log(self.location.href,
stephan5e8bb0a2022-09-20 08:27:57 +000091 "metrics for",self.location.href,":\n",
stephan56fae742022-09-24 10:12:19 +000092 metrics,
stephan5e8bb0a2022-09-20 08:27:57 +000093 "\nTotal of",n,"op(s) for",t,"ms",
94 "approx",w,"ms spent waiting on OPFS APIs.");
stephan56fae742022-09-24 10:12:19 +000095 console.log("Serialization metrics:",metrics.s11n);
stephanaec046a2022-09-19 18:22:29 +000096};
stephan07315542022-09-17 20:50:12 +000097
stephan07315542022-09-17 20:50:12 +000098/**
99 Map of sqlite3_file pointers (integers) to metadata related to a
100 given OPFS file handles. The pointers are, in this side of the
101 interface, opaque file handle IDs provided by the synchronous
102 part of this constellation. Each value is an object with a structure
103 demonstrated in the xOpen() impl.
104*/
105const __openFiles = Object.create(null);
106
107/**
stephan8200a6d2022-09-17 23:29:27 +0000108 Expects an OPFS file path. It gets resolved, such that ".."
stephane72ddfd2022-10-14 15:52:29 +0000109 components are properly expanded, and returned. If the 2nd arg is
110 true, the result is returned as an array of path elements, else an
111 absolute path string is returned.
stephan07315542022-09-17 20:50:12 +0000112*/
stephan8200a6d2022-09-17 23:29:27 +0000113const getResolvedPath = function(filename,splitIt){
114 const p = new URL(
115 filename, 'file://irrelevant'
116 ).pathname;
117 return splitIt ? p.split('/').filter((v)=>!!v) : p;
stephan509f4052022-09-19 09:58:01 +0000118};
stephan07315542022-09-17 20:50:12 +0000119
120/**
121 Takes the absolute path to a filesystem element. Returns an array
stephane72ddfd2022-10-14 15:52:29 +0000122 of [handleOfContainingDir, filename]. If the 2nd argument is truthy
123 then each directory element leading to the file is created along
124 the way. Throws if any creation or resolution fails.
stephan07315542022-09-17 20:50:12 +0000125*/
stephan56fae742022-09-24 10:12:19 +0000126const getDirForFilename = async function f(absFilename, createDirs = false){
stephan8200a6d2022-09-17 23:29:27 +0000127 const path = getResolvedPath(absFilename, true);
stephan07315542022-09-17 20:50:12 +0000128 const filename = path.pop();
stephan8200a6d2022-09-17 23:29:27 +0000129 let dh = state.rootDir;
130 for(const dirName of path){
131 if(dirName){
132 dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs});
stephan07315542022-09-17 20:50:12 +0000133 }
stephan132a87b2022-09-17 15:08:22 +0000134 }
stephan07315542022-09-17 20:50:12 +0000135 return [dh, filename];
136};
stephan132a87b2022-09-17 15:08:22 +0000137
stephan7ff8da82022-10-03 09:21:37 +0000138/**
stephand0010602022-10-30 11:22:45 +0000139 An error class specifically for use with getSyncHandle(), the goal
140 of which is to eventually be able to distinguish unambiguously
141 between locking-related failures and other types, noting that we
142 cannot currently do so because createSyncAccessHandle() does not
143 define its exceptions in the required level of detail.
144*/
145class GetSyncHandleError extends Error {
146 constructor(errorObject, ...msg){
147 super();
148 this.error = errorObject;
149 this.message = [
150 ...msg, ': Original exception ['+errorObject.name+']:',
151 errorObject.message
152 ].join(' ');
153 this.name = 'GetSyncHandleError';
154 }
155};
156
157/**
stephan7ff8da82022-10-03 09:21:37 +0000158 Returns the sync access handle associated with the given file
stephanf861b362022-10-25 08:06:17 +0000159 handle object (which must be a valid handle object, as created by
160 xOpen()), lazily opening it if needed.
stephanc7fb48d2022-10-04 09:12:05 +0000161
162 In order to help alleviate cross-tab contention for a dabase,
163 if an exception is thrown while acquiring the handle, this routine
164 will wait briefly and try again, up to 3 times. If acquisition
165 still fails at that point it will give up and propagate the
166 exception.
stephan7ff8da82022-10-03 09:21:37 +0000167*/
stephan5f0b67c2022-10-03 11:33:35 +0000168const getSyncHandle = async (fh)=>{
169 if(!fh.syncHandle){
stephanc7fb48d2022-10-04 09:12:05 +0000170 const t = performance.now();
171 log("Acquiring sync handle for",fh.filenameAbs);
stephan9163ef12022-10-30 10:24:53 +0000172 const maxTries = 4, msBase = 300;
173 let i = 1, ms = msBase;
174 for(; true; ms = msBase * ++i){
stephanc7fb48d2022-10-04 09:12:05 +0000175 try {
stephand0010602022-10-30 11:22:45 +0000176 //if(1 || i<2) toss("Just testing getSyncHandle() exception handling.");
stephanc7fb48d2022-10-04 09:12:05 +0000177 //TODO? A config option which tells it to throw here
stephan3e771c02022-10-14 13:26:18 +0000178 //randomly every now and then, for testing purposes.
stephanc7fb48d2022-10-04 09:12:05 +0000179 fh.syncHandle = await fh.fileHandle.createSyncAccessHandle();
180 break;
181 }catch(e){
182 if(i === maxTries){
stephand0010602022-10-30 11:22:45 +0000183 throw new GetSyncHandleError(
184 e, "Error getting sync handle.",maxTries,
185 "attempts failed.",fh.filenameAbs
186 );
stephanc7fb48d2022-10-04 09:12:05 +0000187 }
stephan3e771c02022-10-14 13:26:18 +0000188 warn("Error getting sync handle. Waiting",ms,
stephanc7fb48d2022-10-04 09:12:05 +0000189 "ms and trying again.",fh.filenameAbs,e);
stephanf861b362022-10-25 08:06:17 +0000190 Atomics.wait(state.sabOPView, state.opIds.retry, 0, ms);
stephanc7fb48d2022-10-04 09:12:05 +0000191 }
192 }
193 log("Got sync handle for",fh.filenameAbs,'in',performance.now() - t,'ms');
stephan5f0b67c2022-10-03 11:33:35 +0000194 }
stephan5f0b67c2022-10-03 11:33:35 +0000195 return fh.syncHandle;
196};
197
stephane72ddfd2022-10-14 15:52:29 +0000198/**
199 If the given file-holding object has a sync handle attached to it,
200 that handle is remove and asynchronously closed. Though it may
201 sound sensible to continue work as soon as the close() returns
202 (noting that it's asynchronous), doing so can cause operations
203 performed soon afterwards, e.g. a call to getSyncHandle() to fail
204 because they may happen out of order from the close(). OPFS does
205 not guaranty that the actual order of operations is retained in
206 such cases. i.e. always "await" on the result of this function.
207*/
stephan5f0b67c2022-10-03 11:33:35 +0000208const closeSyncHandle = async (fh)=>{
209 if(fh.syncHandle){
stephan9a557732022-10-04 17:06:51 +0000210 log("Closing sync handle for",fh.filenameAbs);
stephan5f0b67c2022-10-03 11:33:35 +0000211 const h = fh.syncHandle;
212 delete fh.syncHandle;
213 return h.close();
214 }
215};
stephan132a87b2022-09-17 15:08:22 +0000216
stephan07315542022-09-17 20:50:12 +0000217/**
stephan72ab4002022-09-21 12:27:35 +0000218 Stores the given value at state.sabOPView[state.opIds.rc] and then
219 Atomics.notify()'s it.
stephan07315542022-09-17 20:50:12 +0000220*/
221const storeAndNotify = (opName, value)=>{
stephand0010602022-10-30 11:22:45 +0000222 log(opName+"() => notify( rc =",value,")");
stephanc9e26022022-09-20 10:11:52 +0000223 Atomics.store(state.sabOPView, state.opIds.rc, value);
224 Atomics.notify(state.sabOPView, state.opIds.rc);
stephan07315542022-09-17 20:50:12 +0000225};
stephan132a87b2022-09-17 15:08:22 +0000226
stephan07315542022-09-17 20:50:12 +0000227/**
228 Throws if fh is a file-holding object which is flagged as read-only.
229*/
230const affirmNotRO = function(opName,fh){
231 if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs);
232};
233
stephane72ddfd2022-10-14 15:52:29 +0000234/**
235 We track 2 different timers: the "metrics" timer records how much
236 time we spend performing work. The "wait" timer records how much
237 time we spend waiting on the underlying OPFS timer. See the calls
238 to mTimeStart(), mTimeEnd(), wTimeStart(), and wTimeEnd()
239 throughout this file to see how they're used.
240*/
241const __mTimer = Object.create(null);
242__mTimer.op = undefined;
243__mTimer.start = undefined;
stephanaec046a2022-09-19 18:22:29 +0000244const mTimeStart = (op)=>{
stephane72ddfd2022-10-14 15:52:29 +0000245 __mTimer.start = performance.now();
246 __mTimer.op = op;
stephanaec046a2022-09-19 18:22:29 +0000247 //metrics[op] || toss("Maintenance required: missing metrics for",op);
248 ++metrics[op].count;
249};
250const mTimeEnd = ()=>(
stephane72ddfd2022-10-14 15:52:29 +0000251 metrics[__mTimer.op].time += performance.now() - __mTimer.start
stephanaec046a2022-09-19 18:22:29 +0000252);
stephane72ddfd2022-10-14 15:52:29 +0000253const __wTimer = Object.create(null);
254__wTimer.op = undefined;
255__wTimer.start = undefined;
stephan5e8bb0a2022-09-20 08:27:57 +0000256const wTimeStart = (op)=>{
stephane72ddfd2022-10-14 15:52:29 +0000257 __wTimer.start = performance.now();
258 __wTimer.op = op;
stephan5e8bb0a2022-09-20 08:27:57 +0000259 //metrics[op] || toss("Maintenance required: missing metrics for",op);
260};
261const wTimeEnd = ()=>(
stephane72ddfd2022-10-14 15:52:29 +0000262 metrics[__wTimer.op].wait += performance.now() - __wTimer.start
stephan5e8bb0a2022-09-20 08:27:57 +0000263);
stephanaec046a2022-09-19 18:22:29 +0000264
stephan07315542022-09-17 20:50:12 +0000265/**
stephane72ddfd2022-10-14 15:52:29 +0000266 Gets set to true by the 'opfs-async-shutdown' command to quit the
267 wait loop. This is only intended for debugging purposes: we cannot
268 inspect this file's state while the tight waitLoop() is running and
269 need a way to stop that loop for introspection purposes.
stephan3c272ba2022-10-04 00:54:00 +0000270*/
271let flagAsyncShutdown = false;
272
stephan9a557732022-10-04 17:06:51 +0000273
stephan3c272ba2022-10-04 00:54:00 +0000274/**
stephan07315542022-09-17 20:50:12 +0000275 Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods
stephane72ddfd2022-10-14 15:52:29 +0000276 methods, as well as helpers like mkdir(). Maintenance reminder:
277 members are in alphabetical order to simplify finding them.
stephan07315542022-09-17 20:50:12 +0000278*/
279const vfsAsyncImpls = {
stephan3c272ba2022-10-04 00:54:00 +0000280 'opfs-async-metrics': async ()=>{
281 mTimeStart('opfs-async-metrics');
stephan56fae742022-09-24 10:12:19 +0000282 metrics.dump();
stephan3c272ba2022-10-04 00:54:00 +0000283 storeAndNotify('opfs-async-metrics', 0);
stephan56fae742022-09-24 10:12:19 +0000284 mTimeEnd();
285 },
stephan3c272ba2022-10-04 00:54:00 +0000286 'opfs-async-shutdown': async ()=>{
287 flagAsyncShutdown = true;
288 storeAndNotify('opfs-async-shutdown', 0);
289 },
stephan56fae742022-09-24 10:12:19 +0000290 mkdir: async (dirname)=>{
stephan5e8bb0a2022-09-20 08:27:57 +0000291 mTimeStart('mkdir');
stephanaec046a2022-09-19 18:22:29 +0000292 let rc = 0;
stephan5e8bb0a2022-09-20 08:27:57 +0000293 wTimeStart('mkdir');
stephanaec046a2022-09-19 18:22:29 +0000294 try {
stephan56fae742022-09-24 10:12:19 +0000295 await getDirForFilename(dirname+"/filepart", true);
stephanaec046a2022-09-19 18:22:29 +0000296 }catch(e){
stephan56fae742022-09-24 10:12:19 +0000297 state.s11n.storeException(2,e);
stephanaec046a2022-09-19 18:22:29 +0000298 rc = state.sq3Codes.SQLITE_IOERR;
stephan72ab4002022-09-21 12:27:35 +0000299 }finally{
300 wTimeEnd();
stephanaec046a2022-09-19 18:22:29 +0000301 }
302 storeAndNotify('mkdir', rc);
stephan5e8bb0a2022-09-20 08:27:57 +0000303 mTimeEnd();
stephanaec046a2022-09-19 18:22:29 +0000304 },
stephan56fae742022-09-24 10:12:19 +0000305 xAccess: async (filename)=>{
stephanaec046a2022-09-19 18:22:29 +0000306 mTimeStart('xAccess');
stephan8200a6d2022-09-17 23:29:27 +0000307 /* OPFS cannot support the full range of xAccess() queries sqlite3
308 calls for. We can essentially just tell if the file is
309 accessible, but if it is it's automatically writable (unless
310 it's locked, which we cannot(?) know without trying to open
311 it). OPFS does not have the notion of read-only.
312
313 The return semantics of this function differ from sqlite3's
314 xAccess semantics because we are limited in what we can
315 communicate back to our synchronous communication partner: 0 =
316 accessible, non-0 means not accessible.
317 */
318 let rc = 0;
stephan5e8bb0a2022-09-20 08:27:57 +0000319 wTimeStart('xAccess');
stephan8200a6d2022-09-17 23:29:27 +0000320 try{
stephan56fae742022-09-24 10:12:19 +0000321 const [dh, fn] = await getDirForFilename(filename);
stephan8200a6d2022-09-17 23:29:27 +0000322 await dh.getFileHandle(fn);
323 }catch(e){
stephan56fae742022-09-24 10:12:19 +0000324 state.s11n.storeException(2,e);
stephan8200a6d2022-09-17 23:29:27 +0000325 rc = state.sq3Codes.SQLITE_IOERR;
stephan72ab4002022-09-21 12:27:35 +0000326 }finally{
327 wTimeEnd();
stephan8200a6d2022-09-17 23:29:27 +0000328 }
stephan07315542022-09-17 20:50:12 +0000329 storeAndNotify('xAccess', rc);
stephanaec046a2022-09-19 18:22:29 +0000330 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000331 },
stephanf861b362022-10-25 08:06:17 +0000332 xClose: async function(fid/*sqlite3_file pointer*/){
stephan07315542022-09-17 20:50:12 +0000333 const opName = 'xClose';
stephanaec046a2022-09-19 18:22:29 +0000334 mTimeStart(opName);
stephan07315542022-09-17 20:50:12 +0000335 const fh = __openFiles[fid];
stephan5e8bb0a2022-09-20 08:27:57 +0000336 let rc = 0;
337 wTimeStart('xClose');
stephan07315542022-09-17 20:50:12 +0000338 if(fh){
339 delete __openFiles[fid];
stephan5f0b67c2022-10-03 11:33:35 +0000340 await closeSyncHandle(fh);
stephan07315542022-09-17 20:50:12 +0000341 if(fh.deleteOnClose){
342 try{ await fh.dirHandle.removeEntry(fh.filenamePart) }
343 catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) }
stephan132a87b2022-09-17 15:08:22 +0000344 }
stephan07315542022-09-17 20:50:12 +0000345 }else{
stephan72ab4002022-09-21 12:27:35 +0000346 state.s11n.serialize();
stephan5e8bb0a2022-09-20 08:27:57 +0000347 rc = state.sq3Codes.SQLITE_NOTFOUND;
stephan132a87b2022-09-17 15:08:22 +0000348 }
stephan5e8bb0a2022-09-20 08:27:57 +0000349 wTimeEnd();
350 storeAndNotify(opName, rc);
stephanaec046a2022-09-19 18:22:29 +0000351 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000352 },
stephanc4b87be2022-09-20 01:28:47 +0000353 xDelete: async function(...args){
354 mTimeStart('xDelete');
355 const rc = await vfsAsyncImpls.xDeleteNoWait(...args);
356 storeAndNotify('xDelete', rc);
357 mTimeEnd();
358 },
stephan138647a2022-09-20 03:31:02 +0000359 xDeleteNoWait: async function(filename, syncDir = 0, recursive = false){
stephan8200a6d2022-09-17 23:29:27 +0000360 /* The syncDir flag is, for purposes of the VFS API's semantics,
361 ignored here. However, if it has the value 0x1234 then: after
362 deleting the given file, recursively try to delete any empty
363 directories left behind in its wake (ignoring any errors and
364 stopping at the first failure).
365
366 That said: we don't know for sure that removeEntry() fails if
367 the dir is not empty because the API is not documented. It has,
368 however, a "recursive" flag which defaults to false, so
369 presumably it will fail if the dir is not empty and that flag
370 is false.
371 */
stephanf3860122022-09-18 17:32:35 +0000372 let rc = 0;
stephan5e8bb0a2022-09-20 08:27:57 +0000373 wTimeStart('xDelete');
stephan07315542022-09-17 20:50:12 +0000374 try {
stephan8200a6d2022-09-17 23:29:27 +0000375 while(filename){
stephan56fae742022-09-24 10:12:19 +0000376 const [hDir, filenamePart] = await getDirForFilename(filename, false);
stephan8200a6d2022-09-17 23:29:27 +0000377 if(!filenamePart) break;
stephanf3860122022-09-18 17:32:35 +0000378 await hDir.removeEntry(filenamePart, {recursive});
stephan8200a6d2022-09-17 23:29:27 +0000379 if(0x1234 !== syncDir) break;
380 filename = getResolvedPath(filename, true);
381 filename.pop();
382 filename = filename.join('/');
383 }
stephan07315542022-09-17 20:50:12 +0000384 }catch(e){
stephan56fae742022-09-24 10:12:19 +0000385 state.s11n.storeException(2,e);
stephanf3860122022-09-18 17:32:35 +0000386 rc = state.sq3Codes.SQLITE_IOERR_DELETE;
stephan132a87b2022-09-17 15:08:22 +0000387 }
stephan5e8bb0a2022-09-20 08:27:57 +0000388 wTimeEnd();
stephanf3860122022-09-18 17:32:35 +0000389 return rc;
390 },
stephanf861b362022-10-25 08:06:17 +0000391 xFileSize: async function(fid/*sqlite3_file pointer*/){
stephanaec046a2022-09-19 18:22:29 +0000392 mTimeStart('xFileSize');
stephan07315542022-09-17 20:50:12 +0000393 const fh = __openFiles[fid];
stephand0010602022-10-30 11:22:45 +0000394 let rc;
stephan5e8bb0a2022-09-20 08:27:57 +0000395 wTimeStart('xFileSize');
stephan07315542022-09-17 20:50:12 +0000396 try{
stephand0010602022-10-30 11:22:45 +0000397 rc = await (await getSyncHandle(fh)).getSize();
398 state.s11n.serialize(Number(rc));
399 rc = 0;
stephan07315542022-09-17 20:50:12 +0000400 }catch(e){
stephan56fae742022-09-24 10:12:19 +0000401 state.s11n.storeException(2,e);
stephand0010602022-10-30 11:22:45 +0000402 rc = (e instanceof GetSyncHandleError)
403 ? state.sq3Codes.SQLITE_IOERR_LOCK
404 : state.sq3Codes.SQLITE_IOERR;
stephan132a87b2022-09-17 15:08:22 +0000405 }
stephan5e8bb0a2022-09-20 08:27:57 +0000406 wTimeEnd();
stephand0010602022-10-30 11:22:45 +0000407 storeAndNotify('xFileSize', rc);
stephanaec046a2022-09-19 18:22:29 +0000408 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000409 },
stephanf861b362022-10-25 08:06:17 +0000410 xLock: async function(fid/*sqlite3_file pointer*/,
411 lockType/*SQLITE_LOCK_...*/){
stephan9a557732022-10-04 17:06:51 +0000412 mTimeStart('xLock');
413 const fh = __openFiles[fid];
414 let rc = 0;
415 if( !fh.syncHandle ){
stephane72ddfd2022-10-14 15:52:29 +0000416 wTimeStart('xLock');
stephan9a557732022-10-04 17:06:51 +0000417 try { await getSyncHandle(fh) }
418 catch(e){
419 state.s11n.storeException(1,e);
stephand0010602022-10-30 11:22:45 +0000420 rc = (e instanceof GetSyncHandleError)
421 ? state.sq3Codes.SQLITE_IOERR_LOCK
422 : state.sq3Codes.SQLITE_IOERR;
stephan9a557732022-10-04 17:06:51 +0000423 }
stephane72ddfd2022-10-14 15:52:29 +0000424 wTimeEnd();
stephan9a557732022-10-04 17:06:51 +0000425 }
426 storeAndNotify('xLock',rc);
427 mTimeEnd();
428 },
stephanf861b362022-10-25 08:06:17 +0000429 xOpen: async function(fid/*sqlite3_file pointer*/, filename,
430 flags/*SQLITE_OPEN_...*/){
stephan07315542022-09-17 20:50:12 +0000431 const opName = 'xOpen';
stephanaec046a2022-09-19 18:22:29 +0000432 mTimeStart(opName);
stephanc4b87be2022-09-20 01:28:47 +0000433 const deleteOnClose = (state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags);
434 const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags);
stephan5e8bb0a2022-09-20 08:27:57 +0000435 wTimeStart('xOpen');
stephan07315542022-09-17 20:50:12 +0000436 try{
stephan07315542022-09-17 20:50:12 +0000437 let hDir, filenamePart;
438 try {
stephan56fae742022-09-24 10:12:19 +0000439 [hDir, filenamePart] = await getDirForFilename(filename, !!create);
stephan132a87b2022-09-17 15:08:22 +0000440 }catch(e){
stephan07315542022-09-17 20:50:12 +0000441 storeAndNotify(opName, state.sql3Codes.SQLITE_NOTFOUND);
stephanaec046a2022-09-19 18:22:29 +0000442 mTimeEnd();
stephan5e8bb0a2022-09-20 08:27:57 +0000443 wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000444 return;
stephan132a87b2022-09-17 15:08:22 +0000445 }
stephanc4b87be2022-09-20 01:28:47 +0000446 const hFile = await hDir.getFileHandle(filenamePart, {create});
stephan07315542022-09-17 20:50:12 +0000447 /**
448 wa-sqlite, at this point, grabs a SyncAccessHandle and
stephan5f0b67c2022-10-03 11:33:35 +0000449 assigns it to the syncHandle prop of the file state
stephan07315542022-09-17 20:50:12 +0000450 object, but only for certain cases and it's unclear why it
451 places that limitation on it.
452 */
stephan5e8bb0a2022-09-20 08:27:57 +0000453 wTimeEnd();
stephan5f0b67c2022-10-03 11:33:35 +0000454 __openFiles[fid] = Object.assign(Object.create(null),{
stephan7ff8da82022-10-03 09:21:37 +0000455 filenameAbs: filename,
456 filenamePart: filenamePart,
457 dirHandle: hDir,
458 fileHandle: hFile,
459 sabView: state.sabFileBufView,
460 readOnly: create
461 ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags),
462 deleteOnClose: deleteOnClose
463 });
stephan07315542022-09-17 20:50:12 +0000464 storeAndNotify(opName, 0);
465 }catch(e){
stephan5e8bb0a2022-09-20 08:27:57 +0000466 wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000467 error(opName,e);
stephan56fae742022-09-24 10:12:19 +0000468 state.s11n.storeException(1,e);
stephan07315542022-09-17 20:50:12 +0000469 storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
470 }
stephanaec046a2022-09-19 18:22:29 +0000471 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000472 },
stephanf861b362022-10-25 08:06:17 +0000473 xRead: async function(fid/*sqlite3_file pointer*/,n,offset64){
stephanaec046a2022-09-19 18:22:29 +0000474 mTimeStart('xRead');
stephane72ddfd2022-10-14 15:52:29 +0000475 let rc = 0, nRead;
stephan5f0b67c2022-10-03 11:33:35 +0000476 const fh = __openFiles[fid];
stephan07315542022-09-17 20:50:12 +0000477 try{
stephan5e8bb0a2022-09-20 08:27:57 +0000478 wTimeStart('xRead');
stephane72ddfd2022-10-14 15:52:29 +0000479 nRead = (await getSyncHandle(fh)).read(
stephanaec046a2022-09-19 18:22:29 +0000480 fh.sabView.subarray(0, n),
stephanf861b362022-10-25 08:06:17 +0000481 {at: Number(offset64)}
stephanaec046a2022-09-19 18:22:29 +0000482 );
stephan5e8bb0a2022-09-20 08:27:57 +0000483 wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000484 if(nRead < n){/* Zero-fill remaining bytes */
stephanaec046a2022-09-19 18:22:29 +0000485 fh.sabView.fill(0, nRead, n);
stephan07315542022-09-17 20:50:12 +0000486 rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
487 }
488 }catch(e){
stephane72ddfd2022-10-14 15:52:29 +0000489 if(undefined===nRead) wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000490 error("xRead() failed",e,fh);
stephan56fae742022-09-24 10:12:19 +0000491 state.s11n.storeException(1,e);
stephand0010602022-10-30 11:22:45 +0000492 rc = (e instanceof GetSyncHandleError)
493 ? state.sq3Codes.SQLITE_IOERR_LOCK
494 : state.sq3Codes.SQLITE_IOERR_READ;
stephan07315542022-09-17 20:50:12 +0000495 }
496 storeAndNotify('xRead',rc);
stephanaec046a2022-09-19 18:22:29 +0000497 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000498 },
stephanf861b362022-10-25 08:06:17 +0000499 xSync: async function(fid/*sqlite3_file pointer*/,flags/*ignored*/){
stephanaec046a2022-09-19 18:22:29 +0000500 mTimeStart('xSync');
stephan07315542022-09-17 20:50:12 +0000501 const fh = __openFiles[fid];
stephan72ab4002022-09-21 12:27:35 +0000502 let rc = 0;
stephan5f0b67c2022-10-03 11:33:35 +0000503 if(!fh.readOnly && fh.syncHandle){
stephan72ab4002022-09-21 12:27:35 +0000504 try {
505 wTimeStart('xSync');
stephan5f0b67c2022-10-03 11:33:35 +0000506 await fh.syncHandle.flush();
stephan72ab4002022-09-21 12:27:35 +0000507 }catch(e){
stephan56fae742022-09-24 10:12:19 +0000508 state.s11n.storeException(2,e);
stephand0010602022-10-30 11:22:45 +0000509 rc = state.sq3Codes.SQLITE_IOERR_FSYNC;
stephan72ab4002022-09-21 12:27:35 +0000510 }
stephane72ddfd2022-10-14 15:52:29 +0000511 wTimeEnd();
stephan5e8bb0a2022-09-20 08:27:57 +0000512 }
stephan72ab4002022-09-21 12:27:35 +0000513 storeAndNotify('xSync',rc);
stephanaec046a2022-09-19 18:22:29 +0000514 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000515 },
stephanf861b362022-10-25 08:06:17 +0000516 xTruncate: async function(fid/*sqlite3_file pointer*/,size){
stephanaec046a2022-09-19 18:22:29 +0000517 mTimeStart('xTruncate');
stephan07315542022-09-17 20:50:12 +0000518 let rc = 0;
519 const fh = __openFiles[fid];
stephan5e8bb0a2022-09-20 08:27:57 +0000520 wTimeStart('xTruncate');
stephan07315542022-09-17 20:50:12 +0000521 try{
522 affirmNotRO('xTruncate', fh);
stephan7ff8da82022-10-03 09:21:37 +0000523 await (await getSyncHandle(fh)).truncate(size);
stephan07315542022-09-17 20:50:12 +0000524 }catch(e){
525 error("xTruncate():",e,fh);
stephan56fae742022-09-24 10:12:19 +0000526 state.s11n.storeException(2,e);
stephand0010602022-10-30 11:22:45 +0000527 rc = (e instanceof GetSyncHandleError)
528 ? state.sq3Codes.SQLITE_IOERR_LOCK
529 : state.sq3Codes.SQLITE_IOERR_TRUNCATE;
stephan07315542022-09-17 20:50:12 +0000530 }
stephan5e8bb0a2022-09-20 08:27:57 +0000531 wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000532 storeAndNotify('xTruncate',rc);
stephanaec046a2022-09-19 18:22:29 +0000533 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000534 },
stephanf861b362022-10-25 08:06:17 +0000535 xUnlock: async function(fid/*sqlite3_file pointer*/,
536 lockType/*SQLITE_LOCK_...*/){
stephan9a557732022-10-04 17:06:51 +0000537 mTimeStart('xUnlock');
538 let rc = 0;
539 const fh = __openFiles[fid];
540 if( state.sq3Codes.SQLITE_LOCK_NONE===lockType
541 && fh.syncHandle ){
stephane72ddfd2022-10-14 15:52:29 +0000542 wTimeStart('xUnlock');
stephan9a557732022-10-04 17:06:51 +0000543 try { await closeSyncHandle(fh) }
544 catch(e){
545 state.s11n.storeException(1,e);
stephand0010602022-10-30 11:22:45 +0000546 rc = state.sq3Codes.SQLITE_IOERR_UNLOCK;
stephan9a557732022-10-04 17:06:51 +0000547 }
stephane72ddfd2022-10-14 15:52:29 +0000548 wTimeEnd();
stephan9a557732022-10-04 17:06:51 +0000549 }
550 storeAndNotify('xUnlock',rc);
551 mTimeEnd();
552 },
stephanf861b362022-10-25 08:06:17 +0000553 xWrite: async function(fid/*sqlite3_file pointer*/,n,offset64){
stephanaec046a2022-09-19 18:22:29 +0000554 mTimeStart('xWrite');
stephan07315542022-09-17 20:50:12 +0000555 let rc;
stephan5e8bb0a2022-09-20 08:27:57 +0000556 wTimeStart('xWrite');
stephan07315542022-09-17 20:50:12 +0000557 try{
stephanaec046a2022-09-19 18:22:29 +0000558 const fh = __openFiles[fid];
stephan07315542022-09-17 20:50:12 +0000559 affirmNotRO('xWrite', fh);
stephanaec046a2022-09-19 18:22:29 +0000560 rc = (
stephan7ff8da82022-10-03 09:21:37 +0000561 n === (await getSyncHandle(fh))
562 .write(fh.sabView.subarray(0, n),
stephanf861b362022-10-25 08:06:17 +0000563 {at: Number(offset64)})
stephanaec046a2022-09-19 18:22:29 +0000564 ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
stephan07315542022-09-17 20:50:12 +0000565 }catch(e){
566 error("xWrite():",e,fh);
stephan56fae742022-09-24 10:12:19 +0000567 state.s11n.storeException(1,e);
stephand0010602022-10-30 11:22:45 +0000568 rc = (e instanceof GetSyncHandleError)
569 ? state.sq3Codes.SQLITE_IOERR_LOCK
570 : state.sq3Codes.SQLITE_IOERR_WRITE;
stephan07315542022-09-17 20:50:12 +0000571 }
stephane72ddfd2022-10-14 15:52:29 +0000572 wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000573 storeAndNotify('xWrite',rc);
stephanaec046a2022-09-19 18:22:29 +0000574 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000575 }
stephan9a557732022-10-04 17:06:51 +0000576}/*vfsAsyncImpls*/;
stephan07315542022-09-17 20:50:12 +0000577
stephan138647a2022-09-20 03:31:02 +0000578const initS11n = ()=>{
stephanb8c8d4e2022-09-20 13:25:39 +0000579 /**
stephan72ab4002022-09-21 12:27:35 +0000580 ACHTUNG: this code is 100% duplicated in the other half of this
581 proxy! The documentation is maintained in the "synchronous half".
stephanb8c8d4e2022-09-20 13:25:39 +0000582 */
stephan138647a2022-09-20 03:31:02 +0000583 if(state.s11n) return state.s11n;
stephanb8c8d4e2022-09-20 13:25:39 +0000584 const textDecoder = new TextDecoder(),
585 textEncoder = new TextEncoder('utf-8'),
586 viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize),
587 viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
stephan138647a2022-09-20 03:31:02 +0000588 state.s11n = Object.create(null);
stephanb8c8d4e2022-09-20 13:25:39 +0000589 const TypeIds = Object.create(null);
590 TypeIds.number = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' };
591 TypeIds.bigint = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' };
592 TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' };
593 TypeIds.string = { id: 4 };
stephan72ab4002022-09-21 12:27:35 +0000594 const getTypeId = (v)=>(
595 TypeIds[typeof v]
596 || toss("Maintenance required: this value type cannot be serialized.",v)
597 );
stephanb8c8d4e2022-09-20 13:25:39 +0000598 const getTypeIdById = (tid)=>{
599 switch(tid){
stephan72ab4002022-09-21 12:27:35 +0000600 case TypeIds.number.id: return TypeIds.number;
601 case TypeIds.bigint.id: return TypeIds.bigint;
602 case TypeIds.boolean.id: return TypeIds.boolean;
603 case TypeIds.string.id: return TypeIds.string;
604 default: toss("Invalid type ID:",tid);
stephanb8c8d4e2022-09-20 13:25:39 +0000605 }
606 };
stephan138647a2022-09-20 03:31:02 +0000607 state.s11n.deserialize = function(){
stephanb8c8d4e2022-09-20 13:25:39 +0000608 ++metrics.s11n.deserialize.count;
609 const t = performance.now();
stephanb8c8d4e2022-09-20 13:25:39 +0000610 const argc = viewU8[0];
stephan72ab4002022-09-21 12:27:35 +0000611 const rc = argc ? [] : null;
stephanb8c8d4e2022-09-20 13:25:39 +0000612 if(argc){
stephan72ab4002022-09-21 12:27:35 +0000613 const typeIds = [];
614 let offset = 1, i, n, v;
stephanb8c8d4e2022-09-20 13:25:39 +0000615 for(i = 0; i < argc; ++i, ++offset){
616 typeIds.push(getTypeIdById(viewU8[offset]));
617 }
618 for(i = 0; i < argc; ++i){
619 const t = typeIds[i];
620 if(t.getter){
621 v = viewDV[t.getter](offset, state.littleEndian);
622 offset += t.size;
stephan72ab4002022-09-21 12:27:35 +0000623 }else{/*String*/
stephanb8c8d4e2022-09-20 13:25:39 +0000624 n = viewDV.getInt32(offset, state.littleEndian);
625 offset += 4;
626 v = textDecoder.decode(viewU8.slice(offset, offset+n));
627 offset += n;
628 }
629 rc.push(v);
630 }
631 }
632 //log("deserialize:",argc, rc);
633 metrics.s11n.deserialize.time += performance.now() - t;
634 return rc;
635 };
stephan138647a2022-09-20 03:31:02 +0000636 state.s11n.serialize = function(...args){
stephanb8c8d4e2022-09-20 13:25:39 +0000637 const t = performance.now();
stephan72ab4002022-09-21 12:27:35 +0000638 ++metrics.s11n.serialize.count;
stephan5e8bb0a2022-09-20 08:27:57 +0000639 if(args.length){
stephanb8c8d4e2022-09-20 13:25:39 +0000640 //log("serialize():",args);
stephan72ab4002022-09-21 12:27:35 +0000641 const typeIds = [];
642 let i = 0, offset = 1;
643 viewU8[0] = args.length & 0xff /* header = # of args */;
stephanb8c8d4e2022-09-20 13:25:39 +0000644 for(; i < args.length; ++i, ++offset){
stephan72ab4002022-09-21 12:27:35 +0000645 /* Write the TypeIds.id value into the next args.length
646 bytes. */
stephanb8c8d4e2022-09-20 13:25:39 +0000647 typeIds.push(getTypeId(args[i]));
648 viewU8[offset] = typeIds[i].id;
649 }
650 for(i = 0; i < args.length; ++i) {
stephan72ab4002022-09-21 12:27:35 +0000651 /* Deserialize the following bytes based on their
652 corresponding TypeIds.id from the header. */
stephanb8c8d4e2022-09-20 13:25:39 +0000653 const t = typeIds[i];
654 if(t.setter){
655 viewDV[t.setter](offset, args[i], state.littleEndian);
656 offset += t.size;
stephan72ab4002022-09-21 12:27:35 +0000657 }else{/*String*/
stephanb8c8d4e2022-09-20 13:25:39 +0000658 const s = textEncoder.encode(args[i]);
659 viewDV.setInt32(offset, s.byteLength, state.littleEndian);
660 offset += 4;
661 viewU8.set(s, offset);
662 offset += s.byteLength;
663 }
664 }
665 //log("serialize() result:",viewU8.slice(0,offset));
stephan5e8bb0a2022-09-20 08:27:57 +0000666 }else{
stephanb8c8d4e2022-09-20 13:25:39 +0000667 viewU8[0] = 0;
stephan5e8bb0a2022-09-20 08:27:57 +0000668 }
stephanb8c8d4e2022-09-20 13:25:39 +0000669 metrics.s11n.serialize.time += performance.now() - t;
stephan138647a2022-09-20 03:31:02 +0000670 };
stephane8afca32022-09-21 14:02:47 +0000671
672 state.s11n.storeException = state.asyncS11nExceptions
stephan56fae742022-09-24 10:12:19 +0000673 ? ((priority,e)=>{
674 if(priority<=state.asyncS11nExceptions){
stephand0010602022-10-30 11:22:45 +0000675 state.s11n.serialize([e.name,': ',e.message].join(''));
stephan56fae742022-09-24 10:12:19 +0000676 }
677 })
stephane8afca32022-09-21 14:02:47 +0000678 : ()=>{};
679
stephan138647a2022-09-20 03:31:02 +0000680 return state.s11n;
stephanb8c8d4e2022-09-20 13:25:39 +0000681}/*initS11n()*/;
stephan138647a2022-09-20 03:31:02 +0000682
stephan5e8bb0a2022-09-20 08:27:57 +0000683const waitLoop = async function f(){
stephan138647a2022-09-20 03:31:02 +0000684 const opHandlers = Object.create(null);
stephanc9e26022022-09-20 10:11:52 +0000685 for(let k of Object.keys(state.opIds)){
686 const vi = vfsAsyncImpls[k];
687 if(!vi) continue;
stephan138647a2022-09-20 03:31:02 +0000688 const o = Object.create(null);
689 opHandlers[state.opIds[k]] = o;
690 o.key = k;
stephane72ddfd2022-10-14 15:52:29 +0000691 o.f = vi;
stephan138647a2022-09-20 03:31:02 +0000692 }
stephan5f0b67c2022-10-03 11:33:35 +0000693 /**
694 waitTime is how long (ms) to wait for each Atomics.wait().
stephanc7fb48d2022-10-04 09:12:05 +0000695 We need to wake up periodically to give the thread a chance
stephan5f0b67c2022-10-03 11:33:35 +0000696 to do other things.
697 */
stephan9a557732022-10-04 17:06:51 +0000698 const waitTime = 1000;
stephan3c272ba2022-10-04 00:54:00 +0000699 while(!flagAsyncShutdown){
stephan138647a2022-09-20 03:31:02 +0000700 try {
stephan5f0b67c2022-10-03 11:33:35 +0000701 if('timed-out'===Atomics.wait(
702 state.sabOPView, state.opIds.whichOp, 0, waitTime
703 )){
stephan5e8bb0a2022-09-20 08:27:57 +0000704 continue;
705 }
706 const opId = Atomics.load(state.sabOPView, state.opIds.whichOp);
707 Atomics.store(state.sabOPView, state.opIds.whichOp, 0);
stephan138647a2022-09-20 03:31:02 +0000708 const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId);
stephan56fae742022-09-24 10:12:19 +0000709 const args = state.s11n.deserialize() || [];
stephan5f0b67c2022-10-03 11:33:35 +0000710 state.s11n.serialize(/* clear s11n to keep the caller from
711 confusing this with an exception string
712 written by the upcoming operation */);
stephan5e8bb0a2022-09-20 08:27:57 +0000713 //warn("waitLoop() whichOp =",opId, hnd, args);
714 if(hnd.f) await hnd.f(...args);
715 else error("Missing callback for opId",opId);
stephan138647a2022-09-20 03:31:02 +0000716 }catch(e){
stephan5f0b67c2022-10-03 11:33:35 +0000717 error('in waitLoop():',e);
stephan138647a2022-09-20 03:31:02 +0000718 }
stephane72ddfd2022-10-14 15:52:29 +0000719 }
stephan138647a2022-09-20 03:31:02 +0000720};
721
stephan07315542022-09-17 20:50:12 +0000722navigator.storage.getDirectory().then(function(d){
723 const wMsg = (type)=>postMessage({type});
724 state.rootDir = d;
stephan5e8bb0a2022-09-20 08:27:57 +0000725 self.onmessage = function({data}){
stephan07315542022-09-17 20:50:12 +0000726 switch(data.type){
stephan138647a2022-09-20 03:31:02 +0000727 case 'opfs-async-init':{
stephan07315542022-09-17 20:50:12 +0000728 /* Receive shared state from synchronous partner */
stephan138647a2022-09-20 03:31:02 +0000729 const opt = data.args;
730 state.littleEndian = opt.littleEndian;
stephane8afca32022-09-21 14:02:47 +0000731 state.asyncS11nExceptions = opt.asyncS11nExceptions;
stephan07315542022-09-17 20:50:12 +0000732 state.verbose = opt.verbose ?? 2;
733 state.fileBufferSize = opt.fileBufferSize;
stephan138647a2022-09-20 03:31:02 +0000734 state.sabS11nOffset = opt.sabS11nOffset;
735 state.sabS11nSize = opt.sabS11nSize;
stephanc4b87be2022-09-20 01:28:47 +0000736 state.sabOP = opt.sabOP;
737 state.sabOPView = new Int32Array(state.sabOP);
738 state.sabIO = opt.sabIO;
739 state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
stephan138647a2022-09-20 03:31:02 +0000740 state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
stephan07315542022-09-17 20:50:12 +0000741 state.opIds = opt.opIds;
742 state.sq3Codes = opt.sq3Codes;
743 Object.keys(vfsAsyncImpls).forEach((k)=>{
744 if(!Number.isFinite(state.opIds[k])){
745 toss("Maintenance required: missing state.opIds[",k,"]");
746 }
747 });
stephan138647a2022-09-20 03:31:02 +0000748 initS11n();
stephanaec046a2022-09-19 18:22:29 +0000749 metrics.reset();
stephan07315542022-09-17 20:50:12 +0000750 log("init state",state);
stephan138647a2022-09-20 03:31:02 +0000751 wMsg('opfs-async-inited');
stephan5e8bb0a2022-09-20 08:27:57 +0000752 waitLoop();
stephan07315542022-09-17 20:50:12 +0000753 break;
754 }
stephan3c272ba2022-10-04 00:54:00 +0000755 case 'opfs-async-restart':
756 if(flagAsyncShutdown){
757 warn("Restarting after opfs-async-shutdown. Might or might not work.");
758 flagAsyncShutdown = false;
759 waitLoop();
760 }
761 break;
762 case 'opfs-async-metrics':
763 metrics.dump();
764 break;
stephan132a87b2022-09-17 15:08:22 +0000765 }
766 };
stephan138647a2022-09-20 03:31:02 +0000767 wMsg('opfs-async-loaded');
stephan9a557732022-10-04 17:06:51 +0000768}).catch((e)=>error("error initializing OPFS asyncer:",e));