blob: 58bbf79284b2d216b74f0f8055a57b142c58b43f [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.
stephan7ff8da82022-10-03 09:21:37 +000028
29 Potential TODOs:
30
31 - When idle for "a long time", close the sync access handle in order
32 to release the lock, then re-open it on demand. Similarly, delay
33 fetching of the sync access handle until we need it. The intent
34 would be to help multi-tab access to a db avoid locking issues.
stephan132a87b2022-09-17 15:08:22 +000035*/
36'use strict';
stephan07315542022-09-17 20:50:12 +000037const toss = function(...args){throw new Error(args.join(' '))};
38if(self.window === self){
39 toss("This code cannot run from the main thread.",
40 "Load it as a Worker from a separate Worker.");
41}else if(!navigator.storage.getDirectory){
42 toss("This API requires navigator.storage.getDirectory.");
43}
44/**
45 Will hold state copied to this object from the syncronous side of
46 this API.
47*/
48const state = Object.create(null);
49/**
50 verbose:
51
52 0 = no logging output
53 1 = only errors
54 2 = warnings and errors
55 3 = debug, warnings, and errors
56*/
57state.verbose = 2;
58
stephan509f4052022-09-19 09:58:01 +000059const loggers = {
60 0:console.error.bind(console),
61 1:console.warn.bind(console),
62 2:console.log.bind(console)
stephan07315542022-09-17 20:50:12 +000063};
stephan509f4052022-09-19 09:58:01 +000064const logImpl = (level,...args)=>{
65 if(state.verbose>level) loggers[level]("OPFS asyncer:",...args);
stephan07315542022-09-17 20:50:12 +000066};
stephan509f4052022-09-19 09:58:01 +000067const log = (...args)=>logImpl(2, ...args);
68const warn = (...args)=>logImpl(1, ...args);
69const error = (...args)=>logImpl(0, ...args);
stephanf8150112022-09-19 17:09:09 +000070const metrics = Object.create(null);
stephanaec046a2022-09-19 18:22:29 +000071metrics.reset = ()=>{
72 let k;
stephan5e8bb0a2022-09-20 08:27:57 +000073 const r = (m)=>(m.count = m.time = m.wait = 0);
stephanaec046a2022-09-19 18:22:29 +000074 for(k in state.opIds){
75 r(metrics[k] = Object.create(null));
76 }
stephanb8c8d4e2022-09-20 13:25:39 +000077 let s = metrics.s11n = Object.create(null);
78 s = s.serialize = Object.create(null);
79 s.count = s.time = 0;
80 s = metrics.s11n.deserialize = Object.create(null);
81 s.count = s.time = 0;
stephanaec046a2022-09-19 18:22:29 +000082};
83metrics.dump = ()=>{
84 let k, n = 0, t = 0, w = 0;
85 for(k in state.opIds){
86 const m = metrics[k];
87 n += m.count;
88 t += m.time;
stephan5e8bb0a2022-09-20 08:27:57 +000089 w += m.wait;
stephanaec046a2022-09-19 18:22:29 +000090 m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
91 }
92 console.log(self.location.href,
stephan5e8bb0a2022-09-20 08:27:57 +000093 "metrics for",self.location.href,":\n",
stephan56fae742022-09-24 10:12:19 +000094 metrics,
stephan5e8bb0a2022-09-20 08:27:57 +000095 "\nTotal of",n,"op(s) for",t,"ms",
96 "approx",w,"ms spent waiting on OPFS APIs.");
stephan56fae742022-09-24 10:12:19 +000097 console.log("Serialization metrics:",metrics.s11n);
stephanaec046a2022-09-19 18:22:29 +000098};
stephan07315542022-09-17 20:50:12 +000099
stephanf6c686c2022-09-30 11:01:44 +0000100//warn("This file is very much experimental and under construction.",self.location.pathname);
stephan07315542022-09-17 20:50:12 +0000101
102/**
103 Map of sqlite3_file pointers (integers) to metadata related to a
104 given OPFS file handles. The pointers are, in this side of the
105 interface, opaque file handle IDs provided by the synchronous
106 part of this constellation. Each value is an object with a structure
107 demonstrated in the xOpen() impl.
108*/
109const __openFiles = Object.create(null);
110
111/**
stephan8200a6d2022-09-17 23:29:27 +0000112 Expects an OPFS file path. It gets resolved, such that ".."
113 components are properly expanded, and returned. If the 2nd
114 are is true, it's returned as an array of path elements,
115 else it's returned as an absolute path string.
stephan07315542022-09-17 20:50:12 +0000116*/
stephan8200a6d2022-09-17 23:29:27 +0000117const getResolvedPath = function(filename,splitIt){
118 const p = new URL(
119 filename, 'file://irrelevant'
120 ).pathname;
121 return splitIt ? p.split('/').filter((v)=>!!v) : p;
stephan509f4052022-09-19 09:58:01 +0000122};
stephan07315542022-09-17 20:50:12 +0000123
124/**
125 Takes the absolute path to a filesystem element. Returns an array
126 of [handleOfContainingDir, filename]. If the 2nd argument is
127 truthy then each directory element leading to the file is created
128 along the way. Throws if any creation or resolution fails.
129*/
stephan56fae742022-09-24 10:12:19 +0000130const getDirForFilename = async function f(absFilename, createDirs = false){
stephan8200a6d2022-09-17 23:29:27 +0000131 const path = getResolvedPath(absFilename, true);
stephan07315542022-09-17 20:50:12 +0000132 const filename = path.pop();
stephan8200a6d2022-09-17 23:29:27 +0000133 let dh = state.rootDir;
134 for(const dirName of path){
135 if(dirName){
136 dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs});
stephan07315542022-09-17 20:50:12 +0000137 }
stephan132a87b2022-09-17 15:08:22 +0000138 }
stephan07315542022-09-17 20:50:12 +0000139 return [dh, filename];
140};
stephan132a87b2022-09-17 15:08:22 +0000141
stephan7ff8da82022-10-03 09:21:37 +0000142/**
143 Returns the sync access handle associated with the given file
144 handle object (which must be a valid handle object), lazily opening
stephan5f0b67c2022-10-03 11:33:35 +0000145 it if needed. Timestamps the handle for use in relinquishing it
146 during idle time.
stephan7ff8da82022-10-03 09:21:37 +0000147*/
stephan5f0b67c2022-10-03 11:33:35 +0000148const getSyncHandle = async (fh)=>{
149 if(!fh.syncHandle){
150 //const t = performance.now();
151 //warn("Creating sync handle for",fh.filenameAbs);
152 fh.syncHandle = await fh.fileHandle.createSyncAccessHandle();
153 //warn("Got sync handle for",fh.filenameAbs,'in',performance.now() - t,'ms');
154 }
155 fh.syncHandleTime = performance.now();
156 return fh.syncHandle;
157};
158
159const closeSyncHandle = async (fh)=>{
160 if(fh.syncHandle){
161 //warn("Closing sync handle for",fh.filenameAbs);
162 const h = fh.syncHandle;
163 delete fh.syncHandle;
164 return h.close();
165 }
166};
stephan132a87b2022-09-17 15:08:22 +0000167
stephan07315542022-09-17 20:50:12 +0000168/**
stephan72ab4002022-09-21 12:27:35 +0000169 Stores the given value at state.sabOPView[state.opIds.rc] and then
170 Atomics.notify()'s it.
stephan07315542022-09-17 20:50:12 +0000171*/
172const storeAndNotify = (opName, value)=>{
stephanc9e26022022-09-20 10:11:52 +0000173 log(opName+"() => notify(",state.opIds.rc,",",value,")");
174 Atomics.store(state.sabOPView, state.opIds.rc, value);
175 Atomics.notify(state.sabOPView, state.opIds.rc);
stephan07315542022-09-17 20:50:12 +0000176};
stephan132a87b2022-09-17 15:08:22 +0000177
stephan07315542022-09-17 20:50:12 +0000178/**
179 Throws if fh is a file-holding object which is flagged as read-only.
180*/
181const affirmNotRO = function(opName,fh){
182 if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs);
183};
184
stephanaec046a2022-09-19 18:22:29 +0000185
186const opTimer = Object.create(null);
187opTimer.op = undefined;
188opTimer.start = undefined;
189const mTimeStart = (op)=>{
190 opTimer.start = performance.now();
191 opTimer.op = op;
192 //metrics[op] || toss("Maintenance required: missing metrics for",op);
193 ++metrics[op].count;
194};
195const mTimeEnd = ()=>(
196 metrics[opTimer.op].time += performance.now() - opTimer.start
197);
stephan5e8bb0a2022-09-20 08:27:57 +0000198const waitTimer = Object.create(null);
199waitTimer.op = undefined;
200waitTimer.start = undefined;
201const wTimeStart = (op)=>{
202 waitTimer.start = performance.now();
203 waitTimer.op = op;
204 //metrics[op] || toss("Maintenance required: missing metrics for",op);
205};
206const wTimeEnd = ()=>(
207 metrics[waitTimer.op].wait += performance.now() - waitTimer.start
208);
stephanaec046a2022-09-19 18:22:29 +0000209
stephan07315542022-09-17 20:50:12 +0000210/**
stephan3c272ba2022-10-04 00:54:00 +0000211 Set to true by the 'opfs-async-shutdown' command to quite the wait loop.
212 This is only intended for debugging purposes: we cannot inspect this
213 file's state while the tight waitLoop() is running.
214*/
215let flagAsyncShutdown = false;
216
217/**
stephan07315542022-09-17 20:50:12 +0000218 Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods
219 methods. Maintenance reminder: members are in alphabetical order
220 to simplify finding them.
221*/
222const vfsAsyncImpls = {
stephan3c272ba2022-10-04 00:54:00 +0000223 'opfs-async-metrics': async ()=>{
224 mTimeStart('opfs-async-metrics');
stephan56fae742022-09-24 10:12:19 +0000225 metrics.dump();
stephan3c272ba2022-10-04 00:54:00 +0000226 storeAndNotify('opfs-async-metrics', 0);
stephan56fae742022-09-24 10:12:19 +0000227 mTimeEnd();
228 },
stephan3c272ba2022-10-04 00:54:00 +0000229 'opfs-async-shutdown': async ()=>{
230 flagAsyncShutdown = true;
231 storeAndNotify('opfs-async-shutdown', 0);
232 },
stephan56fae742022-09-24 10:12:19 +0000233 mkdir: async (dirname)=>{
stephan5e8bb0a2022-09-20 08:27:57 +0000234 mTimeStart('mkdir');
stephanaec046a2022-09-19 18:22:29 +0000235 let rc = 0;
stephan5e8bb0a2022-09-20 08:27:57 +0000236 wTimeStart('mkdir');
stephanaec046a2022-09-19 18:22:29 +0000237 try {
stephan56fae742022-09-24 10:12:19 +0000238 await getDirForFilename(dirname+"/filepart", true);
stephanaec046a2022-09-19 18:22:29 +0000239 }catch(e){
stephan56fae742022-09-24 10:12:19 +0000240 state.s11n.storeException(2,e);
stephanaec046a2022-09-19 18:22:29 +0000241 rc = state.sq3Codes.SQLITE_IOERR;
stephan72ab4002022-09-21 12:27:35 +0000242 }finally{
243 wTimeEnd();
stephanaec046a2022-09-19 18:22:29 +0000244 }
245 storeAndNotify('mkdir', rc);
stephan5e8bb0a2022-09-20 08:27:57 +0000246 mTimeEnd();
stephanaec046a2022-09-19 18:22:29 +0000247 },
stephan56fae742022-09-24 10:12:19 +0000248 xAccess: async (filename)=>{
stephanaec046a2022-09-19 18:22:29 +0000249 mTimeStart('xAccess');
stephan8200a6d2022-09-17 23:29:27 +0000250 /* OPFS cannot support the full range of xAccess() queries sqlite3
251 calls for. We can essentially just tell if the file is
252 accessible, but if it is it's automatically writable (unless
253 it's locked, which we cannot(?) know without trying to open
254 it). OPFS does not have the notion of read-only.
255
256 The return semantics of this function differ from sqlite3's
257 xAccess semantics because we are limited in what we can
258 communicate back to our synchronous communication partner: 0 =
259 accessible, non-0 means not accessible.
260 */
261 let rc = 0;
stephan5e8bb0a2022-09-20 08:27:57 +0000262 wTimeStart('xAccess');
stephan8200a6d2022-09-17 23:29:27 +0000263 try{
stephan56fae742022-09-24 10:12:19 +0000264 const [dh, fn] = await getDirForFilename(filename);
stephan8200a6d2022-09-17 23:29:27 +0000265 await dh.getFileHandle(fn);
266 }catch(e){
stephan56fae742022-09-24 10:12:19 +0000267 state.s11n.storeException(2,e);
stephan8200a6d2022-09-17 23:29:27 +0000268 rc = state.sq3Codes.SQLITE_IOERR;
stephan72ab4002022-09-21 12:27:35 +0000269 }finally{
270 wTimeEnd();
stephan8200a6d2022-09-17 23:29:27 +0000271 }
stephan07315542022-09-17 20:50:12 +0000272 storeAndNotify('xAccess', rc);
stephanaec046a2022-09-19 18:22:29 +0000273 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000274 },
275 xClose: async function(fid){
276 const opName = 'xClose';
stephanaec046a2022-09-19 18:22:29 +0000277 mTimeStart(opName);
stephan07315542022-09-17 20:50:12 +0000278 const fh = __openFiles[fid];
stephan5e8bb0a2022-09-20 08:27:57 +0000279 let rc = 0;
280 wTimeStart('xClose');
stephan07315542022-09-17 20:50:12 +0000281 if(fh){
282 delete __openFiles[fid];
stephan5f0b67c2022-10-03 11:33:35 +0000283 await closeSyncHandle(fh);
stephan07315542022-09-17 20:50:12 +0000284 if(fh.deleteOnClose){
285 try{ await fh.dirHandle.removeEntry(fh.filenamePart) }
286 catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) }
stephan132a87b2022-09-17 15:08:22 +0000287 }
stephan07315542022-09-17 20:50:12 +0000288 }else{
stephan72ab4002022-09-21 12:27:35 +0000289 state.s11n.serialize();
stephan5e8bb0a2022-09-20 08:27:57 +0000290 rc = state.sq3Codes.SQLITE_NOTFOUND;
stephan132a87b2022-09-17 15:08:22 +0000291 }
stephan5e8bb0a2022-09-20 08:27:57 +0000292 wTimeEnd();
293 storeAndNotify(opName, rc);
stephanaec046a2022-09-19 18:22:29 +0000294 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000295 },
stephanc4b87be2022-09-20 01:28:47 +0000296 xDelete: async function(...args){
297 mTimeStart('xDelete');
298 const rc = await vfsAsyncImpls.xDeleteNoWait(...args);
299 storeAndNotify('xDelete', rc);
300 mTimeEnd();
301 },
stephan138647a2022-09-20 03:31:02 +0000302 xDeleteNoWait: async function(filename, syncDir = 0, recursive = false){
stephan8200a6d2022-09-17 23:29:27 +0000303 /* The syncDir flag is, for purposes of the VFS API's semantics,
304 ignored here. However, if it has the value 0x1234 then: after
305 deleting the given file, recursively try to delete any empty
306 directories left behind in its wake (ignoring any errors and
307 stopping at the first failure).
308
309 That said: we don't know for sure that removeEntry() fails if
310 the dir is not empty because the API is not documented. It has,
311 however, a "recursive" flag which defaults to false, so
312 presumably it will fail if the dir is not empty and that flag
313 is false.
314 */
stephanf3860122022-09-18 17:32:35 +0000315 let rc = 0;
stephan5e8bb0a2022-09-20 08:27:57 +0000316 wTimeStart('xDelete');
stephan07315542022-09-17 20:50:12 +0000317 try {
stephan8200a6d2022-09-17 23:29:27 +0000318 while(filename){
stephan56fae742022-09-24 10:12:19 +0000319 const [hDir, filenamePart] = await getDirForFilename(filename, false);
stephan8200a6d2022-09-17 23:29:27 +0000320 if(!filenamePart) break;
stephanf3860122022-09-18 17:32:35 +0000321 await hDir.removeEntry(filenamePart, {recursive});
stephan8200a6d2022-09-17 23:29:27 +0000322 if(0x1234 !== syncDir) break;
323 filename = getResolvedPath(filename, true);
324 filename.pop();
325 filename = filename.join('/');
326 }
stephan07315542022-09-17 20:50:12 +0000327 }catch(e){
stephan56fae742022-09-24 10:12:19 +0000328 state.s11n.storeException(2,e);
stephanf3860122022-09-18 17:32:35 +0000329 rc = state.sq3Codes.SQLITE_IOERR_DELETE;
stephan132a87b2022-09-17 15:08:22 +0000330 }
stephan5e8bb0a2022-09-20 08:27:57 +0000331 wTimeEnd();
stephanf3860122022-09-18 17:32:35 +0000332 return rc;
333 },
stephan07315542022-09-17 20:50:12 +0000334 xFileSize: async function(fid){
stephanaec046a2022-09-19 18:22:29 +0000335 mTimeStart('xFileSize');
stephan07315542022-09-17 20:50:12 +0000336 const fh = __openFiles[fid];
337 let sz;
stephan5e8bb0a2022-09-20 08:27:57 +0000338 wTimeStart('xFileSize');
stephan07315542022-09-17 20:50:12 +0000339 try{
stephan7ff8da82022-10-03 09:21:37 +0000340 sz = await (await getSyncHandle(fh)).getSize();
stephan138647a2022-09-20 03:31:02 +0000341 state.s11n.serialize(Number(sz));
stephan07315542022-09-17 20:50:12 +0000342 sz = 0;
343 }catch(e){
stephan56fae742022-09-24 10:12:19 +0000344 state.s11n.storeException(2,e);
stephan07315542022-09-17 20:50:12 +0000345 sz = state.sq3Codes.SQLITE_IOERR;
stephan132a87b2022-09-17 15:08:22 +0000346 }
stephan5e8bb0a2022-09-20 08:27:57 +0000347 wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000348 storeAndNotify('xFileSize', sz);
stephanaec046a2022-09-19 18:22:29 +0000349 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000350 },
stephan138647a2022-09-20 03:31:02 +0000351 xOpen: async function(fid/*sqlite3_file pointer*/, filename, flags){
stephan07315542022-09-17 20:50:12 +0000352 const opName = 'xOpen';
stephanaec046a2022-09-19 18:22:29 +0000353 mTimeStart(opName);
stephanc4b87be2022-09-20 01:28:47 +0000354 const deleteOnClose = (state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags);
355 const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags);
stephan5e8bb0a2022-09-20 08:27:57 +0000356 wTimeStart('xOpen');
stephan07315542022-09-17 20:50:12 +0000357 try{
stephan07315542022-09-17 20:50:12 +0000358 let hDir, filenamePart;
359 try {
stephan56fae742022-09-24 10:12:19 +0000360 [hDir, filenamePart] = await getDirForFilename(filename, !!create);
stephan132a87b2022-09-17 15:08:22 +0000361 }catch(e){
stephan07315542022-09-17 20:50:12 +0000362 storeAndNotify(opName, state.sql3Codes.SQLITE_NOTFOUND);
stephanaec046a2022-09-19 18:22:29 +0000363 mTimeEnd();
stephan5e8bb0a2022-09-20 08:27:57 +0000364 wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000365 return;
stephan132a87b2022-09-17 15:08:22 +0000366 }
stephanc4b87be2022-09-20 01:28:47 +0000367 const hFile = await hDir.getFileHandle(filenamePart, {create});
stephan07315542022-09-17 20:50:12 +0000368 /**
369 wa-sqlite, at this point, grabs a SyncAccessHandle and
stephan5f0b67c2022-10-03 11:33:35 +0000370 assigns it to the syncHandle prop of the file state
stephan07315542022-09-17 20:50:12 +0000371 object, but only for certain cases and it's unclear why it
372 places that limitation on it.
373 */
stephan5e8bb0a2022-09-20 08:27:57 +0000374 wTimeEnd();
stephan5f0b67c2022-10-03 11:33:35 +0000375 __openFiles[fid] = Object.assign(Object.create(null),{
stephan7ff8da82022-10-03 09:21:37 +0000376 filenameAbs: filename,
377 filenamePart: filenamePart,
378 dirHandle: hDir,
379 fileHandle: hFile,
380 sabView: state.sabFileBufView,
381 readOnly: create
382 ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags),
383 deleteOnClose: deleteOnClose
384 });
stephan07315542022-09-17 20:50:12 +0000385 storeAndNotify(opName, 0);
386 }catch(e){
stephan5e8bb0a2022-09-20 08:27:57 +0000387 wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000388 error(opName,e);
stephan56fae742022-09-24 10:12:19 +0000389 state.s11n.storeException(1,e);
stephan07315542022-09-17 20:50:12 +0000390 storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
391 }
stephanaec046a2022-09-19 18:22:29 +0000392 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000393 },
stephan138647a2022-09-20 03:31:02 +0000394 xRead: async function(fid,n,offset){
stephanaec046a2022-09-19 18:22:29 +0000395 mTimeStart('xRead');
stephan07315542022-09-17 20:50:12 +0000396 let rc = 0;
stephan5f0b67c2022-10-03 11:33:35 +0000397 const fh = __openFiles[fid];
stephan07315542022-09-17 20:50:12 +0000398 try{
stephan5e8bb0a2022-09-20 08:27:57 +0000399 wTimeStart('xRead');
stephan7ff8da82022-10-03 09:21:37 +0000400 const nRead = (await getSyncHandle(fh)).read(
stephanaec046a2022-09-19 18:22:29 +0000401 fh.sabView.subarray(0, n),
402 {at: Number(offset)}
403 );
stephan5e8bb0a2022-09-20 08:27:57 +0000404 wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000405 if(nRead < n){/* Zero-fill remaining bytes */
stephanaec046a2022-09-19 18:22:29 +0000406 fh.sabView.fill(0, nRead, n);
stephan07315542022-09-17 20:50:12 +0000407 rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
408 }
409 }catch(e){
410 error("xRead() failed",e,fh);
stephan56fae742022-09-24 10:12:19 +0000411 state.s11n.storeException(1,e);
stephan07315542022-09-17 20:50:12 +0000412 rc = state.sq3Codes.SQLITE_IOERR_READ;
413 }
414 storeAndNotify('xRead',rc);
stephanaec046a2022-09-19 18:22:29 +0000415 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000416 },
stephan138647a2022-09-20 03:31:02 +0000417 xSync: async function(fid,flags/*ignored*/){
stephanaec046a2022-09-19 18:22:29 +0000418 mTimeStart('xSync');
stephan07315542022-09-17 20:50:12 +0000419 const fh = __openFiles[fid];
stephan72ab4002022-09-21 12:27:35 +0000420 let rc = 0;
stephan5f0b67c2022-10-03 11:33:35 +0000421 if(!fh.readOnly && fh.syncHandle){
stephan72ab4002022-09-21 12:27:35 +0000422 try {
423 wTimeStart('xSync');
stephan5f0b67c2022-10-03 11:33:35 +0000424 await fh.syncHandle.flush();
stephan72ab4002022-09-21 12:27:35 +0000425 }catch(e){
stephan56fae742022-09-24 10:12:19 +0000426 state.s11n.storeException(2,e);
stephan72ab4002022-09-21 12:27:35 +0000427 }finally{
428 wTimeEnd();
429 }
stephan5e8bb0a2022-09-20 08:27:57 +0000430 }
stephan72ab4002022-09-21 12:27:35 +0000431 storeAndNotify('xSync',rc);
stephanaec046a2022-09-19 18:22:29 +0000432 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000433 },
stephan138647a2022-09-20 03:31:02 +0000434 xTruncate: async function(fid,size){
stephanaec046a2022-09-19 18:22:29 +0000435 mTimeStart('xTruncate');
stephan07315542022-09-17 20:50:12 +0000436 let rc = 0;
437 const fh = __openFiles[fid];
stephan5e8bb0a2022-09-20 08:27:57 +0000438 wTimeStart('xTruncate');
stephan07315542022-09-17 20:50:12 +0000439 try{
440 affirmNotRO('xTruncate', fh);
stephan7ff8da82022-10-03 09:21:37 +0000441 await (await getSyncHandle(fh)).truncate(size);
stephan07315542022-09-17 20:50:12 +0000442 }catch(e){
443 error("xTruncate():",e,fh);
stephan56fae742022-09-24 10:12:19 +0000444 state.s11n.storeException(2,e);
stephan07315542022-09-17 20:50:12 +0000445 rc = state.sq3Codes.SQLITE_IOERR_TRUNCATE;
446 }
stephan5e8bb0a2022-09-20 08:27:57 +0000447 wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000448 storeAndNotify('xTruncate',rc);
stephanaec046a2022-09-19 18:22:29 +0000449 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000450 },
stephan138647a2022-09-20 03:31:02 +0000451 xWrite: async function(fid,n,offset){
stephanaec046a2022-09-19 18:22:29 +0000452 mTimeStart('xWrite');
stephan07315542022-09-17 20:50:12 +0000453 let rc;
stephan5e8bb0a2022-09-20 08:27:57 +0000454 wTimeStart('xWrite');
stephan07315542022-09-17 20:50:12 +0000455 try{
stephanaec046a2022-09-19 18:22:29 +0000456 const fh = __openFiles[fid];
stephan07315542022-09-17 20:50:12 +0000457 affirmNotRO('xWrite', fh);
stephanaec046a2022-09-19 18:22:29 +0000458 rc = (
stephan7ff8da82022-10-03 09:21:37 +0000459 n === (await getSyncHandle(fh))
460 .write(fh.sabView.subarray(0, n),
461 {at: Number(offset)})
stephanaec046a2022-09-19 18:22:29 +0000462 ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
stephan07315542022-09-17 20:50:12 +0000463 }catch(e){
464 error("xWrite():",e,fh);
stephan56fae742022-09-24 10:12:19 +0000465 state.s11n.storeException(1,e);
stephan07315542022-09-17 20:50:12 +0000466 rc = state.sq3Codes.SQLITE_IOERR_WRITE;
stephan5e8bb0a2022-09-20 08:27:57 +0000467 }finally{
468 wTimeEnd();
stephan07315542022-09-17 20:50:12 +0000469 }
470 storeAndNotify('xWrite',rc);
stephanaec046a2022-09-19 18:22:29 +0000471 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000472 }
473};
474
stephan138647a2022-09-20 03:31:02 +0000475const initS11n = ()=>{
stephanb8c8d4e2022-09-20 13:25:39 +0000476 /**
stephan72ab4002022-09-21 12:27:35 +0000477 ACHTUNG: this code is 100% duplicated in the other half of this
478 proxy! The documentation is maintained in the "synchronous half".
stephanb8c8d4e2022-09-20 13:25:39 +0000479 */
stephan138647a2022-09-20 03:31:02 +0000480 if(state.s11n) return state.s11n;
stephanb8c8d4e2022-09-20 13:25:39 +0000481 const textDecoder = new TextDecoder(),
482 textEncoder = new TextEncoder('utf-8'),
483 viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize),
484 viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
stephan138647a2022-09-20 03:31:02 +0000485 state.s11n = Object.create(null);
stephanb8c8d4e2022-09-20 13:25:39 +0000486 const TypeIds = Object.create(null);
487 TypeIds.number = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' };
488 TypeIds.bigint = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' };
489 TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' };
490 TypeIds.string = { id: 4 };
stephan72ab4002022-09-21 12:27:35 +0000491 const getTypeId = (v)=>(
492 TypeIds[typeof v]
493 || toss("Maintenance required: this value type cannot be serialized.",v)
494 );
stephanb8c8d4e2022-09-20 13:25:39 +0000495 const getTypeIdById = (tid)=>{
496 switch(tid){
stephan72ab4002022-09-21 12:27:35 +0000497 case TypeIds.number.id: return TypeIds.number;
498 case TypeIds.bigint.id: return TypeIds.bigint;
499 case TypeIds.boolean.id: return TypeIds.boolean;
500 case TypeIds.string.id: return TypeIds.string;
501 default: toss("Invalid type ID:",tid);
stephanb8c8d4e2022-09-20 13:25:39 +0000502 }
503 };
stephan138647a2022-09-20 03:31:02 +0000504 state.s11n.deserialize = function(){
stephanb8c8d4e2022-09-20 13:25:39 +0000505 ++metrics.s11n.deserialize.count;
506 const t = performance.now();
stephanb8c8d4e2022-09-20 13:25:39 +0000507 const argc = viewU8[0];
stephan72ab4002022-09-21 12:27:35 +0000508 const rc = argc ? [] : null;
stephanb8c8d4e2022-09-20 13:25:39 +0000509 if(argc){
stephan72ab4002022-09-21 12:27:35 +0000510 const typeIds = [];
511 let offset = 1, i, n, v;
stephanb8c8d4e2022-09-20 13:25:39 +0000512 for(i = 0; i < argc; ++i, ++offset){
513 typeIds.push(getTypeIdById(viewU8[offset]));
514 }
515 for(i = 0; i < argc; ++i){
516 const t = typeIds[i];
517 if(t.getter){
518 v = viewDV[t.getter](offset, state.littleEndian);
519 offset += t.size;
stephan72ab4002022-09-21 12:27:35 +0000520 }else{/*String*/
stephanb8c8d4e2022-09-20 13:25:39 +0000521 n = viewDV.getInt32(offset, state.littleEndian);
522 offset += 4;
523 v = textDecoder.decode(viewU8.slice(offset, offset+n));
524 offset += n;
525 }
526 rc.push(v);
527 }
528 }
529 //log("deserialize:",argc, rc);
530 metrics.s11n.deserialize.time += performance.now() - t;
531 return rc;
532 };
stephan138647a2022-09-20 03:31:02 +0000533 state.s11n.serialize = function(...args){
stephanb8c8d4e2022-09-20 13:25:39 +0000534 const t = performance.now();
stephan72ab4002022-09-21 12:27:35 +0000535 ++metrics.s11n.serialize.count;
stephan5e8bb0a2022-09-20 08:27:57 +0000536 if(args.length){
stephanb8c8d4e2022-09-20 13:25:39 +0000537 //log("serialize():",args);
stephan72ab4002022-09-21 12:27:35 +0000538 const typeIds = [];
539 let i = 0, offset = 1;
540 viewU8[0] = args.length & 0xff /* header = # of args */;
stephanb8c8d4e2022-09-20 13:25:39 +0000541 for(; i < args.length; ++i, ++offset){
stephan72ab4002022-09-21 12:27:35 +0000542 /* Write the TypeIds.id value into the next args.length
543 bytes. */
stephanb8c8d4e2022-09-20 13:25:39 +0000544 typeIds.push(getTypeId(args[i]));
545 viewU8[offset] = typeIds[i].id;
546 }
547 for(i = 0; i < args.length; ++i) {
stephan72ab4002022-09-21 12:27:35 +0000548 /* Deserialize the following bytes based on their
549 corresponding TypeIds.id from the header. */
stephanb8c8d4e2022-09-20 13:25:39 +0000550 const t = typeIds[i];
551 if(t.setter){
552 viewDV[t.setter](offset, args[i], state.littleEndian);
553 offset += t.size;
stephan72ab4002022-09-21 12:27:35 +0000554 }else{/*String*/
stephanb8c8d4e2022-09-20 13:25:39 +0000555 const s = textEncoder.encode(args[i]);
556 viewDV.setInt32(offset, s.byteLength, state.littleEndian);
557 offset += 4;
558 viewU8.set(s, offset);
559 offset += s.byteLength;
560 }
561 }
562 //log("serialize() result:",viewU8.slice(0,offset));
stephan5e8bb0a2022-09-20 08:27:57 +0000563 }else{
stephanb8c8d4e2022-09-20 13:25:39 +0000564 viewU8[0] = 0;
stephan5e8bb0a2022-09-20 08:27:57 +0000565 }
stephanb8c8d4e2022-09-20 13:25:39 +0000566 metrics.s11n.serialize.time += performance.now() - t;
stephan138647a2022-09-20 03:31:02 +0000567 };
stephane8afca32022-09-21 14:02:47 +0000568
569 state.s11n.storeException = state.asyncS11nExceptions
stephan56fae742022-09-24 10:12:19 +0000570 ? ((priority,e)=>{
571 if(priority<=state.asyncS11nExceptions){
572 state.s11n.serialize(e.message);
573 }
574 })
stephane8afca32022-09-21 14:02:47 +0000575 : ()=>{};
576
stephan138647a2022-09-20 03:31:02 +0000577 return state.s11n;
stephanb8c8d4e2022-09-20 13:25:39 +0000578}/*initS11n()*/;
stephan138647a2022-09-20 03:31:02 +0000579
stephan5e8bb0a2022-09-20 08:27:57 +0000580const waitLoop = async function f(){
stephan138647a2022-09-20 03:31:02 +0000581 const opHandlers = Object.create(null);
stephanc9e26022022-09-20 10:11:52 +0000582 for(let k of Object.keys(state.opIds)){
583 const vi = vfsAsyncImpls[k];
584 if(!vi) continue;
stephan138647a2022-09-20 03:31:02 +0000585 const o = Object.create(null);
586 opHandlers[state.opIds[k]] = o;
587 o.key = k;
stephan56fae742022-09-24 10:12:19 +0000588 o.f = vi || toss("No vfsAsyncImpls[",k,"]");
stephan138647a2022-09-20 03:31:02 +0000589 }
stephan5f0b67c2022-10-03 11:33:35 +0000590 /**
591 waitTime is how long (ms) to wait for each Atomics.wait().
592 We need to wait up periodically to give the thread a chance
593 to do other things.
594 */
595 const waitTime = 250;
596 /**
597 relinquishTime defines the_approximate_ number of ms
598 after which a db sync access handle will be relinquished
599 so that we do not hold a persistent lock on it. When the following loop
600 times out while waiting, every (approximate) increment of this
601 value it will relinquish any db handles which have been idle for
602 at least this much time.
603
604 Reaquisition of a sync handle seems to take an average of
605 0.6-0.9ms on this dev machine but takes anywhere from 1-3ms
606 every once in a while (maybe 1 time in 5 or 10).
607 */
608 const relinquishTime = 1000;
609 let lastOpTime = performance.now();
610 let now;
stephan3c272ba2022-10-04 00:54:00 +0000611 while(!flagAsyncShutdown){
stephan138647a2022-09-20 03:31:02 +0000612 try {
stephan5f0b67c2022-10-03 11:33:35 +0000613 if('timed-out'===Atomics.wait(
614 state.sabOPView, state.opIds.whichOp, 0, waitTime
615 )){
616 if(relinquishTime &&
617 (lastOpTime + relinquishTime <= (now = performance.now()))){
618 for(const fh of Object.values(__openFiles)){
619 if(fh.syncHandle && (
620 now - relinquishTime >= fh.syncHandleTime
621 )){
622 //warn("Relinquishing for timeout:",fh.filenameAbs);
stephan4f5bbed2022-10-03 13:03:41 +0000623 await closeSyncHandle(fh)
624 /* Testing shows that we have to wait on this async
625 op to finish, else we might try to re-open it
626 before the close has run. The FS layer does not
627 retain the order those operations, apparently. */;
stephan5f0b67c2022-10-03 11:33:35 +0000628 }
629 }
630 }
stephan5e8bb0a2022-09-20 08:27:57 +0000631 continue;
632 }
stephan5f0b67c2022-10-03 11:33:35 +0000633 lastOpTime = performance.now();
stephan5e8bb0a2022-09-20 08:27:57 +0000634 const opId = Atomics.load(state.sabOPView, state.opIds.whichOp);
635 Atomics.store(state.sabOPView, state.opIds.whichOp, 0);
stephan138647a2022-09-20 03:31:02 +0000636 const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId);
stephan56fae742022-09-24 10:12:19 +0000637 const args = state.s11n.deserialize() || [];
stephan5f0b67c2022-10-03 11:33:35 +0000638 state.s11n.serialize(/* clear s11n to keep the caller from
639 confusing this with an exception string
640 written by the upcoming operation */);
stephan5e8bb0a2022-09-20 08:27:57 +0000641 //warn("waitLoop() whichOp =",opId, hnd, args);
642 if(hnd.f) await hnd.f(...args);
643 else error("Missing callback for opId",opId);
stephan138647a2022-09-20 03:31:02 +0000644 }catch(e){
stephan5f0b67c2022-10-03 11:33:35 +0000645 error('in waitLoop():',e);
stephan138647a2022-09-20 03:31:02 +0000646 }
stephan5e8bb0a2022-09-20 08:27:57 +0000647 };
stephan138647a2022-09-20 03:31:02 +0000648};
649
stephan07315542022-09-17 20:50:12 +0000650navigator.storage.getDirectory().then(function(d){
651 const wMsg = (type)=>postMessage({type});
652 state.rootDir = d;
stephan5e8bb0a2022-09-20 08:27:57 +0000653 self.onmessage = function({data}){
stephan07315542022-09-17 20:50:12 +0000654 switch(data.type){
stephan138647a2022-09-20 03:31:02 +0000655 case 'opfs-async-init':{
stephan07315542022-09-17 20:50:12 +0000656 /* Receive shared state from synchronous partner */
stephan138647a2022-09-20 03:31:02 +0000657 const opt = data.args;
658 state.littleEndian = opt.littleEndian;
stephane8afca32022-09-21 14:02:47 +0000659 state.asyncS11nExceptions = opt.asyncS11nExceptions;
stephan07315542022-09-17 20:50:12 +0000660 state.verbose = opt.verbose ?? 2;
661 state.fileBufferSize = opt.fileBufferSize;
stephan138647a2022-09-20 03:31:02 +0000662 state.sabS11nOffset = opt.sabS11nOffset;
663 state.sabS11nSize = opt.sabS11nSize;
stephanc4b87be2022-09-20 01:28:47 +0000664 state.sabOP = opt.sabOP;
665 state.sabOPView = new Int32Array(state.sabOP);
666 state.sabIO = opt.sabIO;
667 state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
stephan138647a2022-09-20 03:31:02 +0000668 state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize);
stephan07315542022-09-17 20:50:12 +0000669 state.opIds = opt.opIds;
670 state.sq3Codes = opt.sq3Codes;
671 Object.keys(vfsAsyncImpls).forEach((k)=>{
672 if(!Number.isFinite(state.opIds[k])){
673 toss("Maintenance required: missing state.opIds[",k,"]");
674 }
675 });
stephan138647a2022-09-20 03:31:02 +0000676 initS11n();
stephanaec046a2022-09-19 18:22:29 +0000677 metrics.reset();
stephan07315542022-09-17 20:50:12 +0000678 log("init state",state);
stephan138647a2022-09-20 03:31:02 +0000679 wMsg('opfs-async-inited');
stephan5e8bb0a2022-09-20 08:27:57 +0000680 waitLoop();
stephan07315542022-09-17 20:50:12 +0000681 break;
682 }
stephan3c272ba2022-10-04 00:54:00 +0000683 case 'opfs-async-restart':
684 if(flagAsyncShutdown){
685 warn("Restarting after opfs-async-shutdown. Might or might not work.");
686 flagAsyncShutdown = false;
687 waitLoop();
688 }
689 break;
690 case 'opfs-async-metrics':
691 metrics.dump();
692 break;
stephan132a87b2022-09-17 15:08:22 +0000693 }
694 };
stephan138647a2022-09-20 03:31:02 +0000695 wMsg('opfs-async-loaded');
stephan8200a6d2022-09-17 23:29:27 +0000696}).catch((e)=>error(e));