blob: a1ed4362d3abcdc7b5215387e490767fecfc9ffa [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);
stephanc4b87be2022-09-20 01:28:47 +0000134 Atomics.store(state.sabOPView, state.opIds[opName], value);
135 Atomics.notify(state.sabOPView, 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 },
stephanc4b87be2022-09-20 01:28:47 +0000217 xDelete: async function(...args){
218 mTimeStart('xDelete');
219 const rc = await vfsAsyncImpls.xDeleteNoWait(...args);
220 storeAndNotify('xDelete', rc);
221 mTimeEnd();
222 },
stephanf3860122022-09-18 17:32:35 +0000223 xDeleteNoWait: async function({filename, syncDir, recursive = false}){
stephan8200a6d2022-09-17 23:29:27 +0000224 /* The syncDir flag is, for purposes of the VFS API's semantics,
225 ignored here. However, if it has the value 0x1234 then: after
226 deleting the given file, recursively try to delete any empty
227 directories left behind in its wake (ignoring any errors and
228 stopping at the first failure).
229
230 That said: we don't know for sure that removeEntry() fails if
231 the dir is not empty because the API is not documented. It has,
232 however, a "recursive" flag which defaults to false, so
233 presumably it will fail if the dir is not empty and that flag
234 is false.
235 */
stephan07315542022-09-17 20:50:12 +0000236 log("xDelete(",arguments[0],")");
stephanf3860122022-09-18 17:32:35 +0000237 let rc = 0;
stephan07315542022-09-17 20:50:12 +0000238 try {
stephan8200a6d2022-09-17 23:29:27 +0000239 while(filename){
240 const [hDir, filenamePart] = await getDirForPath(filename, false);
241 //log("Removing:",hDir, filenamePart);
242 if(!filenamePart) break;
stephanf3860122022-09-18 17:32:35 +0000243 await hDir.removeEntry(filenamePart, {recursive});
stephan8200a6d2022-09-17 23:29:27 +0000244 if(0x1234 !== syncDir) break;
245 filename = getResolvedPath(filename, true);
246 filename.pop();
247 filename = filename.join('/');
248 }
stephan07315542022-09-17 20:50:12 +0000249 }catch(e){
stephan8200a6d2022-09-17 23:29:27 +0000250 /* Ignoring: _presumably_ the file can't be found or a dir is
251 not empty. */
252 //error("Delete failed",filename, e.message);
stephanf3860122022-09-18 17:32:35 +0000253 rc = state.sq3Codes.SQLITE_IOERR_DELETE;
stephan132a87b2022-09-17 15:08:22 +0000254 }
stephanf3860122022-09-18 17:32:35 +0000255 return rc;
256 },
stephan07315542022-09-17 20:50:12 +0000257 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();
stephanc4b87be2022-09-20 01:28:47 +0000264 if(!fh.sabViewFileSize){
265 fh.sabViewFileSize = new DataView(state.sabIO,state.fbInt64Offset,8);
266 }
stephanf8150112022-09-19 17:09:09 +0000267 fh.sabViewFileSize.setBigInt64(0, BigInt(sz), true);
stephan07315542022-09-17 20:50:12 +0000268 sz = 0;
269 }catch(e){
270 error("xFileSize():",e, fh);
271 sz = state.sq3Codes.SQLITE_IOERR;
stephan132a87b2022-09-17 15:08:22 +0000272 }
stephan07315542022-09-17 20:50:12 +0000273 storeAndNotify('xFileSize', sz);
stephanaec046a2022-09-19 18:22:29 +0000274 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000275 },
276 xOpen: async function({
277 fid/*sqlite3_file pointer*/,
stephan07315542022-09-17 20:50:12 +0000278 filename,
stephanc4b87be2022-09-20 01:28:47 +0000279 flags
stephan07315542022-09-17 20:50:12 +0000280 }){
281 const opName = 'xOpen';
stephanaec046a2022-09-19 18:22:29 +0000282 mTimeStart(opName);
stephanc4b87be2022-09-20 01:28:47 +0000283 log(opName+"(",arguments[0],")");
284 const deleteOnClose = (state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags);
285 const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags);
stephan07315542022-09-17 20:50:12 +0000286 try{
stephan07315542022-09-17 20:50:12 +0000287 let hDir, filenamePart;
288 try {
289 [hDir, filenamePart] = await getDirForPath(filename, !!create);
stephan132a87b2022-09-17 15:08:22 +0000290 }catch(e){
stephan07315542022-09-17 20:50:12 +0000291 storeAndNotify(opName, state.sql3Codes.SQLITE_NOTFOUND);
stephanaec046a2022-09-19 18:22:29 +0000292 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000293 return;
stephan132a87b2022-09-17 15:08:22 +0000294 }
stephanc4b87be2022-09-20 01:28:47 +0000295 const hFile = await hDir.getFileHandle(filenamePart, {create});
296 const fobj = Object.create(null);
stephan07315542022-09-17 20:50:12 +0000297 /**
298 wa-sqlite, at this point, grabs a SyncAccessHandle and
299 assigns it to the accessHandle prop of the file state
300 object, but only for certain cases and it's unclear why it
301 places that limitation on it.
302 */
303 fobj.accessHandle = await hFile.createSyncAccessHandle();
stephanc4b87be2022-09-20 01:28:47 +0000304 __openFiles[fid] = fobj;
305 fobj.filenameAbs = filename;
306 fobj.filenamePart = filenamePart;
307 fobj.dirHandle = hDir;
308 fobj.fileHandle = hFile;
309 fobj.sabView = state.sabFileBufView;
310 fobj.readOnly = create ? false : (state.sq3Codes.SQLITE_OPEN_READONLY & flags);
311 fobj.deleteOnClose = deleteOnClose;
stephan07315542022-09-17 20:50:12 +0000312 storeAndNotify(opName, 0);
313 }catch(e){
314 error(opName,e);
315 storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
316 }
stephanaec046a2022-09-19 18:22:29 +0000317 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000318 },
319 xRead: async function({fid,n,offset}){
stephanaec046a2022-09-19 18:22:29 +0000320 mTimeStart('xRead');
stephan07315542022-09-17 20:50:12 +0000321 log("xRead(",arguments[0],")");
322 let rc = 0;
stephan07315542022-09-17 20:50:12 +0000323 try{
stephanaec046a2022-09-19 18:22:29 +0000324 const fh = __openFiles[fid];
325 const nRead = fh.accessHandle.read(
326 fh.sabView.subarray(0, n),
327 {at: Number(offset)}
328 );
stephan07315542022-09-17 20:50:12 +0000329 if(nRead < n){/* Zero-fill remaining bytes */
stephanaec046a2022-09-19 18:22:29 +0000330 fh.sabView.fill(0, nRead, n);
stephan07315542022-09-17 20:50:12 +0000331 rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
332 }
333 }catch(e){
334 error("xRead() failed",e,fh);
335 rc = state.sq3Codes.SQLITE_IOERR_READ;
336 }
337 storeAndNotify('xRead',rc);
stephanaec046a2022-09-19 18:22:29 +0000338 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000339 },
stephan07315542022-09-17 20:50:12 +0000340 xSync: async function({fid,flags/*ignored*/}){
stephanaec046a2022-09-19 18:22:29 +0000341 mTimeStart('xSync');
stephan07315542022-09-17 20:50:12 +0000342 log("xSync(",arguments[0],")");
343 const fh = __openFiles[fid];
344 if(!fh.readOnly && fh.accessHandle) await fh.accessHandle.flush();
345 storeAndNotify('xSync',0);
stephanaec046a2022-09-19 18:22:29 +0000346 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000347 },
348 xTruncate: async function({fid,size}){
stephanaec046a2022-09-19 18:22:29 +0000349 mTimeStart('xTruncate');
stephan07315542022-09-17 20:50:12 +0000350 log("xTruncate(",arguments[0],")");
351 let rc = 0;
352 const fh = __openFiles[fid];
353 try{
354 affirmNotRO('xTruncate', fh);
stephan61418d52022-09-19 14:56:13 +0000355 await fh.accessHandle.truncate(Number(size));
stephan07315542022-09-17 20:50:12 +0000356 }catch(e){
357 error("xTruncate():",e,fh);
358 rc = state.sq3Codes.SQLITE_IOERR_TRUNCATE;
359 }
360 storeAndNotify('xTruncate',rc);
stephanaec046a2022-09-19 18:22:29 +0000361 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000362 },
stephanaec046a2022-09-19 18:22:29 +0000363 xWrite: async function({fid,n,offset}){
364 mTimeStart('xWrite');
stephan07315542022-09-17 20:50:12 +0000365 log("xWrite(",arguments[0],")");
366 let rc;
367 try{
stephanaec046a2022-09-19 18:22:29 +0000368 const fh = __openFiles[fid];
stephan07315542022-09-17 20:50:12 +0000369 affirmNotRO('xWrite', fh);
stephanaec046a2022-09-19 18:22:29 +0000370 rc = (
371 n === fh.accessHandle.write(fh.sabView.subarray(0, n),
372 {at: Number(offset)})
373 ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
stephan07315542022-09-17 20:50:12 +0000374 }catch(e){
375 error("xWrite():",e,fh);
376 rc = state.sq3Codes.SQLITE_IOERR_WRITE;
377 }
378 storeAndNotify('xWrite',rc);
stephanaec046a2022-09-19 18:22:29 +0000379 mTimeEnd();
stephan07315542022-09-17 20:50:12 +0000380 }
381};
382
383navigator.storage.getDirectory().then(function(d){
384 const wMsg = (type)=>postMessage({type});
385 state.rootDir = d;
386 log("state.rootDir =",state.rootDir);
387 self.onmessage = async function({data}){
388 log("self.onmessage()",data);
389 switch(data.type){
390 case 'init':{
391 /* Receive shared state from synchronous partner */
392 const opt = data.payload;
393 state.verbose = opt.verbose ?? 2;
394 state.fileBufferSize = opt.fileBufferSize;
395 state.fbInt64Offset = opt.fbInt64Offset;
stephanc4b87be2022-09-20 01:28:47 +0000396 state.sabOP = opt.sabOP;
397 state.sabOPView = new Int32Array(state.sabOP);
398 state.sabIO = opt.sabIO;
399 state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize);
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));