1
0
mirror of https://github.com/sqlite/sqlite.git synced 2025-06-07 19:02:21 +03:00
sqlite/ext/wasm/sqlite3-opfs-async-proxy.js
stephan 0731554629 Add the remaining vfs/io_methods wrappers to the OPFS sync/async proxy, but most are not yet tested.
FossilOrigin-Name: 44db9132145b3072488ea91db53f6c06be74544beccad5fd07efd22c0f03dc04
2022-09-17 20:50:12 +00:00

328 lines
10 KiB
JavaScript

/*
2022-09-16
The author disclaims copyright to this source code. In place of a
legal notice, here is a blessing:
* May you do good and not evil.
* May you find forgiveness for yourself and forgive others.
* May you share freely, never taking more than you give.
***********************************************************************
A EXTREMELY INCOMPLETE and UNDER CONSTRUCTION experiment for OPFS: a
Worker which manages asynchronous OPFS handles on behalf of a
synchronous API which controls it via a combination of Worker
messages, SharedArrayBuffer, and Atomics.
Highly indebted to:
https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/OriginPrivateFileSystemVFS.js
for demonstrating how to use the OPFS APIs.
This file is to be loaded as a Worker. It does not have any direct
access to the sqlite3 JS/WASM bits, so any bits which it needs (most
notably SQLITE_xxx integer codes) have to be imported into it via an
initialization process.
*/
'use strict';
const toss = function(...args){throw new Error(args.join(' '))};
if(self.window === self){
toss("This code cannot run from the main thread.",
"Load it as a Worker from a separate Worker.");
}else if(!navigator.storage.getDirectory){
toss("This API requires navigator.storage.getDirectory.");
}
/**
Will hold state copied to this object from the syncronous side of
this API.
*/
const state = Object.create(null);
/**
verbose:
0 = no logging output
1 = only errors
2 = warnings and errors
3 = debug, warnings, and errors
*/
state.verbose = 2;
const __logPrefix = "OPFS asyncer:";
const log = (...args)=>{
if(state.verbose>2) console.log(__logPrefix,...args);
};
const warn = (...args)=>{
if(state.verbose>1) console.warn(__logPrefix,...args);
};
const error = (...args)=>{
if(state.verbose) console.error(__logPrefix,...args);
};
warn("This file is very much experimental and under construction.",self.location.pathname);
/**
Map of sqlite3_file pointers (integers) to metadata related to a
given OPFS file handles. The pointers are, in this side of the
interface, opaque file handle IDs provided by the synchronous
part of this constellation. Each value is an object with a structure
demonstrated in the xOpen() impl.
*/
const __openFiles = Object.create(null);
/**
Map of dir names to FileSystemDirectoryHandle objects.
*/
const __dirCache = new Map;
/**
Takes the absolute path to a filesystem element. Returns an array
of [handleOfContainingDir, filename]. If the 2nd argument is
truthy then each directory element leading to the file is created
along the way. Throws if any creation or resolution fails.
*/
const getDirForPath = async function f(absFilename, createDirs = false){
const url = new URL(
absFilename, 'file://xyz'
) /* use URL to resolve path pieces such as a/../b */;
const path = url.pathname.split('/').filter((v)=>!!v);
const filename = path.pop();
const allDirs = '/'+path.join('/');
let dh = __dirCache.get(allDirs);
if(!dh){
dh = state.rootDir;
for(const dirName of path){
if(dirName){
dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs});
}
}
__dirCache.set(allDirs, dh);
}
return [dh, filename];
};
/**
Stores the given value at the array index reserved for the given op
and then Atomics.notify()'s it.
*/
const storeAndNotify = (opName, value)=>{
log(opName+"() is notify()ing w/ value:",value);
Atomics.store(state.opBuf, state.opIds[opName], value);
Atomics.notify(state.opBuf, state.opIds[opName]);
};
const isInt32 = function(n){
return ('bigint'!==typeof n /*TypeError: can't convert BigInt to number*/)
&& !!(n===(n|0) && n<=2147483647 && n>=-2147483648);
};
const affirm32Bits = function(n){
return isInt32(n) || toss("Number is too large (>31 bits) (FIXME!):",n);
};
/**
Throws if fh is a file-holding object which is flagged as read-only.
*/
const affirmNotRO = function(opName,fh){
if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs);
};
/**
Asynchronous wrappers for sqlite3_vfs and sqlite3_io_methods
methods. Maintenance reminder: members are in alphabetical order
to simplify finding them.
*/
const vfsAsyncImpls = {
xAccess: async function({filename, exists, readWrite}){
warn("xAccess(",arguments[0],") is TODO");
const rc = state.sq3Codes.SQLITE_IOERR;
storeAndNotify('xAccess', rc);
},
xClose: async function(fid){
const opName = 'xClose';
log(opName+"(",arguments[0],")");
const fh = __openFiles[fid];
if(fh){
delete __openFiles[fid];
if(fh.accessHandle) await fh.accessHandle.close();
if(fh.deleteOnClose){
try{ await fh.dirHandle.removeEntry(fh.filenamePart) }
catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) }
}
storeAndNotify(opName, 0);
}else{
storeAndNotify(opName, state.sq3Codes.SQLITE_NOFOUND);
}
},
xDelete: async function({filename, syncDir/*ignored*/}){
log("xDelete(",arguments[0],")");
try {
const [hDir, filenamePart] = await getDirForPath(filename, false);
await hDir.removeEntry(filenamePart);
}catch(e){
/* Ignoring: _presumably_ the file can't be found. */
}
storeAndNotify('xDelete', 0);
},
xFileSize: async function(fid){
log("xFileSize(",arguments,")");
const fh = __openFiles[fid];
let sz;
try{
sz = await fh.accessHandle.getSize();
fh.sabViewFileSize.setBigInt64(0, BigInt(sz));
sz = 0;
}catch(e){
error("xFileSize():",e, fh);
sz = state.sq3Codes.SQLITE_IOERR;
}
storeAndNotify('xFileSize', sz);
},
xOpen: async function({
fid/*sqlite3_file pointer*/,
sab/*file-specific SharedArrayBuffer*/,
filename,
fileType = undefined /*mainDb, mainJournal, etc.*/,
create = false, readOnly = false, deleteOnClose = false
}){
const opName = 'xOpen';
try{
if(create) readOnly = false;
log(opName+"(",arguments[0],")");
let hDir, filenamePart;
try {
[hDir, filenamePart] = await getDirForPath(filename, !!create);
}catch(e){
storeAndNotify(opName, state.sql3Codes.SQLITE_NOTFOUND);
return;
}
const hFile = await hDir.getFileHandle(filenamePart, {create: !!create});
log(opName,"filenamePart =",filenamePart, 'hDir =',hDir);
const fobj = __openFiles[fid] = Object.create(null);
fobj.filenameAbs = filename;
fobj.filenamePart = filenamePart;
fobj.dirHandle = hDir;
fobj.fileHandle = hFile;
fobj.fileType = fileType;
fobj.sab = sab;
fobj.sabViewFileSize = new DataView(sab,state.fbInt64Offset,8);
fobj.create = !!create;
fobj.readOnly = !!readOnly;
fobj.deleteOnClose = !!deleteOnClose;
/**
wa-sqlite, at this point, grabs a SyncAccessHandle and
assigns it to the accessHandle prop of the file state
object, but only for certain cases and it's unclear why it
places that limitation on it.
*/
fobj.accessHandle = await hFile.createSyncAccessHandle();
storeAndNotify(opName, 0);
}catch(e){
error(opName,e);
storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR);
}
},
xRead: async function({fid,n,offset}){
log("xRead(",arguments[0],")");
let rc = 0;
const fh = __openFiles[fid];
try{
const aRead = new Uint8array(fh.sab, n);
const nRead = fh.accessHandle.read(aRead, {at: offset});
if(nRead < n){/* Zero-fill remaining bytes */
new Uint8array(fh.sab).fill(0, nRead, n);
rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ;
}
}catch(e){
error("xRead() failed",e,fh);
rc = state.sq3Codes.SQLITE_IOERR_READ;
}
storeAndNotify('xRead',rc);
},
xSleep: async function f(ms){
log("xSleep(",ms,")");
await new Promise((resolve)=>{
setTimeout(()=>resolve(), ms);
}).finally(()=>storeAndNotify('xSleep',0));
},
xSync: async function({fid,flags/*ignored*/}){
log("xSync(",arguments[0],")");
const fh = __openFiles[fid];
if(!fh.readOnly && fh.accessHandle) await fh.accessHandle.flush();
storeAndNotify('xSync',0);
},
xTruncate: async function({fid,size}){
log("xTruncate(",arguments[0],")");
let rc = 0;
const fh = __openFiles[fid];
try{
affirmNotRO('xTruncate', fh);
await fh.accessHandle.truncate(size);
}catch(e){
error("xTruncate():",e,fh);
rc = state.sq3Codes.SQLITE_IOERR_TRUNCATE;
}
storeAndNotify('xTruncate',rc);
},
xWrite: async function({fid,src,n,offset}){
log("xWrite(",arguments[0],")");
let rc;
try{
const fh = __openFiles[fid];
affirmNotRO('xWrite', fh);
const nOut = fh.accessHandle.write(new UInt8Array(fh.sab, 0, n), {at: offset});
rc = (nOut===n) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE;
}catch(e){
error("xWrite():",e,fh);
rc = state.sq3Codes.SQLITE_IOERR_WRITE;
}
storeAndNotify('xWrite',rc);
}
};
navigator.storage.getDirectory().then(function(d){
const wMsg = (type)=>postMessage({type});
state.rootDir = d;
log("state.rootDir =",state.rootDir);
self.onmessage = async function({data}){
log("self.onmessage()",data);
switch(data.type){
case 'init':{
/* Receive shared state from synchronous partner */
const opt = data.payload;
state.verbose = opt.verbose ?? 2;
state.fileBufferSize = opt.fileBufferSize;
state.fbInt64Offset = opt.fbInt64Offset;
state.opSab = opt.opSab;
state.opBuf = new Int32Array(state.opSab);
state.opIds = opt.opIds;
state.sq3Codes = opt.sq3Codes;
Object.keys(vfsAsyncImpls).forEach((k)=>{
if(!Number.isFinite(state.opIds[k])){
toss("Maintenance required: missing state.opIds[",k,"]");
}
});
log("init state",state);
wMsg('inited');
break;
}
default:{
let err;
const m = vfsAsyncImpls[data.type] || toss("Unknown message type:",data.type);
try {
await m(data.payload).catch((e)=>err=e);
}catch(e){
err = e;
}
if(err){
error("Error handling",data.type+"():",e);
storeAndNotify(data.type, state.sq3Codes.SQLITE_ERROR);
}
break;
}
}
};
wMsg('loaded');
});