blob: fb472488bf71f3a8a7dc9a984a1a4271119edc4a [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}
37/**
38 Will hold state copied to this object from the syncronous side of
39 this API.
40*/
41const state = Object.create(null);
42/**
43 verbose:
44
45 0 = no logging output
46 1 = only errors
47 2 = warnings and errors
48 3 = debug, warnings, and errors
49*/
50state.verbose = 2;
51
stephan509f4052022-09-19 09:58:01 +000052const loggers = {
53 0:console.error.bind(console),
54 1:console.warn.bind(console),
55 2:console.log.bind(console)
stephan07315542022-09-17 20:50:12 +000056};
stephan509f4052022-09-19 09:58:01 +000057const logImpl = (level,...args)=>{
58 if(state.verbose>level) loggers[level]("OPFS asyncer:",...args);
stephan07315542022-09-17 20:50:12 +000059};
stephan509f4052022-09-19 09:58:01 +000060const log = (...args)=>logImpl(2, ...args);
61const warn = (...args)=>logImpl(1, ...args);
62const error = (...args)=>logImpl(0, ...args);
stephanf8150112022-09-19 17:09:09 +000063const metrics = Object.create(null);
stephanaec046a2022-09-19 18:22:29 +000064metrics.reset = ()=>{
65 let k;
66 const r = (m)=>(m.count = m.time = 0);
67 for(k in state.opIds){
68 r(metrics[k] = Object.create(null));
69 }
70};
71metrics.dump = ()=>{
72 let k, n = 0, t = 0, w = 0;
73 for(k in state.opIds){
74 const m = metrics[k];
75 n += m.count;
76 t += m.time;
77 m.avgTime = (m.count && m.time) ? (m.time / m.count) : 0;
78 }
79 console.log(self.location.href,
80 "metrics for",self.location.href,":",metrics,
81 "\nTotal of",n,"op(s) for",t,"ms");
82};
stephan07315542022-09-17 20:50:12 +000083
stephan509f4052022-09-19 09:58:01 +000084warn("This file is very much experimental and under construction.",
85 self.location.pathname);
stephan07315542022-09-17 20:50:12 +000086
87/**
88 Map of sqlite3_file pointers (integers) to metadata related to a
89 given OPFS file handles. The pointers are, in this side of the
90 interface, opaque file handle IDs provided by the synchronous
91 part of this constellation. Each value is an object with a structure
92 demonstrated in the xOpen() impl.
93*/
94const __openFiles = Object.create(null);
95
96/**
stephan8200a6d2022-09-17 23:29:27 +000097 Expects an OPFS file path. It gets resolved, such that ".."
98 components are properly expanded, and returned. If the 2nd
99 are is true, it's returned as an array of path elements,
100 else it's returned as an absolute path string.
stephan07315542022-09-17 20:50:12 +0000101*/
stephan8200a6d2022-09-17 23:29:27 +0000102const getResolvedPath = function(filename,splitIt){
103 const p = new URL(
104 filename, 'file://irrelevant'
105 ).pathname;
106 return splitIt ? p.split('/').filter((v)=>!!v) : p;
stephan509f4052022-09-19 09:58:01 +0000107};
stephan07315542022-09-17 20:50:12 +0000108
109/**
110 Takes the absolute path to a filesystem element. Returns an array
111 of [handleOfContainingDir, filename]. If the 2nd argument is
112 truthy then each directory element leading to the file is created
113 along the way. Throws if any creation or resolution fails.
114*/
115const getDirForPath = async function f(absFilename, createDirs = false){
stephan8200a6d2022-09-17 23:29:27 +0000116 const path = getResolvedPath(absFilename, true);
stephan07315542022-09-17 20:50:12 +0000117 const filename = path.pop();
stephan8200a6d2022-09-17 23:29:27 +0000118 let dh = state.rootDir;
119 for(const dirName of path){
120 if(dirName){
121 dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs});
stephan07315542022-09-17 20:50:12 +0000122 }
stephan132a87b2022-09-17 15:08:22 +0000123 }
stephan07315542022-09-17 20:50:12 +0000124 return [dh, filename];
125};
stephan132a87b2022-09-17 15:08:22 +0000126
stephan132a87b2022-09-17 15:08:22 +0000127
stephan07315542022-09-17 20:50:12 +0000128/**
129 Stores the given value at the array index reserved for the given op
130 and then Atomics.notify()'s it.
131*/
132const storeAndNotify = (opName, value)=>{
133 log(opName+"() is notify()ing w/ value:",value);
stephane6f8a092022-09-17 21:13:26 +0000134 Atomics.store(state.opSABView, state.opIds[opName], value);
135 Atomics.notify(state.opSABView, state.opIds[opName]);
stephan07315542022-09-17 20:50:12 +0000136};
stephan132a87b2022-09-17 15:08:22 +0000137
stephan07315542022-09-17 20:50:12 +0000138/**
139 Throws if fh is a file-holding object which is flagged as read-only.
140*/
141const affirmNotRO = function(opName,fh){
142 if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs);
143};
144
stephanaec046a2022-09-19 18:22:29 +0000145
146const opTimer = Object.create(null);
147opTimer.op = undefined;
148opTimer.start = undefined;
149const mTimeStart = (op)=>{
150 opTimer.start = performance.now();
151 opTimer.op = op;
152 //metrics[op] || toss("Maintenance required: missing metrics for",op);
153 ++metrics[op].count;
154};
155const mTimeEnd = ()=>(
156 metrics[opTimer.op].time += performance.now() - opTimer.start
157);
158
stephan07315542022-09-17 20:50:12 +0000159/**
160 Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods
161 methods. Maintenance reminder: members are in alphabetical order
162 to simplify finding them.
163*/
164const vfsAsyncImpls = {
stephanaec046a2022-09-19 18:22:29 +0000165 mkdir: async function(dirname){
166 let rc = 0;
167 try {
168 await getDirForPath(dirname+"/filepart", true);
169 }catch(e){
170 //error("mkdir failed",filename, e.message);
171 rc = state.sq3Codes.SQLITE_IOERR;
172 }
173 storeAndNotify('mkdir', rc);
174 },
stephan8200a6d2022-09-17 23:29:27 +0000175 xAccess: async function(filename){
176 log("xAccess(",arguments[0],")");
stephanaec046a2022-09-19 18:22:29 +0000177 mTimeStart('xAccess');
stephan8200a6d2022-09-17 23:29:27 +0000178 /* OPFS cannot support the full range of xAccess() queries sqlite3
179 calls for. We can essentially just tell if the file is
180 accessible, but if it is it's automatically writable (unless
181 it's locked, which we cannot(?) know without trying to open
182 it). OPFS does not have the notion of read-only.
183
184 The return semantics of this function differ from sqlite3's
185 xAccess semantics because we are limited in what we can
186 communicate back to our synchronous communication partner: 0 =
187 accessible, non-0 means not accessible.
188 */
189 let rc = 0;
190 try{
191 const [dh, fn] = await getDirForPath(filename);
192 await dh.getFileHandle(fn);
193 }catch(e){
194 rc = state.sq3Codes.SQLITE_IOERR;
195 }
stephan07315542022-09-17 20:50:12 +0000196 storeAndNotify('xAccess', rc);
stephanaec046a2022-09-19 18:22:29 +0000197 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000198 },
199 xClose: async function(fid){
200 const opName = 'xClose';
stephanaec046a2022-09-19 18:22:29 +0000201 mTimeStart(opName);
stephan07315542022-09-17 20:50:12 +0000202 log(opName+"(",arguments[0],")");
203 const fh = __openFiles[fid];
204 if(fh){
205 delete __openFiles[fid];
206 if(fh.accessHandle) await fh.accessHandle.close();
207 if(fh.deleteOnClose){
208 try{ await fh.dirHandle.removeEntry(fh.filenamePart) }
209 catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) }
stephan132a87b2022-09-17 15:08:22 +0000210 }
stephan07315542022-09-17 20:50:12 +0000211 storeAndNotify(opName, 0);
212 }else{
213 storeAndNotify(opName, state.sq3Codes.SQLITE_NOFOUND);
stephan132a87b2022-09-17 15:08:22 +0000214 }
stephanaec046a2022-09-19 18:22:29 +0000215 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000216 },
stephanf3860122022-09-18 17:32:35 +0000217 xDeleteNoWait: async function({filename, syncDir, recursive = false}){
stephan8200a6d2022-09-17 23:29:27 +0000218 /* The syncDir flag is, for purposes of the VFS API's semantics,
219 ignored here. However, if it has the value 0x1234 then: after
220 deleting the given file, recursively try to delete any empty
221 directories left behind in its wake (ignoring any errors and
222 stopping at the first failure).
223
224 That said: we don't know for sure that removeEntry() fails if
225 the dir is not empty because the API is not documented. It has,
226 however, a "recursive" flag which defaults to false, so
227 presumably it will fail if the dir is not empty and that flag
228 is false.
229 */
stephan07315542022-09-17 20:50:12 +0000230 log("xDelete(",arguments[0],")");
stephanf3860122022-09-18 17:32:35 +0000231 let rc = 0;
stephan07315542022-09-17 20:50:12 +0000232 try {
stephan8200a6d2022-09-17 23:29:27 +0000233 while(filename){
234 const [hDir, filenamePart] = await getDirForPath(filename, false);
235 //log("Removing:",hDir, filenamePart);
236 if(!filenamePart) break;
stephanf3860122022-09-18 17:32:35 +0000237 await hDir.removeEntry(filenamePart, {recursive});
stephan8200a6d2022-09-17 23:29:27 +0000238 if(0x1234 !== syncDir) break;
239 filename = getResolvedPath(filename, true);
240 filename.pop();
241 filename = filename.join('/');
242 }
stephan07315542022-09-17 20:50:12 +0000243 }catch(e){
stephan8200a6d2022-09-17 23:29:27 +0000244 /* Ignoring: _presumably_ the file can't be found or a dir is
245 not empty. */
246 //error("Delete failed",filename, e.message);
stephanf3860122022-09-18 17:32:35 +0000247 rc = state.sq3Codes.SQLITE_IOERR_DELETE;
stephan132a87b2022-09-17 15:08:22 +0000248 }
stephanf3860122022-09-18 17:32:35 +0000249 return rc;
250 },
251 xDelete: async function(...args){
stephanaec046a2022-09-19 18:22:29 +0000252 mTimeStart('xDelete');
stephanf3860122022-09-18 17:32:35 +0000253 const rc = await vfsAsyncImpls.xDeleteNoWait(...args);
254 storeAndNotify('xDelete', rc);
stephanaec046a2022-09-19 18:22:29 +0000255 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000256 },
257 xFileSize: async function(fid){
stephanaec046a2022-09-19 18:22:29 +0000258 mTimeStart('xFileSize');
stephan07315542022-09-17 20:50:12 +0000259 log("xFileSize(",arguments,")");
260 const fh = __openFiles[fid];
261 let sz;
262 try{
263 sz = await fh.accessHandle.getSize();
stephanf8150112022-09-19 17:09:09 +0000264 fh.sabViewFileSize.setBigInt64(0, BigInt(sz), true);
stephan07315542022-09-17 20:50:12 +0000265 sz = 0;
266 }catch(e){
267 error("xFileSize():",e, fh);
268 sz = state.sq3Codes.SQLITE_IOERR;
stephan132a87b2022-09-17 15:08:22 +0000269 }
stephan07315542022-09-17 20:50:12 +0000270 storeAndNotify('xFileSize', sz);
stephanaec046a2022-09-19 18:22:29 +0000271 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000272 },
273 xOpen: async function({
274 fid/*sqlite3_file pointer*/,
275 sab/*file-specific SharedArrayBuffer*/,
276 filename,
277 fileType = undefined /*mainDb, mainJournal, etc.*/,
278 create = false, readOnly = false, deleteOnClose = false
279 }){
280 const opName = 'xOpen';
stephanaec046a2022-09-19 18:22:29 +0000281 mTimeStart(opName);
stephan07315542022-09-17 20:50:12 +0000282 try{
283 if(create) readOnly = false;
stephan132a87b2022-09-17 15:08:22 +0000284 log(opName+"(",arguments[0],")");
stephan07315542022-09-17 20:50:12 +0000285 let hDir, filenamePart;
286 try {
287 [hDir, filenamePart] = await getDirForPath(filename, !!create);
stephan132a87b2022-09-17 15:08:22 +0000288 }catch(e){
stephan07315542022-09-17 20:50:12 +0000289 storeAndNotify(opName, state.sql3Codes.SQLITE_NOTFOUND);
stephanaec046a2022-09-19 18:22:29 +0000290 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000291 return;
stephan132a87b2022-09-17 15:08:22 +0000292 }
stephan07315542022-09-17 20:50:12 +0000293 const hFile = await hDir.getFileHandle(filenamePart, {create: !!create});
294 log(opName,"filenamePart =",filenamePart, 'hDir =',hDir);
295 const fobj = __openFiles[fid] = Object.create(null);
296 fobj.filenameAbs = filename;
297 fobj.filenamePart = filenamePart;
298 fobj.dirHandle = hDir;
299 fobj.fileHandle = hFile;
300 fobj.fileType = fileType;
301 fobj.sab = sab;
stephanaec046a2022-09-19 18:22:29 +0000302 fobj.sabView = new Uint8Array(sab,0,state.fbInt64Offset);
stephan07315542022-09-17 20:50:12 +0000303 fobj.sabViewFileSize = new DataView(sab,state.fbInt64Offset,8);
304 fobj.create = !!create;
305 fobj.readOnly = !!readOnly;
306 fobj.deleteOnClose = !!deleteOnClose;
307 /**
308 wa-sqlite, at this point, grabs a SyncAccessHandle and
309 assigns it to the accessHandle prop of the file state
310 object, but only for certain cases and it's unclear why it
311 places that limitation on it.
312 */
313 fobj.accessHandle = await hFile.createSyncAccessHandle();
314 storeAndNotify(opName, 0);
315 }catch(e){
316 error(opName,e);
317 storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
318 }
stephanaec046a2022-09-19 18:22:29 +0000319 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000320 },
321 xRead: async function({fid,n,offset}){
stephanaec046a2022-09-19 18:22:29 +0000322 mTimeStart('xRead');
stephan07315542022-09-17 20:50:12 +0000323 log("xRead(",arguments[0],")");
324 let rc = 0;
stephan07315542022-09-17 20:50:12 +0000325 try{
stephanaec046a2022-09-19 18:22:29 +0000326 const fh = __openFiles[fid];
327 const nRead = fh.accessHandle.read(
328 fh.sabView.subarray(0, n),
329 {at: Number(offset)}
330 );
stephan07315542022-09-17 20:50:12 +0000331 if(nRead < n){/* Zero-fill remaining bytes */
stephanaec046a2022-09-19 18:22:29 +0000332 fh.sabView.fill(0, nRead, n);
stephan07315542022-09-17 20:50:12 +0000333 rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
334 }
335 }catch(e){
336 error("xRead() failed",e,fh);
337 rc = state.sq3Codes.SQLITE_IOERR_READ;
338 }
339 storeAndNotify('xRead',rc);
stephanaec046a2022-09-19 18:22:29 +0000340 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000341 },
stephan07315542022-09-17 20:50:12 +0000342 xSync: async function({fid,flags/*ignored*/}){
stephanaec046a2022-09-19 18:22:29 +0000343 mTimeStart('xSync');
stephan07315542022-09-17 20:50:12 +0000344 log("xSync(",arguments[0],")");
345 const fh = __openFiles[fid];
346 if(!fh.readOnly && fh.accessHandle) await fh.accessHandle.flush();
347 storeAndNotify('xSync',0);
stephanaec046a2022-09-19 18:22:29 +0000348 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000349 },
350 xTruncate: async function({fid,size}){
stephanaec046a2022-09-19 18:22:29 +0000351 mTimeStart('xTruncate');
stephan07315542022-09-17 20:50:12 +0000352 log("xTruncate(",arguments[0],")");
353 let rc = 0;
354 const fh = __openFiles[fid];
355 try{
356 affirmNotRO('xTruncate', fh);
stephan61418d52022-09-19 14:56:13 +0000357 await fh.accessHandle.truncate(Number(size));
stephan07315542022-09-17 20:50:12 +0000358 }catch(e){
359 error("xTruncate():",e,fh);
360 rc = state.sq3Codes.SQLITE_IOERR_TRUNCATE;
361 }
362 storeAndNotify('xTruncate',rc);
stephanaec046a2022-09-19 18:22:29 +0000363 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000364 },
stephanaec046a2022-09-19 18:22:29 +0000365 xWrite: async function({fid,n,offset}){
366 mTimeStart('xWrite');
stephan07315542022-09-17 20:50:12 +0000367 log("xWrite(",arguments[0],")");
368 let rc;
369 try{
stephanaec046a2022-09-19 18:22:29 +0000370 const fh = __openFiles[fid];
stephan07315542022-09-17 20:50:12 +0000371 affirmNotRO('xWrite', fh);
stephanaec046a2022-09-19 18:22:29 +0000372 rc = (
373 n === fh.accessHandle.write(fh.sabView.subarray(0, n),
374 {at: Number(offset)})
375 ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
stephan07315542022-09-17 20:50:12 +0000376 }catch(e){
377 error("xWrite():",e,fh);
378 rc = state.sq3Codes.SQLITE_IOERR_WRITE;
379 }
380 storeAndNotify('xWrite',rc);
stephanaec046a2022-09-19 18:22:29 +0000381 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000382 }
383};
384
385navigator.storage.getDirectory().then(function(d){
386 const wMsg = (type)=>postMessage({type});
387 state.rootDir = d;
388 log("state.rootDir =",state.rootDir);
389 self.onmessage = async function({data}){
390 log("self.onmessage()",data);
391 switch(data.type){
392 case 'init':{
393 /* Receive shared state from synchronous partner */
394 const opt = data.payload;
395 state.verbose = opt.verbose ?? 2;
396 state.fileBufferSize = opt.fileBufferSize;
397 state.fbInt64Offset = opt.fbInt64Offset;
stephane6f8a092022-09-17 21:13:26 +0000398 state.opSAB = opt.opSAB;
399 state.opSABView = new Int32Array(state.opSAB);
stephan07315542022-09-17 20:50:12 +0000400 state.opIds = opt.opIds;
401 state.sq3Codes = opt.sq3Codes;
402 Object.keys(vfsAsyncImpls).forEach((k)=>{
403 if(!Number.isFinite(state.opIds[k])){
404 toss("Maintenance required: missing state.opIds[",k,"]");
405 }
406 });
stephanaec046a2022-09-19 18:22:29 +0000407 metrics.reset();
stephan07315542022-09-17 20:50:12 +0000408 log("init state",state);
409 wMsg('inited');
410 break;
411 }
412 default:{
413 let err;
414 const m = vfsAsyncImpls[data.type] || toss("Unknown message type:",data.type);
415 try {
416 await m(data.payload).catch((e)=>err=e);
417 }catch(e){
418 err = e;
419 }
420 if(err){
421 error("Error handling",data.type+"():",e);
422 storeAndNotify(data.type, state.sq3Codes.SQLITE_ERROR);
423 }
424 break;
425 }
stephan132a87b2022-09-17 15:08:22 +0000426 }
427 };
stephan07315542022-09-17 20:50:12 +0000428 wMsg('loaded');
stephan8200a6d2022-09-17 23:29:27 +0000429}).catch((e)=>error(e));