blob: d261dd354c5c52525c55cfe9a09e69ca1da58898 [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
stephane6f8a092022-09-17 21:13:26 +000013 An INCOMPLETE and UNDER CONSTRUCTION experiment for OPFS: a Worker
14 which manages asynchronous OPFS handles on behalf of a synchronous
15 API which controls it via a combination of Worker messages,
16 SharedArrayBuffer, and Atomics.
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.
stephan132a87b2022-09-17 15:08:22 +000028*/
29'use strict';
stephan07315542022-09-17 20:50:12 +000030const toss = function(...args){throw new Error(args.join(' '))};
31if(self.window === self){
32 toss("This code cannot run from the main thread.",
33 "Load it as a Worker from a separate Worker.");
34}else if(!navigator.storage.getDirectory){
35 toss("This API requires navigator.storage.getDirectory.");
36}
stephan9a557732022-10-04 17:06:51 +000037
stephan07315542022-09-17 20:50:12 +000038/**
39 Will hold state copied to this object from the syncronous side of
40 this API.
41*/
42const state = Object.create(null);
43/**
44 verbose:
45
46 0 = no logging output
47 1 = only errors
48 2 = warnings and errors
49 3 = debug, warnings, and errors
50*/
51state.verbose = 2;
52
stephan509f4052022-09-19 09:58:01 +000053const loggers = {
54 0:console.error.bind(console),
55 1:console.warn.bind(console),
56 2:console.log.bind(console)
stephan07315542022-09-17 20:50:12 +000057};
stephan509f4052022-09-19 09:58:01 +000058const logImpl = (level,...args)=>{
59 if(state.verbose>level) loggers[level]("OPFS asyncer:",...args);
stephan07315542022-09-17 20:50:12 +000060};
stephan509f4052022-09-19 09:58:01 +000061const log = (...args)=>logImpl(2, ...args);
62const warn = (...args)=>logImpl(1, ...args);
63const error = (...args)=>logImpl(0, ...args);
stephanf8150112022-09-19 17:09:09 +000064const metrics = Object.create(null);
stephanaec046a2022-09-19 18:22:29 +000065metrics.reset = ()=>{
66 let k;
stephan5e8bb0a2022-09-20 08:27:57 +000067 const r = (m)=>(m.count = m.time = m.wait = 0);
stephanaec046a2022-09-19 18:22:29 +000068 for(k in state.opIds){
69 r(metrics[k] = Object.create(null));
70 }
stephanb8c8d4e2022-09-20 13:25:39 +000071 let s = metrics.s11n = Object.create(null);
72 s = s.serialize = Object.create(null);
73 s.count = s.time = 0;
74 s = metrics.s11n.deserialize = Object.create(null);
75 s.count = s.time = 0;
stephanaec046a2022-09-19 18:22:29 +000076};
77metrics.dump = ()=>{
78 let k, n = 0, t = 0, w = 0;
79 for(k in state.opIds){
80 const m = metrics[k];
81 n += m.count;
82 t += m.time;
stephan5e8bb0a2022-09-20 08:27:57 +000083 w += m.wait;
stephanaec046a2022-09-19 18:22:29 +000084 m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
85 }
86 console.log(self.location.href,
stephan5e8bb0a2022-09-20 08:27:57 +000087 "metrics for",self.location.href,":\n",
stephan56fae742022-09-24 10:12:19 +000088 metrics,
stephan5e8bb0a2022-09-20 08:27:57 +000089 "\nTotal of",n,"op(s) for",t,"ms",
90 "approx",w,"ms spent waiting on OPFS APIs.");
stephan56fae742022-09-24 10:12:19 +000091 console.log("Serialization metrics:",metrics.s11n);
stephanaec046a2022-09-19 18:22:29 +000092};
stephan07315542022-09-17 20:50:12 +000093
stephan07315542022-09-17 20:50:12 +000094/**
95 Map of sqlite3_file pointers (integers) to metadata related to a
96 given OPFS file handles. The pointers are, in this side of the
97 interface, opaque file handle IDs provided by the synchronous
98 part of this constellation. Each value is an object with a structure
99 demonstrated in the xOpen() impl.
100*/
101const __openFiles = Object.create(null);
102
103/**
stephan8200a6d2022-09-17 23:29:27 +0000104 Expects an OPFS file path. It gets resolved, such that ".."
105 components are properly expanded, and returned. If the 2nd
106 are is true, it's returned as an array of path elements,
107 else it's returned as an absolute path string.
stephan07315542022-09-17 20:50:12 +0000108*/
stephan8200a6d2022-09-17 23:29:27 +0000109const getResolvedPath = function(filename,splitIt){
110 const p = new URL(
111 filename, 'file://irrelevant'
112 ).pathname;
113 return splitIt ? p.split('/').filter((v)=>!!v) : p;
stephan509f4052022-09-19 09:58:01 +0000114};
stephan07315542022-09-17 20:50:12 +0000115
116/**
117 Takes the absolute path to a filesystem element. Returns an array
118 of [handleOfContainingDir, filename]. If the 2nd argument is
119 truthy then each directory element leading to the file is created
120 along the way. Throws if any creation or resolution fails.
121*/
stephan56fae742022-09-24 10:12:19 +0000122const getDirForFilename = async function f(absFilename, createDirs = false){
stephan8200a6d2022-09-17 23:29:27 +0000123 const path = getResolvedPath(absFilename, true);
stephan07315542022-09-17 20:50:12 +0000124 const filename = path.pop();
stephan8200a6d2022-09-17 23:29:27 +0000125 let dh = state.rootDir;
126 for(const dirName of path){
127 if(dirName){
128 dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs});
stephan07315542022-09-17 20:50:12 +0000129 }
stephan132a87b2022-09-17 15:08:22 +0000130 }
stephan07315542022-09-17 20:50:12 +0000131 return [dh, filename];
132};
stephan132a87b2022-09-17 15:08:22 +0000133
stephan7ff8da82022-10-03 09:21:37 +0000134/**
135 Returns the sync access handle associated with the given file
136 handle object (which must be a valid handle object), lazily opening
stephan9a557732022-10-04 17:06:51 +0000137 it if needed.
stephanc7fb48d2022-10-04 09:12:05 +0000138
139 In order to help alleviate cross-tab contention for a dabase,
140 if an exception is thrown while acquiring the handle, this routine
141 will wait briefly and try again, up to 3 times. If acquisition
142 still fails at that point it will give up and propagate the
143 exception.
stephan7ff8da82022-10-03 09:21:37 +0000144*/
stephan5f0b67c2022-10-03 11:33:35 +0000145const getSyncHandle = async (fh)=>{
146 if(!fh.syncHandle){
stephanc7fb48d2022-10-04 09:12:05 +0000147 const t = performance.now();
148 log("Acquiring sync handle for",fh.filenameAbs);
149 const maxTries = 3;
150 let i = 1, ms = 300;
151 for(; true; ms *= ++i){
152 try {
153 //if(1===i) toss("Just testing.");
154 //TODO? A config option which tells it to throw here
stephan3e771c02022-10-14 13:26:18 +0000155 //randomly every now and then, for testing purposes.
stephanc7fb48d2022-10-04 09:12:05 +0000156 fh.syncHandle = await fh.fileHandle.createSyncAccessHandle();
157 break;
158 }catch(e){
159 if(i === maxTries){
160 toss("Error getting sync handle.",maxTries,
161 "attempts failed. ",fh.filenameAbs, ":", e.message);
162 throw e;
163 }
stephan3e771c02022-10-14 13:26:18 +0000164 warn("Error getting sync handle. Waiting",ms,
stephanc7fb48d2022-10-04 09:12:05 +0000165 "ms and trying again.",fh.filenameAbs,e);
166 Atomics.wait(state.sabOPView, state.opIds.xSleep, 0, ms);
167 }
168 }
169 log("Got sync handle for",fh.filenameAbs,'in',performance.now() - t,'ms');
stephan5f0b67c2022-10-03 11:33:35 +0000170 }
stephan5f0b67c2022-10-03 11:33:35 +0000171 return fh.syncHandle;
172};
173
174const closeSyncHandle = async (fh)=>{
175 if(fh.syncHandle){
stephan9a557732022-10-04 17:06:51 +0000176 log("Closing sync handle for",fh.filenameAbs);
stephan5f0b67c2022-10-03 11:33:35 +0000177 const h = fh.syncHandle;
178 delete fh.syncHandle;
179 return h.close();
180 }
181};
stephan132a87b2022-09-17 15:08:22 +0000182
stephan07315542022-09-17 20:50:12 +0000183/**
stephan72ab4002022-09-21 12:27:35 +0000184 Stores the given value at state.sabOPView[state.opIds.rc] and then
185 Atomics.notify()'s it.
stephan07315542022-09-17 20:50:12 +0000186*/
187const storeAndNotify = (opName, value)=>{
stephanc9e26022022-09-20 10:11:52 +0000188 log(opName+"() => notify(",state.opIds.rc,",",value,")");
189 Atomics.store(state.sabOPView, state.opIds.rc, value);
190 Atomics.notify(state.sabOPView, state.opIds.rc);
stephan07315542022-09-17 20:50:12 +0000191};
stephan132a87b2022-09-17 15:08:22 +0000192
stephan07315542022-09-17 20:50:12 +0000193/**
194 Throws if fh is a file-holding object which is flagged as read-only.
195*/
196const affirmNotRO = function(opName,fh){
197 if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs);
198};
199
stephanaec046a2022-09-19 18:22:29 +0000200
201const opTimer = Object.create(null);
202opTimer.op = undefined;
203opTimer.start = undefined;
204const mTimeStart = (op)=>{
205 opTimer.start = performance.now();
206 opTimer.op = op;
207 //metrics[op] || toss("Maintenance required: missing metrics for",op);
208 ++metrics[op].count;
209};
210const mTimeEnd = ()=>(
211 metrics[opTimer.op].time += performance.now() - opTimer.start
212);
stephan5e8bb0a2022-09-20 08:27:57 +0000213const waitTimer = Object.create(null);
214waitTimer.op = undefined;
215waitTimer.start = undefined;
216const wTimeStart = (op)=>{
217 waitTimer.start = performance.now();
218 waitTimer.op = op;
219 //metrics[op] || toss("Maintenance required: missing metrics for",op);
220};
221const wTimeEnd = ()=>(
222 metrics[waitTimer.op].wait += performance.now() - waitTimer.start
223);
stephanaec046a2022-09-19 18:22:29 +0000224
stephan07315542022-09-17 20:50:12 +0000225/**
stephan3c272ba2022-10-04 00:54:00 +0000226 Set to true by the 'opfs-async-shutdown' command to quite the wait loop.
227 This is only intended for debugging purposes: we cannot inspect this
228 file's state while the tight waitLoop() is running.
229*/
230let flagAsyncShutdown = false;
231
stephan9a557732022-10-04 17:06:51 +0000232
stephan3c272ba2022-10-04 00:54:00 +0000233/**
stephan07315542022-09-17 20:50:12 +0000234 Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods
235 methods. Maintenance reminder: members are in alphabetical order
236 to simplify finding them.
237*/
238const vfsAsyncImpls = {
stephan3c272ba2022-10-04 00:54:00 +0000239 'opfs-async-metrics': async ()=>{
240 mTimeStart('opfs-async-metrics');
stephan56fae742022-09-24 10:12:19 +0000241 metrics.dump();
stephan3c272ba2022-10-04 00:54:00 +0000242 storeAndNotify('opfs-async-metrics', 0);
stephan56fae742022-09-24 10:12:19 +0000243 mTimeEnd();
244 },
stephan3c272ba2022-10-04 00:54:00 +0000245 'opfs-async-shutdown': async ()=>{
246 flagAsyncShutdown = true;
247 storeAndNotify('opfs-async-shutdown', 0);
248 },
stephan56fae742022-09-24 10:12:19 +0000249 mkdir: async (dirname)=>{
stephan5e8bb0a2022-09-20 08:27:57 +0000250 mTimeStart('mkdir');
stephanaec046a2022-09-19 18:22:29 +0000251 let rc = 0;
stephan5e8bb0a2022-09-20 08:27:57 +0000252 wTimeStart('mkdir');
stephanaec046a2022-09-19 18:22:29 +0000253 try {
stephan56fae742022-09-24 10:12:19 +0000254 await getDirForFilename(dirname+"/filepart", true);
stephanaec046a2022-09-19 18:22:29 +0000255 }catch(e){
stephan56fae742022-09-24 10:12:19 +0000256 state.s11n.storeException(2,e);
stephanaec046a2022-09-19 18:22:29 +0000257 rc = state.sq3Codes.SQLITE_IOERR;
stephan72ab4002022-09-21 12:27:35 +0000258 }finally{
259 wTimeEnd();
stephanaec046a2022-09-19 18:22:29 +0000260 }
261 storeAndNotify('mkdir', rc);
stephan5e8bb0a2022-09-20 08:27:57 +0000262 mTimeEnd();
stephanaec046a2022-09-19 18:22:29 +0000263 },
stephan56fae742022-09-24 10:12:19 +0000264 xAccess: async (filename)=>{
stephanaec046a2022-09-19 18:22:29 +0000265 mTimeStart('xAccess');
stephan8200a6d2022-09-17 23:29:27 +0000266 /* OPFS cannot support the full range of xAccess() queries sqlite3
267 calls for. We can essentially just tell if the file is
268 accessible, but if it is it's automatically writable (unless
269 it's locked, which we cannot(?) know without trying to open
270 it). OPFS does not have the notion of read-only.
271
272 The return semantics of this function differ from sqlite3's
273 xAccess semantics because we are limited in what we can
274 communicate back to our synchronous communication partner: 0 =
275 accessible, non-0 means not accessible.
276 */
277 let rc = 0;
stephan5e8bb0a2022-09-20 08:27:57 +0000278 wTimeStart('xAccess');
stephan8200a6d2022-09-17 23:29:27 +0000279 try{
stephan56fae742022-09-24 10:12:19 +0000280 const [dh, fn] = await getDirForFilename(filename);
stephan8200a6d2022-09-17 23:29:27 +0000281 await dh.getFileHandle(fn);
282 }catch(e){
stephan56fae742022-09-24 10:12:19 +0000283 state.s11n.storeException(2,e);
stephan8200a6d2022-09-17 23:29:27 +0000284 rc = state.sq3Codes.SQLITE_IOERR;
stephan72ab4002022-09-21 12:27:35 +0000285 }finally{
286 wTimeEnd();
stephan8200a6d2022-09-17 23:29:27 +0000287 }
stephan07315542022-09-17 20:50:12 +0000288 storeAndNotify('xAccess', rc);
stephanaec046a2022-09-19 18:22:29 +0000289 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000290 },
291 xClose: async function(fid){
292 const opName = 'xClose';
stephanaec046a2022-09-19 18:22:29 +0000293 mTimeStart(opName);
stephan07315542022-09-17 20:50:12 +0000294 const fh = __openFiles[fid];
stephan5e8bb0a2022-09-20 08:27:57 +0000295 let rc = 0;
296 wTimeStart('xClose');
stephan07315542022-09-17 20:50:12 +0000297 if(fh){
298 delete __openFiles[fid];
stephan5f0b67c2022-10-03 11:33:35 +0000299 await closeSyncHandle(fh);
stephan07315542022-09-17 20:50:12 +0000300 if(fh.deleteOnClose){
301 try{ await fh.dirHandle.removeEntry(fh.filenamePart) }
302 catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) }
stephan132a87b2022-09-17 15:08:22 +0000303 }
stephan07315542022-09-17 20:50:12 +0000304 }else{
stephan72ab4002022-09-21 12:27:35 +0000305 state.s11n.serialize();
stephan5e8bb0a2022-09-20 08:27:57 +0000306 rc = state.sq3Codes.SQLITE_NOTFOUND;
stephan132a87b2022-09-17 15:08:22 +0000307 }
stephan5e8bb0a2022-09-20 08:27:57 +0000308 wTimeEnd();
309 storeAndNotify(opName, rc);
stephanaec046a2022-09-19 18:22:29 +0000310 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000311 },
stephanc4b87be2022-09-20 01:28:47 +0000312 xDelete: async function(...args){
313 mTimeStart('xDelete');
314 const rc = await vfsAsyncImpls.xDeleteNoWait(...args);
315 storeAndNotify('xDelete', rc);
316 mTimeEnd();
317 },
stephan138647a2022-09-20 03:31:02 +0000318 xDeleteNoWait: async function(filename, syncDir = 0, recursive = false){
stephan8200a6d2022-09-17 23:29:27 +0000319 /* The syncDir flag is, for purposes of the VFS API's semantics,
320 ignored here. However, if it has the value 0x1234 then: after
321 deleting the given file, recursively try to delete any empty
322 directories left behind in its wake (ignoring any errors and
323 stopping at the first failure).
324
325 That said: we don't know for sure that removeEntry() fails if
326 the dir is not empty because the API is not documented. It has,
327 however, a "recursive" flag which defaults to false, so
328 presumably it will fail if the dir is not empty and that flag
329 is false.
330 */
stephanf3860122022-09-18 17:32:35 +0000331 let rc = 0;
stephan5e8bb0a2022-09-20 08:27:57 +0000332 wTimeStart('xDelete');
stephan07315542022-09-17 20:50:12 +0000333 try {
stephan8200a6d2022-09-17 23:29:27 +0000334 while(filename){
stephan56fae742022-09-24 10:12:19 +0000335 const [hDir, filenamePart] = await getDirForFilename(filename, false);
stephan8200a6d2022-09-17 23:29:27 +0000336 if(!filenamePart) break;
stephanf3860122022-09-18 17:32:35 +0000337 await hDir.removeEntry(filenamePart, {recursive});
stephan8200a6d2022-09-17 23:29:27 +0000338 if(0x1234 !== syncDir) break;
339 filename = getResolvedPath(filename, true);
340 filename.pop();
341 filename = filename.join('/');
342 }
stephan07315542022-09-17 20:50:12 +0000343 }catch(e){
stephan56fae742022-09-24 10:12:19 +0000344 state.s11n.storeException(2,e);
stephanf3860122022-09-18 17:32:35 +0000345 rc = state.sq3Codes.SQLITE_IOERR_DELETE;
stephan132a87b2022-09-17 15:08:22 +0000346 }
stephan5e8bb0a2022-09-20 08:27:57 +0000347 wTimeEnd();
stephanf3860122022-09-18 17:32:35 +0000348 return rc;
349 },
stephan07315542022-09-17 20:50:12 +0000350 xFileSize: async function(fid){
stephanaec046a2022-09-19 18:22:29 +0000351 mTimeStart('xFileSize');
stephan07315542022-09-17 20:50:12 +0000352 const fh = __openFiles[fid];
353 let sz;
stephan5e8bb0a2022-09-20 08:27:57 +0000354 wTimeStart('xFileSize');
stephan07315542022-09-17 20:50:12 +0000355 try{
stephan7ff8da82022-10-03 09:21:37 +0000356 sz = await (await getSyncHandle(fh)).getSize();
stephan138647a2022-09-20 03:31:02 +0000357 state.s11n.serialize(Number(sz));
stephan07315542022-09-17 20:50:12 +0000358 sz = 0;
359 }catch(e){
stephan56fae742022-09-24 10:12:19 +0000360 state.s11n.storeException(2,e);
stephan07315542022-09-17 20:50:12 +0000361 sz = state.sq3Codes.SQLITE_IOERR;
stephan132a87b2022-09-17 15:08:22 +0000362 }
stephan5e8bb0a2022-09-20 08:27:57 +0000363 wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000364 storeAndNotify('xFileSize', sz);
stephanaec046a2022-09-19 18:22:29 +0000365 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000366 },
stephan9a557732022-10-04 17:06:51 +0000367 xLock: async function(fid,lockType){
368 mTimeStart('xLock');
369 const fh = __openFiles[fid];
370 let rc = 0;
371 if( !fh.syncHandle ){
372 try { await getSyncHandle(fh) }
373 catch(e){
374 state.s11n.storeException(1,e);
375 rc = state.sq3Codes.SQLITE_IOERR;
376 }
377 }
378 storeAndNotify('xLock',rc);
379 mTimeEnd();
380 },
stephan138647a2022-09-20 03:31:02 +0000381 xOpen: async function(fid/*sqlite3_file pointer*/, filename, flags){
stephan07315542022-09-17 20:50:12 +0000382 const opName = 'xOpen';
stephanaec046a2022-09-19 18:22:29 +0000383 mTimeStart(opName);
stephanc4b87be2022-09-20 01:28:47 +0000384 const deleteOnClose = (state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags);
385 const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags);
stephan5e8bb0a2022-09-20 08:27:57 +0000386 wTimeStart('xOpen');
stephan07315542022-09-17 20:50:12 +0000387 try{
stephan07315542022-09-17 20:50:12 +0000388 let hDir, filenamePart;
389 try {
stephan56fae742022-09-24 10:12:19 +0000390 [hDir, filenamePart] = await getDirForFilename(filename, !!create);
stephan132a87b2022-09-17 15:08:22 +0000391 }catch(e){
stephan07315542022-09-17 20:50:12 +0000392 storeAndNotify(opName, state.sql3Codes.SQLITE_NOTFOUND);
stephanaec046a2022-09-19 18:22:29 +0000393 mTimeEnd();
stephan5e8bb0a2022-09-20 08:27:57 +0000394 wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000395 return;
stephan132a87b2022-09-17 15:08:22 +0000396 }
stephanc4b87be2022-09-20 01:28:47 +0000397 const hFile = await hDir.getFileHandle(filenamePart, {create});
stephan07315542022-09-17 20:50:12 +0000398 /**
399 wa-sqlite, at this point, grabs a SyncAccessHandle and
stephan5f0b67c2022-10-03 11:33:35 +0000400 assigns it to the syncHandle prop of the file state
stephan07315542022-09-17 20:50:12 +0000401 object, but only for certain cases and it's unclear why it
402 places that limitation on it.
403 */
stephan5e8bb0a2022-09-20 08:27:57 +0000404 wTimeEnd();
stephan5f0b67c2022-10-03 11:33:35 +0000405 __openFiles[fid] = Object.assign(Object.create(null),{
stephan7ff8da82022-10-03 09:21:37 +0000406 filenameAbs: filename,
407 filenamePart: filenamePart,
408 dirHandle: hDir,
409 fileHandle: hFile,
410 sabView: state.sabFileBufView,
411 readOnly: create
412 ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags),
413 deleteOnClose: deleteOnClose
414 });
stephan07315542022-09-17 20:50:12 +0000415 storeAndNotify(opName, 0);
416 }catch(e){
stephan5e8bb0a2022-09-20 08:27:57 +0000417 wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000418 error(opName,e);
stephan56fae742022-09-24 10:12:19 +0000419 state.s11n.storeException(1,e);
stephan07315542022-09-17 20:50:12 +0000420 storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
421 }
stephanaec046a2022-09-19 18:22:29 +0000422 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000423 },
stephan138647a2022-09-20 03:31:02 +0000424 xRead: async function(fid,n,offset){
stephanaec046a2022-09-19 18:22:29 +0000425 mTimeStart('xRead');
stephan07315542022-09-17 20:50:12 +0000426 let rc = 0;
stephan5f0b67c2022-10-03 11:33:35 +0000427 const fh = __openFiles[fid];
stephan07315542022-09-17 20:50:12 +0000428 try{
stephan5e8bb0a2022-09-20 08:27:57 +0000429 wTimeStart('xRead');
stephan7ff8da82022-10-03 09:21:37 +0000430 const nRead = (await getSyncHandle(fh)).read(
stephanaec046a2022-09-19 18:22:29 +0000431 fh.sabView.subarray(0, n),
432 {at: Number(offset)}
433 );
stephan5e8bb0a2022-09-20 08:27:57 +0000434 wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000435 if(nRead < n){/* Zero-fill remaining bytes */
stephanaec046a2022-09-19 18:22:29 +0000436 fh.sabView.fill(0, nRead, n);
stephan07315542022-09-17 20:50:12 +0000437 rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
438 }
439 }catch(e){
440 error("xRead() failed",e,fh);
stephan56fae742022-09-24 10:12:19 +0000441 state.s11n.storeException(1,e);
stephan07315542022-09-17 20:50:12 +0000442 rc = state.sq3Codes.SQLITE_IOERR_READ;
443 }
444 storeAndNotify('xRead',rc);
stephanaec046a2022-09-19 18:22:29 +0000445 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000446 },
stephan138647a2022-09-20 03:31:02 +0000447 xSync: async function(fid,flags/*ignored*/){
stephanaec046a2022-09-19 18:22:29 +0000448 mTimeStart('xSync');
stephan07315542022-09-17 20:50:12 +0000449 const fh = __openFiles[fid];
stephan72ab4002022-09-21 12:27:35 +0000450 let rc = 0;
stephan5f0b67c2022-10-03 11:33:35 +0000451 if(!fh.readOnly && fh.syncHandle){
stephan72ab4002022-09-21 12:27:35 +0000452 try {
453 wTimeStart('xSync');
stephan5f0b67c2022-10-03 11:33:35 +0000454 await fh.syncHandle.flush();
stephan72ab4002022-09-21 12:27:35 +0000455 }catch(e){
stephan56fae742022-09-24 10:12:19 +0000456 state.s11n.storeException(2,e);
stephan72ab4002022-09-21 12:27:35 +0000457 }finally{
458 wTimeEnd();
459 }
stephan5e8bb0a2022-09-20 08:27:57 +0000460 }
stephan72ab4002022-09-21 12:27:35 +0000461 storeAndNotify('xSync',rc);
stephanaec046a2022-09-19 18:22:29 +0000462 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000463 },
stephan138647a2022-09-20 03:31:02 +0000464 xTruncate: async function(fid,size){
stephanaec046a2022-09-19 18:22:29 +0000465 mTimeStart('xTruncate');
stephan07315542022-09-17 20:50:12 +0000466 let rc = 0;
467 const fh = __openFiles[fid];
stephan5e8bb0a2022-09-20 08:27:57 +0000468 wTimeStart('xTruncate');
stephan07315542022-09-17 20:50:12 +0000469 try{
470 affirmNotRO('xTruncate', fh);
stephan7ff8da82022-10-03 09:21:37 +0000471 await (await getSyncHandle(fh)).truncate(size);
stephan07315542022-09-17 20:50:12 +0000472 }catch(e){
473 error("xTruncate():",e,fh);
stephan56fae742022-09-24 10:12:19 +0000474 state.s11n.storeException(2,e);
stephan07315542022-09-17 20:50:12 +0000475 rc = state.sq3Codes.SQLITE_IOERR_TRUNCATE;
476 }
stephan5e8bb0a2022-09-20 08:27:57 +0000477 wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000478 storeAndNotify('xTruncate',rc);
stephanaec046a2022-09-19 18:22:29 +0000479 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000480 },
stephan9a557732022-10-04 17:06:51 +0000481 xUnlock: async function(fid,lockType){
482 mTimeStart('xUnlock');
483 let rc = 0;
484 const fh = __openFiles[fid];
485 if( state.sq3Codes.SQLITE_LOCK_NONE===lockType
486 && fh.syncHandle ){
487 try { await closeSyncHandle(fh) }
488 catch(e){
489 state.s11n.storeException(1,e);
490 rc = state.sq3Codes.SQLITE_IOERR;
491 /* Maybe we want to not report this? "Destructors do not
492 throw." */
493 }
494 }
495 storeAndNotify('xUnlock',rc);
496 mTimeEnd();
497 },
stephan138647a2022-09-20 03:31:02 +0000498 xWrite: async function(fid,n,offset){
stephanaec046a2022-09-19 18:22:29 +0000499 mTimeStart('xWrite');
stephan07315542022-09-17 20:50:12 +0000500 let rc;
stephan5e8bb0a2022-09-20 08:27:57 +0000501 wTimeStart('xWrite');
stephan07315542022-09-17 20:50:12 +0000502 try{
stephanaec046a2022-09-19 18:22:29 +0000503 const fh = __openFiles[fid];
stephan07315542022-09-17 20:50:12 +0000504 affirmNotRO('xWrite', fh);
stephanaec046a2022-09-19 18:22:29 +0000505 rc = (
stephan7ff8da82022-10-03 09:21:37 +0000506 n === (await getSyncHandle(fh))
507 .write(fh.sabView.subarray(0, n),
508 {at: Number(offset)})
stephanaec046a2022-09-19 18:22:29 +0000509 ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
stephan07315542022-09-17 20:50:12 +0000510 }catch(e){
511 error("xWrite():",e,fh);
stephan56fae742022-09-24 10:12:19 +0000512 state.s11n.storeException(1,e);
stephan07315542022-09-17 20:50:12 +0000513 rc = state.sq3Codes.SQLITE_IOERR_WRITE;
stephan5e8bb0a2022-09-20 08:27:57 +0000514 }finally{
515 wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000516 }
517 storeAndNotify('xWrite',rc);
stephanaec046a2022-09-19 18:22:29 +0000518 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000519 }
stephan9a557732022-10-04 17:06:51 +0000520}/*vfsAsyncImpls*/;
stephan07315542022-09-17 20:50:12 +0000521
stephan138647a2022-09-20 03:31:02 +0000522const initS11n = ()=>{
stephanb8c8d4e2022-09-20 13:25:39 +0000523 /**
stephan72ab4002022-09-21 12:27:35 +0000524 ACHTUNG: this code is 100% duplicated in the other half of this
525 proxy! The documentation is maintained in the "synchronous half".
stephanb8c8d4e2022-09-20 13:25:39 +0000526 */
stephan138647a2022-09-20 03:31:02 +0000527 if(state.s11n) return state.s11n;
stephanb8c8d4e2022-09-20 13:25:39 +0000528 const textDecoder = new TextDecoder(),
529 textEncoder = new TextEncoder('utf-8'),
530 viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize),
531 viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
stephan138647a2022-09-20 03:31:02 +0000532 state.s11n = Object.create(null);
stephanb8c8d4e2022-09-20 13:25:39 +0000533 const TypeIds = Object.create(null);
534 TypeIds.number = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' };
535 TypeIds.bigint = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' };
536 TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' };
537 TypeIds.string = { id: 4 };
stephan72ab4002022-09-21 12:27:35 +0000538 const getTypeId = (v)=>(
539 TypeIds[typeof v]
540 || toss("Maintenance required: this value type cannot be serialized.",v)
541 );
stephanb8c8d4e2022-09-20 13:25:39 +0000542 const getTypeIdById = (tid)=>{
543 switch(tid){
stephan72ab4002022-09-21 12:27:35 +0000544 case TypeIds.number.id: return TypeIds.number;
545 case TypeIds.bigint.id: return TypeIds.bigint;
546 case TypeIds.boolean.id: return TypeIds.boolean;
547 case TypeIds.string.id: return TypeIds.string;
548 default: toss("Invalid type ID:",tid);
stephanb8c8d4e2022-09-20 13:25:39 +0000549 }
550 };
stephan138647a2022-09-20 03:31:02 +0000551 state.s11n.deserialize = function(){
stephanb8c8d4e2022-09-20 13:25:39 +0000552 ++metrics.s11n.deserialize.count;
553 const t = performance.now();
stephanb8c8d4e2022-09-20 13:25:39 +0000554 const argc = viewU8[0];
stephan72ab4002022-09-21 12:27:35 +0000555 const rc = argc ? [] : null;
stephanb8c8d4e2022-09-20 13:25:39 +0000556 if(argc){
stephan72ab4002022-09-21 12:27:35 +0000557 const typeIds = [];
558 let offset = 1, i, n, v;
stephanb8c8d4e2022-09-20 13:25:39 +0000559 for(i = 0; i < argc; ++i, ++offset){
560 typeIds.push(getTypeIdById(viewU8[offset]));
561 }
562 for(i = 0; i < argc; ++i){
563 const t = typeIds[i];
564 if(t.getter){
565 v = viewDV[t.getter](offset, state.littleEndian);
566 offset += t.size;
stephan72ab4002022-09-21 12:27:35 +0000567 }else{/*String*/
stephanb8c8d4e2022-09-20 13:25:39 +0000568 n = viewDV.getInt32(offset, state.littleEndian);
569 offset += 4;
570 v = textDecoder.decode(viewU8.slice(offset, offset+n));
571 offset += n;
572 }
573 rc.push(v);
574 }
575 }
576 //log("deserialize:",argc, rc);
577 metrics.s11n.deserialize.time += performance.now() - t;
578 return rc;
579 };
stephan138647a2022-09-20 03:31:02 +0000580 state.s11n.serialize = function(...args){
stephanb8c8d4e2022-09-20 13:25:39 +0000581 const t = performance.now();
stephan72ab4002022-09-21 12:27:35 +0000582 ++metrics.s11n.serialize.count;
stephan5e8bb0a2022-09-20 08:27:57 +0000583 if(args.length){
stephanb8c8d4e2022-09-20 13:25:39 +0000584 //log("serialize():",args);
stephan72ab4002022-09-21 12:27:35 +0000585 const typeIds = [];
586 let i = 0, offset = 1;
587 viewU8[0] = args.length & 0xff /* header = # of args */;
stephanb8c8d4e2022-09-20 13:25:39 +0000588 for(; i < args.length; ++i, ++offset){
stephan72ab4002022-09-21 12:27:35 +0000589 /* Write the TypeIds.id value into the next args.length
590 bytes. */
stephanb8c8d4e2022-09-20 13:25:39 +0000591 typeIds.push(getTypeId(args[i]));
592 viewU8[offset] = typeIds[i].id;
593 }
594 for(i = 0; i < args.length; ++i) {
stephan72ab4002022-09-21 12:27:35 +0000595 /* Deserialize the following bytes based on their
596 corresponding TypeIds.id from the header. */
stephanb8c8d4e2022-09-20 13:25:39 +0000597 const t = typeIds[i];
598 if(t.setter){
599 viewDV[t.setter](offset, args[i], state.littleEndian);
600 offset += t.size;
stephan72ab4002022-09-21 12:27:35 +0000601 }else{/*String*/
stephanb8c8d4e2022-09-20 13:25:39 +0000602 const s = textEncoder.encode(args[i]);
603 viewDV.setInt32(offset, s.byteLength, state.littleEndian);
604 offset += 4;
605 viewU8.set(s, offset);
606 offset += s.byteLength;
607 }
608 }
609 //log("serialize() result:",viewU8.slice(0,offset));
stephan5e8bb0a2022-09-20 08:27:57 +0000610 }else{
stephanb8c8d4e2022-09-20 13:25:39 +0000611 viewU8[0] = 0;
stephan5e8bb0a2022-09-20 08:27:57 +0000612 }
stephanb8c8d4e2022-09-20 13:25:39 +0000613 metrics.s11n.serialize.time += performance.now() - t;
stephan138647a2022-09-20 03:31:02 +0000614 };
stephane8afca32022-09-21 14:02:47 +0000615
616 state.s11n.storeException = state.asyncS11nExceptions
stephan56fae742022-09-24 10:12:19 +0000617 ? ((priority,e)=>{
618 if(priority<=state.asyncS11nExceptions){
619 state.s11n.serialize(e.message);
620 }
621 })
stephane8afca32022-09-21 14:02:47 +0000622 : ()=>{};
623
stephan138647a2022-09-20 03:31:02 +0000624 return state.s11n;
stephanb8c8d4e2022-09-20 13:25:39 +0000625}/*initS11n()*/;
stephan138647a2022-09-20 03:31:02 +0000626
stephan5e8bb0a2022-09-20 08:27:57 +0000627const waitLoop = async function f(){
stephan138647a2022-09-20 03:31:02 +0000628 const opHandlers = Object.create(null);
stephanc9e26022022-09-20 10:11:52 +0000629 for(let k of Object.keys(state.opIds)){
630 const vi = vfsAsyncImpls[k];
631 if(!vi) continue;
stephan138647a2022-09-20 03:31:02 +0000632 const o = Object.create(null);
633 opHandlers[state.opIds[k]] = o;
634 o.key = k;
stephan56fae742022-09-24 10:12:19 +0000635 o.f = vi || toss("No vfsAsyncImpls[",k,"]");
stephan138647a2022-09-20 03:31:02 +0000636 }
stephan5f0b67c2022-10-03 11:33:35 +0000637 /**
638 waitTime is how long (ms) to wait for each Atomics.wait().
stephanc7fb48d2022-10-04 09:12:05 +0000639 We need to wake up periodically to give the thread a chance
stephan5f0b67c2022-10-03 11:33:35 +0000640 to do other things.
641 */
stephan9a557732022-10-04 17:06:51 +0000642 const waitTime = 1000;
stephan5f0b67c2022-10-03 11:33:35 +0000643 let lastOpTime = performance.now();
644 let now;
stephan3c272ba2022-10-04 00:54:00 +0000645 while(!flagAsyncShutdown){
stephan138647a2022-09-20 03:31:02 +0000646 try {
stephan5f0b67c2022-10-03 11:33:35 +0000647 if('timed-out'===Atomics.wait(
648 state.sabOPView, state.opIds.whichOp, 0, waitTime
649 )){
stephan5e8bb0a2022-09-20 08:27:57 +0000650 continue;
651 }
stephan5f0b67c2022-10-03 11:33:35 +0000652 lastOpTime = performance.now();
stephan5e8bb0a2022-09-20 08:27:57 +0000653 const opId = Atomics.load(state.sabOPView, state.opIds.whichOp);
654 Atomics.store(state.sabOPView, state.opIds.whichOp, 0);
stephan138647a2022-09-20 03:31:02 +0000655 const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId);
stephan56fae742022-09-24 10:12:19 +0000656 const args = state.s11n.deserialize() || [];
stephan5f0b67c2022-10-03 11:33:35 +0000657 state.s11n.serialize(/* clear s11n to keep the caller from
658 confusing this with an exception string
659 written by the upcoming operation */);
stephan5e8bb0a2022-09-20 08:27:57 +0000660 //warn("waitLoop() whichOp =",opId, hnd, args);
661 if(hnd.f) await hnd.f(...args);
662 else error("Missing callback for opId",opId);
stephan138647a2022-09-20 03:31:02 +0000663 }catch(e){
stephan5f0b67c2022-10-03 11:33:35 +0000664 error('in waitLoop():',e);
stephan138647a2022-09-20 03:31:02 +0000665 }
stephan5e8bb0a2022-09-20 08:27:57 +0000666 };
stephan138647a2022-09-20 03:31:02 +0000667};
668
stephan07315542022-09-17 20:50:12 +0000669navigator.storage.getDirectory().then(function(d){
670 const wMsg = (type)=>postMessage({type});
671 state.rootDir = d;
stephan5e8bb0a2022-09-20 08:27:57 +0000672 self.onmessage = function({data}){
stephan07315542022-09-17 20:50:12 +0000673 switch(data.type){
stephan138647a2022-09-20 03:31:02 +0000674 case 'opfs-async-init':{
stephan07315542022-09-17 20:50:12 +0000675 /* Receive shared state from synchronous partner */
stephan138647a2022-09-20 03:31:02 +0000676 const opt = data.args;
677 state.littleEndian = opt.littleEndian;
stephane8afca32022-09-21 14:02:47 +0000678 state.asyncS11nExceptions = opt.asyncS11nExceptions;
stephan07315542022-09-17 20:50:12 +0000679 state.verbose = opt.verbose ?? 2;
680 state.fileBufferSize = opt.fileBufferSize;
stephan138647a2022-09-20 03:31:02 +0000681 state.sabS11nOffset = opt.sabS11nOffset;
682 state.sabS11nSize = opt.sabS11nSize;
stephanc4b87be2022-09-20 01:28:47 +0000683 state.sabOP = opt.sabOP;
684 state.sabOPView = new Int32Array(state.sabOP);
685 state.sabIO = opt.sabIO;
686 state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
stephan138647a2022-09-20 03:31:02 +0000687 state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
stephan07315542022-09-17 20:50:12 +0000688 state.opIds = opt.opIds;
689 state.sq3Codes = opt.sq3Codes;
690 Object.keys(vfsAsyncImpls).forEach((k)=>{
691 if(!Number.isFinite(state.opIds[k])){
692 toss("Maintenance required: missing state.opIds[",k,"]");
693 }
694 });
stephan138647a2022-09-20 03:31:02 +0000695 initS11n();
stephanaec046a2022-09-19 18:22:29 +0000696 metrics.reset();
stephan07315542022-09-17 20:50:12 +0000697 log("init state",state);
stephan138647a2022-09-20 03:31:02 +0000698 wMsg('opfs-async-inited');
stephan5e8bb0a2022-09-20 08:27:57 +0000699 waitLoop();
stephan07315542022-09-17 20:50:12 +0000700 break;
701 }
stephan3c272ba2022-10-04 00:54:00 +0000702 case 'opfs-async-restart':
703 if(flagAsyncShutdown){
704 warn("Restarting after opfs-async-shutdown. Might or might not work.");
705 flagAsyncShutdown = false;
706 waitLoop();
707 }
708 break;
709 case 'opfs-async-metrics':
710 metrics.dump();
711 break;
stephan132a87b2022-09-17 15:08:22 +0000712 }
713 };
stephan138647a2022-09-20 03:31:02 +0000714 wMsg('opfs-async-loaded');
stephan9a557732022-10-04 17:06:51 +0000715}).catch((e)=>error("error initializing OPFS asyncer:",e));