blob: 4a60e8aa1a5b0a586106e7da344e27f562864c66 [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
52const __logPrefix = "OPFS asyncer:";
53const log = (...args)=>{
54 if(state.verbose>2) console.log(__logPrefix,...args);
55};
56const warn = (...args)=>{
57 if(state.verbose>1) console.warn(__logPrefix,...args);
58};
59const error = (...args)=>{
60 if(state.verbose) console.error(__logPrefix,...args);
61};
62
63warn("This file is very much experimental and under construction.",self.location.pathname);
64
65/**
66 Map of sqlite3_file pointers (integers) to metadata related to a
67 given OPFS file handles. The pointers are, in this side of the
68 interface, opaque file handle IDs provided by the synchronous
69 part of this constellation. Each value is an object with a structure
70 demonstrated in the xOpen() impl.
71*/
72const __openFiles = Object.create(null);
73
74/**
stephan8200a6d2022-09-17 23:29:27 +000075 Expects an OPFS file path. It gets resolved, such that ".."
76 components are properly expanded, and returned. If the 2nd
77 are is true, it's returned as an array of path elements,
78 else it's returned as an absolute path string.
stephan07315542022-09-17 20:50:12 +000079*/
stephan8200a6d2022-09-17 23:29:27 +000080const getResolvedPath = function(filename,splitIt){
81 const p = new URL(
82 filename, 'file://irrelevant'
83 ).pathname;
84 return splitIt ? p.split('/').filter((v)=>!!v) : p;
85}
stephan07315542022-09-17 20:50:12 +000086
87/**
88 Takes the absolute path to a filesystem element. Returns an array
89 of [handleOfContainingDir, filename]. If the 2nd argument is
90 truthy then each directory element leading to the file is created
91 along the way. Throws if any creation or resolution fails.
92*/
93const getDirForPath = async function f(absFilename, createDirs = false){
stephan8200a6d2022-09-17 23:29:27 +000094 const path = getResolvedPath(absFilename, true);
stephan07315542022-09-17 20:50:12 +000095 const filename = path.pop();
stephan8200a6d2022-09-17 23:29:27 +000096 let dh = state.rootDir;
97 for(const dirName of path){
98 if(dirName){
99 dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs});
stephan07315542022-09-17 20:50:12 +0000100 }
stephan132a87b2022-09-17 15:08:22 +0000101 }
stephan07315542022-09-17 20:50:12 +0000102 return [dh, filename];
103};
stephan132a87b2022-09-17 15:08:22 +0000104
stephan132a87b2022-09-17 15:08:22 +0000105
stephan07315542022-09-17 20:50:12 +0000106/**
107 Stores the given value at the array index reserved for the given op
108 and then Atomics.notify()'s it.
109*/
110const storeAndNotify = (opName, value)=>{
111 log(opName+"() is notify()ing w/ value:",value);
stephane6f8a092022-09-17 21:13:26 +0000112 Atomics.store(state.opSABView, state.opIds[opName], value);
113 Atomics.notify(state.opSABView, state.opIds[opName]);
stephan07315542022-09-17 20:50:12 +0000114};
stephan132a87b2022-09-17 15:08:22 +0000115
stephan07315542022-09-17 20:50:12 +0000116/**
117 Throws if fh is a file-holding object which is flagged as read-only.
118*/
119const affirmNotRO = function(opName,fh){
120 if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs);
121};
122
123/**
124 Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods
125 methods. Maintenance reminder: members are in alphabetical order
126 to simplify finding them.
127*/
128const vfsAsyncImpls = {
stephan8200a6d2022-09-17 23:29:27 +0000129 xAccess: async function(filename){
130 log("xAccess(",arguments[0],")");
131 /* OPFS cannot support the full range of xAccess() queries sqlite3
132 calls for. We can essentially just tell if the file is
133 accessible, but if it is it's automatically writable (unless
134 it's locked, which we cannot(?) know without trying to open
135 it). OPFS does not have the notion of read-only.
136
137 The return semantics of this function differ from sqlite3's
138 xAccess semantics because we are limited in what we can
139 communicate back to our synchronous communication partner: 0 =
140 accessible, non-0 means not accessible.
141 */
142 let rc = 0;
143 try{
144 const [dh, fn] = await getDirForPath(filename);
145 await dh.getFileHandle(fn);
146 }catch(e){
147 rc = state.sq3Codes.SQLITE_IOERR;
148 }
stephan07315542022-09-17 20:50:12 +0000149 storeAndNotify('xAccess', rc);
150 },
151 xClose: async function(fid){
152 const opName = 'xClose';
153 log(opName+"(",arguments[0],")");
154 const fh = __openFiles[fid];
155 if(fh){
156 delete __openFiles[fid];
157 if(fh.accessHandle) await fh.accessHandle.close();
158 if(fh.deleteOnClose){
159 try{ await fh.dirHandle.removeEntry(fh.filenamePart) }
160 catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) }
stephan132a87b2022-09-17 15:08:22 +0000161 }
stephan07315542022-09-17 20:50:12 +0000162 storeAndNotify(opName, 0);
163 }else{
164 storeAndNotify(opName, state.sq3Codes.SQLITE_NOFOUND);
stephan132a87b2022-09-17 15:08:22 +0000165 }
stephan07315542022-09-17 20:50:12 +0000166 },
167 xDelete: async function({filename, syncDir/*ignored*/}){
stephan8200a6d2022-09-17 23:29:27 +0000168 /* The syncDir flag is, for purposes of the VFS API's semantics,
169 ignored here. However, if it has the value 0x1234 then: after
170 deleting the given file, recursively try to delete any empty
171 directories left behind in its wake (ignoring any errors and
172 stopping at the first failure).
173
174 That said: we don't know for sure that removeEntry() fails if
175 the dir is not empty because the API is not documented. It has,
176 however, a "recursive" flag which defaults to false, so
177 presumably it will fail if the dir is not empty and that flag
178 is false.
179 */
stephan07315542022-09-17 20:50:12 +0000180 log("xDelete(",arguments[0],")");
181 try {
stephan8200a6d2022-09-17 23:29:27 +0000182 while(filename){
183 const [hDir, filenamePart] = await getDirForPath(filename, false);
184 //log("Removing:",hDir, filenamePart);
185 if(!filenamePart) break;
186 await hDir.removeEntry(filenamePart);
187 if(0x1234 !== syncDir) break;
188 filename = getResolvedPath(filename, true);
189 filename.pop();
190 filename = filename.join('/');
191 }
stephan07315542022-09-17 20:50:12 +0000192 }catch(e){
stephan8200a6d2022-09-17 23:29:27 +0000193 /* Ignoring: _presumably_ the file can't be found or a dir is
194 not empty. */
195 //error("Delete failed",filename, e.message);
stephan132a87b2022-09-17 15:08:22 +0000196 }
stephan07315542022-09-17 20:50:12 +0000197 storeAndNotify('xDelete', 0);
198 },
199 xFileSize: async function(fid){
200 log("xFileSize(",arguments,")");
201 const fh = __openFiles[fid];
202 let sz;
203 try{
204 sz = await fh.accessHandle.getSize();
205 fh.sabViewFileSize.setBigInt64(0, BigInt(sz));
206 sz = 0;
207 }catch(e){
208 error("xFileSize():",e, fh);
209 sz = state.sq3Codes.SQLITE_IOERR;
stephan132a87b2022-09-17 15:08:22 +0000210 }
stephan07315542022-09-17 20:50:12 +0000211 storeAndNotify('xFileSize', sz);
212 },
213 xOpen: async function({
214 fid/*sqlite3_file pointer*/,
215 sab/*file-specific SharedArrayBuffer*/,
216 filename,
217 fileType = undefined /*mainDb, mainJournal, etc.*/,
218 create = false, readOnly = false, deleteOnClose = false
219 }){
220 const opName = 'xOpen';
221 try{
222 if(create) readOnly = false;
stephan132a87b2022-09-17 15:08:22 +0000223 log(opName+"(",arguments[0],")");
stephan07315542022-09-17 20:50:12 +0000224 let hDir, filenamePart;
225 try {
226 [hDir, filenamePart] = await getDirForPath(filename, !!create);
stephan132a87b2022-09-17 15:08:22 +0000227 }catch(e){
stephan07315542022-09-17 20:50:12 +0000228 storeAndNotify(opName, state.sql3Codes.SQLITE_NOTFOUND);
229 return;
stephan132a87b2022-09-17 15:08:22 +0000230 }
stephan07315542022-09-17 20:50:12 +0000231 const hFile = await hDir.getFileHandle(filenamePart, {create: !!create});
232 log(opName,"filenamePart =",filenamePart, 'hDir =',hDir);
233 const fobj = __openFiles[fid] = Object.create(null);
234 fobj.filenameAbs = filename;
235 fobj.filenamePart = filenamePart;
236 fobj.dirHandle = hDir;
237 fobj.fileHandle = hFile;
238 fobj.fileType = fileType;
239 fobj.sab = sab;
240 fobj.sabViewFileSize = new DataView(sab,state.fbInt64Offset,8);
241 fobj.create = !!create;
242 fobj.readOnly = !!readOnly;
243 fobj.deleteOnClose = !!deleteOnClose;
244 /**
245 wa-sqlite, at this point, grabs a SyncAccessHandle and
246 assigns it to the accessHandle prop of the file state
247 object, but only for certain cases and it's unclear why it
248 places that limitation on it.
249 */
250 fobj.accessHandle = await hFile.createSyncAccessHandle();
251 storeAndNotify(opName, 0);
252 }catch(e){
253 error(opName,e);
254 storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
255 }
256 },
257 xRead: async function({fid,n,offset}){
258 log("xRead(",arguments[0],")");
259 let rc = 0;
260 const fh = __openFiles[fid];
261 try{
262 const aRead = new Uint8array(fh.sab, n);
263 const nRead = fh.accessHandle.read(aRead, {at: offset});
264 if(nRead < n){/* Zero-fill remaining bytes */
265 new Uint8array(fh.sab).fill(0, nRead, n);
266 rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
267 }
268 }catch(e){
269 error("xRead() failed",e,fh);
270 rc = state.sq3Codes.SQLITE_IOERR_READ;
271 }
272 storeAndNotify('xRead',rc);
273 },
274 xSleep: async function f(ms){
275 log("xSleep(",ms,")");
276 await new Promise((resolve)=>{
277 setTimeout(()=>resolve(), ms);
278 }).finally(()=>storeAndNotify('xSleep',0));
279 },
280 xSync: async function({fid,flags/*ignored*/}){
281 log("xSync(",arguments[0],")");
282 const fh = __openFiles[fid];
283 if(!fh.readOnly && fh.accessHandle) await fh.accessHandle.flush();
284 storeAndNotify('xSync',0);
285 },
286 xTruncate: async function({fid,size}){
287 log("xTruncate(",arguments[0],")");
288 let rc = 0;
289 const fh = __openFiles[fid];
290 try{
291 affirmNotRO('xTruncate', fh);
292 await fh.accessHandle.truncate(size);
293 }catch(e){
294 error("xTruncate():",e,fh);
295 rc = state.sq3Codes.SQLITE_IOERR_TRUNCATE;
296 }
297 storeAndNotify('xTruncate',rc);
298 },
299 xWrite: async function({fid,src,n,offset}){
300 log("xWrite(",arguments[0],")");
301 let rc;
stephan8200a6d2022-09-17 23:29:27 +0000302 const fh = __openFiles[fid];
stephan07315542022-09-17 20:50:12 +0000303 try{
stephan07315542022-09-17 20:50:12 +0000304 affirmNotRO('xWrite', fh);
305 const nOut = fh.accessHandle.write(new UInt8Array(fh.sab, 0, n), {at: offset});
306 rc = (nOut===n) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
307 }catch(e){
308 error("xWrite():",e,fh);
309 rc = state.sq3Codes.SQLITE_IOERR_WRITE;
310 }
311 storeAndNotify('xWrite',rc);
312 }
313};
314
315navigator.storage.getDirectory().then(function(d){
316 const wMsg = (type)=>postMessage({type});
317 state.rootDir = d;
318 log("state.rootDir =",state.rootDir);
319 self.onmessage = async function({data}){
320 log("self.onmessage()",data);
321 switch(data.type){
322 case 'init':{
323 /* Receive shared state from synchronous partner */
324 const opt = data.payload;
325 state.verbose = opt.verbose ?? 2;
326 state.fileBufferSize = opt.fileBufferSize;
327 state.fbInt64Offset = opt.fbInt64Offset;
stephane6f8a092022-09-17 21:13:26 +0000328 state.opSAB = opt.opSAB;
329 state.opSABView = new Int32Array(state.opSAB);
stephan07315542022-09-17 20:50:12 +0000330 state.opIds = opt.opIds;
331 state.sq3Codes = opt.sq3Codes;
332 Object.keys(vfsAsyncImpls).forEach((k)=>{
333 if(!Number.isFinite(state.opIds[k])){
334 toss("Maintenance required: missing state.opIds[",k,"]");
335 }
336 });
337 log("init state",state);
338 wMsg('inited');
339 break;
340 }
341 default:{
342 let err;
343 const m = vfsAsyncImpls[data.type] || toss("Unknown message type:",data.type);
344 try {
345 await m(data.payload).catch((e)=>err=e);
346 }catch(e){
347 err = e;
348 }
349 if(err){
350 error("Error handling",data.type+"():",e);
351 storeAndNotify(data.type, state.sq3Codes.SQLITE_ERROR);
352 }
353 break;
354 }
stephan132a87b2022-09-17 15:08:22 +0000355 }
356 };
stephan07315542022-09-17 20:50:12 +0000357 wMsg('loaded');
stephan8200a6d2022-09-17 23:29:27 +0000358}).catch((e)=>error(e));