aboutsummaryrefslogtreecommitdiff
path: root/ext/wasm/api/sqlite3-api-opfs.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/wasm/api/sqlite3-api-opfs.js')
-rw-r--r--ext/wasm/api/sqlite3-api-opfs.js393
1 files changed, 393 insertions, 0 deletions
diff --git a/ext/wasm/api/sqlite3-api-opfs.js b/ext/wasm/api/sqlite3-api-opfs.js
new file mode 100644
index 000000000..5a0af2641
--- /dev/null
+++ b/ext/wasm/api/sqlite3-api-opfs.js
@@ -0,0 +1,393 @@
+/*
+ 2022-07-22
+
+ 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.
+
+ ***********************************************************************
+
+ This file contains extensions to the sqlite3 WASM API related to the
+ Origin-Private FileSystem (OPFS). It is intended to be appended to
+ the main JS deliverable somewhere after sqlite3-api-glue.js and
+ before sqlite3-api-cleanup.js.
+
+ Significant notes and limitations:
+
+ - As of this writing, OPFS is still very much in flux and only
+ available in bleeding-edge versions of Chrome (v102+, noting that
+ that number will increase as the OPFS API matures).
+
+ - The _synchronous_ family of OPFS features (which is what this API
+ requires) are only available in non-shared Worker threads. This
+ file tries to detect that case and becomes a no-op if those
+ features do not seem to be available.
+*/
+
+// FileSystemHandle
+// FileSystemDirectoryHandle
+// FileSystemFileHandle
+// FileSystemFileHandle.prototype.createSyncAccessHandle
+self.sqlite3.postInit.push(function(self, sqlite3){
+ const warn = console.warn.bind(console),
+ error = console.error.bind(console);
+ if(!self.importScripts || !self.FileSystemFileHandle
+ || !self.FileSystemFileHandle.prototype.createSyncAccessHandle){
+ warn("OPFS not found or its sync API is not available in this environment.");
+ return;
+ }else if(!sqlite3.capi.wasm.bigIntEnabled){
+ error("OPFS requires BigInt support but sqlite3.capi.wasm.bigIntEnabled is false.");
+ return;
+ }
+ //warn('self.FileSystemFileHandle =',self.FileSystemFileHandle);
+ //warn('self.FileSystemFileHandle.prototype =',self.FileSystemFileHandle.prototype);
+ const toss = (...args)=>{throw new Error(args.join(' '))};
+ const capi = sqlite3.capi,
+ wasm = capi.wasm;
+ const sqlite3_vfs = capi.sqlite3_vfs
+ || toss("Missing sqlite3.capi.sqlite3_vfs object.");
+ const sqlite3_file = capi.sqlite3_file
+ || toss("Missing sqlite3.capi.sqlite3_file object.");
+ const sqlite3_io_methods = capi.sqlite3_io_methods
+ || toss("Missing sqlite3.capi.sqlite3_io_methods object.");
+ const StructBinder = sqlite3.StructBinder || toss("Missing sqlite3.StructBinder.");
+ const debug = console.debug.bind(console),
+ log = console.log.bind(console);
+ warn("UNDER CONSTRUCTION: setting up OPFS VFS...");
+
+ const pDVfs = capi.sqlite3_vfs_find(null)/*pointer to default VFS*/;
+ const dVfs = pDVfs
+ ? new sqlite3_vfs(pDVfs)
+ : null /* dVfs will be null when sqlite3 is built with
+ SQLITE_OS_OTHER. Though we cannot currently handle
+ that case, the hope is to eventually be able to. */;
+ const oVfs = new sqlite3_vfs();
+ const oIom = new sqlite3_io_methods();
+ oVfs.$iVersion = 2/*yes, two*/;
+ oVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof;
+ oVfs.$mxPathname = 1024/*sure, why not?*/;
+ oVfs.$zName = wasm.allocCString("opfs");
+ oVfs.ondispose = [
+ '$zName', oVfs.$zName,
+ 'cleanup dVfs', ()=>(dVfs ? dVfs.dispose() : null)
+ ];
+ if(dVfs){
+ oVfs.$xSleep = dVfs.$xSleep;
+ oVfs.$xRandomness = dVfs.$xRandomness;
+ }
+ // All C-side memory of oVfs is zeroed out, but just to be explicit:
+ oVfs.$xDlOpen = oVfs.$xDlError = oVfs.$xDlSym = oVfs.$xDlClose = null;
+
+ /**
+ Pedantic sidebar about oVfs.ondispose: the entries in that array
+ are items to clean up when oVfs.dispose() is called, but in this
+ environment it will never be called. The VFS instance simply
+ hangs around until the WASM module instance is cleaned up. We
+ "could" _hypothetically_ clean it up by "importing" an
+ sqlite3_os_end() impl into the wasm build, but the shutdown order
+ of the wasm engine and the JS one are undefined so there is no
+ guaranty that the oVfs instance would be available in one
+ environment or the other when sqlite3_os_end() is called (_if_ it
+ gets called at all in a wasm build, which is undefined).
+ */
+
+ /**
+ Installs a StructBinder-bound function pointer member of the
+ given name and function in the given StructType target object.
+ It creates a WASM proxy for the given function and arranges for
+ that proxy to be cleaned up when tgt.dispose() is called. Throws
+ on the slightest hint of error (e.g. tgt is-not-a StructType,
+ name does not map to a struct-bound member, etc.).
+
+ Returns a proxy for this function which is bound to tgt and takes
+ 2 args (name,func). That function returns the same thing,
+ permitting calls to be chained.
+
+ If called with only 1 arg, it has no side effects but returns a
+ func with the same signature as described above.
+ */
+ const installMethod = function callee(tgt, name, func){
+ if(!(tgt instanceof StructBinder.StructType)){
+ toss("Usage error: target object is-not-a StructType.");
+ }
+ if(1===arguments.length){
+ return (n,f)=>callee(tgt,n,f);
+ }
+ if(!callee.argcProxy){
+ callee.argcProxy = function(func,sig){
+ return function(...args){
+ if(func.length!==arguments.length){
+ toss("Argument mismatch. Native signature is:",sig);
+ }
+ return func.apply(this, args);
+ }
+ };
+ callee.removeFuncList = function(){
+ if(this.ondispose.__removeFuncList){
+ this.ondispose.__removeFuncList.forEach(
+ (v,ndx)=>{
+ if('number'===typeof v){
+ try{wasm.uninstallFunction(v)}
+ catch(e){/*ignore*/}
+ }
+ /* else it's a descriptive label for the next number in
+ the list. */
+ }
+ );
+ delete this.ondispose.__removeFuncList;
+ }
+ };
+ }/*static init*/
+ const sigN = tgt.memberSignature(name);
+ if(sigN.length<2){
+ toss("Member",name," is not a function pointer. Signature =",sigN);
+ }
+ const memKey = tgt.memberKey(name);
+ //log("installMethod",tgt, name, sigN);
+ const fProxy = 1
+ // We can remove this proxy middle-man once the VFS is working
+ ? callee.argcProxy(func, sigN)
+ : func;
+ const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true));
+ tgt[memKey] = pFunc;
+ if(!tgt.ondispose) tgt.ondispose = [];
+ if(!tgt.ondispose.__removeFuncList){
+ tgt.ondispose.push('ondispose.__removeFuncList handler',
+ callee.removeFuncList);
+ tgt.ondispose.__removeFuncList = [];
+ }
+ tgt.ondispose.__removeFuncList.push(memKey, pFunc);
+ return (n,f)=>callee(tgt, n, f);
+ }/*installMethod*/;
+
+ /**
+ Map of sqlite3_file pointers to OPFS handles.
+ */
+ const __opfsHandles = Object.create(null);
+
+ const randomFilename = function f(len=16){
+ if(!f._chars){
+ f._chars = "abcdefghijklmnopqrstuvwxyz"+
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+
+ "012346789";
+ f._n = f._chars.length;
+ }
+ const a = [];
+ let i = 0;
+ for( ; i < len; ++i){
+ const ndx = Math.random() * (f._n * 64) % f._n | 0;
+ a[i] = f._chars[ndx];
+ }
+ return a.join('');
+ };
+
+ //const rootDir = await navigator.storage.getDirectory();
+
+ ////////////////////////////////////////////////////////////////////////
+ // Set up OPFS VFS methods...
+ let inst = installMethod(oVfs);
+ inst('xOpen', function(pVfs, zName, pFile, flags, pOutFlags){
+ const f = new sqlite3_file(pFile);
+ f.$pMethods = oIom.pointer;
+ __opfsHandles[pFile] = f;
+ f.opfsHandle = null /* TODO */;
+ if(capi.SQLITE_OPEN_DELETEONCLOSE){
+ f.deleteOnClose = true;
+ }
+ f.filename = zName ? wasm.cstringToJs(zName) : randomFilename();
+ error("OPFS sqlite3_vfs::xOpen is not yet full implemented.");
+ return capi.SQLITE_IOERR;
+ })
+ ('xFullPathname', function(pVfs,zName,nOut,pOut){
+ /* Until/unless we have some notion of "current dir"
+ in OPFS, simply copy zName to pOut... */
+ const i = wasm.cstrncpy(pOut, zName, nOut);
+ return i<nOut ? 0 : capi.SQLITE_CANTOPEN
+ /*CANTOPEN is required by the docs but SQLITE_RANGE would be a closer match*/;
+ })
+ ('xAccess', function(pVfs,zName,flags,pOut){
+ error("OPFS sqlite3_vfs::xAccess is not yet implemented.");
+ let fileExists = 0;
+ switch(flags){
+ case capi.SQLITE_ACCESS_EXISTS: break;
+ case capi.SQLITE_ACCESS_READWRITE: break;
+ case capi.SQLITE_ACCESS_READ/*docs say this is never used*/:
+ default:
+ error("Unexpected flags value for sqlite3_vfs::xAccess():",flags);
+ return capi.SQLITE_MISUSE;
+ }
+ wasm.setMemValue(pOut, fileExists, 'i32');
+ return 0;
+ })
+ ('xDelete', function(pVfs, zName, doSyncDir){
+ error("OPFS sqlite3_vfs::xDelete is not yet implemented.");
+ return capi.SQLITE_IOERR;
+ })
+ ('xGetLastError', function(pVfs,nOut,pOut){
+ debug("OPFS sqlite3_vfs::xGetLastError() has nothing sensible to return.");
+ return 0;
+ })
+ ('xCurrentTime', function(pVfs,pOut){
+ /* If it turns out that we need to adjust for timezone, see:
+ https://stackoverflow.com/a/11760121/1458521 */
+ wasm.setMemValue(pOut, 2440587.5 + (new Date().getTime()/86400000),
+ 'double');
+ return 0;
+ })
+ ('xCurrentTimeInt64',function(pVfs,pOut){
+ // TODO: confirm that this calculation is correct
+ wasm.setMemValue(pOut, (2440587.5 * 86400000) + new Date().getTime(),
+ 'i64');
+ return 0;
+ });
+ if(!oVfs.$xSleep){
+ inst('xSleep', function(pVfs,ms){
+ error("sqlite3_vfs::xSleep(",ms,") cannot be implemented from "+
+ "JS and we have no default VFS to copy the impl from.");
+ return 0;
+ });
+ }
+ if(!oVfs.$xRandomness){
+ inst('xRandomness', function(pVfs, nOut, pOut){
+ const heap = wasm.heap8u();
+ let i = 0;
+ for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF;
+ return i;
+ });
+ }
+
+ ////////////////////////////////////////////////////////////////////////
+ // Set up OPFS sqlite3_io_methods...
+ inst = installMethod(oIom);
+ inst('xClose', async function(pFile){
+ warn("xClose(",arguments,") uses await");
+ const f = __opfsHandles[pFile];
+ delete __opfsHandles[pFile];
+ if(f.opfsHandle){
+ await f.opfsHandle.close();
+ if(f.deleteOnClose){
+ // TODO
+ }
+ }
+ f.dispose();
+ return 0;
+ })
+ ('xRead', /*i(ppij)*/function(pFile,pDest,n,offset){
+ /* int (*xRead)(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst) */
+ try {
+ const f = __opfsHandles[pFile];
+ const heap = wasm.heap8u();
+ const b = new Uint8Array(heap.buffer, pDest, n);
+ const nRead = f.opfsHandle.read(b, {at: offset});
+ if(nRead<n){
+ // MUST zero-fill short reads (per the docs)
+ heap.fill(0, dest + nRead, n - nRead);
+ }
+ return 0;
+ }catch(e){
+ error("xRead(",arguments,") failed:",e);
+ return capi.SQLITE_IOERR_READ;
+ }
+ })
+ ('xWrite', /*i(ppij)*/function(pFile,pSrc,n,offset){
+ /* int (*xWrite)(sqlite3_file*, const void*, int iAmt, sqlite3_int64 iOfst) */
+ try {
+ const f = __opfsHandles[pFile];
+ const b = new Uint8Array(wasm.heap8u().buffer, pSrc, n);
+ const nOut = f.opfsHandle.write(b, {at: offset});
+ if(nOut<n){
+ error("xWrite(",arguments,") short write!");
+ return capi.SQLITE_IOERR_WRITE;
+ }
+ return 0;
+ }catch(e){
+ error("xWrite(",arguments,") failed:",e);
+ return capi.SQLITE_IOERR_WRITE;
+ }
+ })
+ ('xTruncate', /*i(pj)*/async function(pFile,sz){
+ /* int (*xTruncate)(sqlite3_file*, sqlite3_int64 size) */
+ try{
+ warn("xTruncate(",arguments,") uses await");
+ const f = __opfsHandles[pFile];
+ await f.opfsHandle.truncate(sz);
+ return 0;
+ }
+ catch(e){
+ error("xTruncate(",arguments,") failed:",e);
+ return capi.SQLITE_IOERR_TRUNCATE;
+ }
+ })
+ ('xSync', /*i(pi)*/async function(pFile,flags){
+ /* int (*xSync)(sqlite3_file*, int flags) */
+ try {
+ warn("xSync(",arguments,") uses await");
+ const f = __opfsHandles[pFile];
+ await f.opfsHandle.flush();
+ return 0;
+ }catch(e){
+ error("xSync(",arguments,") failed:",e);
+ return capi.SQLITE_IOERR_SYNC;
+ }
+ })
+ ('xFileSize', /*i(pp)*/async function(pFile,pSz){
+ /* int (*xFileSize)(sqlite3_file*, sqlite3_int64 *pSize) */
+ try {
+ warn("xFileSize(",arguments,") uses await");
+ const f = __opfsHandles[pFile];
+ const fsz = await f.opfsHandle.getSize();
+ capi.wasm.setMemValue(pSz, fsz,'i64');
+ return 0;
+ }catch(e){
+ error("xFileSize(",arguments,") failed:",e);
+ return capi.SQLITE_IOERR_SEEK;
+ }
+ })
+ ('xLock', /*i(pi)*/function(pFile,lockType){
+ /* int (*xLock)(sqlite3_file*, int) */
+ // Opening a handle locks it automatically.
+ warn("xLock(",arguments,") is a no-op");
+ return 0;
+ })
+ ('xUnlock', /*i(pi)*/function(pFile,lockType){
+ /* int (*xUnlock)(sqlite3_file*, int) */
+ // Opening a handle locks it automatically.
+ warn("xUnlock(",arguments,") is a no-op");
+ return 0;
+ })
+ ('xCheckReservedLock', /*i(pp)*/function(pFile,pOut){
+ /* int (*xCheckReservedLock)(sqlite3_file*, int *pResOut) */
+ // Exclusive lock is automatically acquired when opened
+ warn("xCheckReservedLock(",arguments,") is a no-op");
+ wasm.setMemValue(pOut,1,'i32');
+ return 0;
+ })
+ ('xFileControl', /*i(pip)*/function(pFile,op,pArg){
+ /* int (*xFileControl)(sqlite3_file*, int op, void *pArg) */
+ debug("xFileControl(",arguments,") is a no-op");
+ return capi.SQLITE_NOTFOUND;
+ })
+ ('xDeviceCharacteristics',/*i(p)*/function(pFile){
+ /* int (*xDeviceCharacteristics)(sqlite3_file*) */
+ debug("xDeviceCharacteristics(",pFile,")");
+ return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
+ });
+ // xSectorSize may be NULL
+ //('xSectorSize', function(pFile){
+ // /* int (*xSectorSize)(sqlite3_file*) */
+ // log("xSectorSize(",pFile,")");
+ // return 4096 /* ==> SQLITE_DEFAULT_SECTOR_SIZE */;
+ //})
+
+ const rc = capi.sqlite3_vfs_register(oVfs.pointer, 0);
+ if(rc){
+ oVfs.dispose();
+ toss("sqlite3_vfs_register(OPFS) failed with rc",rc);
+ }
+ capi.sqlite3_vfs_register.addReference(oVfs, oIom);
+ warn("End of (very incomplete) OPFS setup.", oVfs);
+ //oVfs.dispose()/*only because we can't yet do anything with it*/;
+});