| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| "use strict"; |
| const wPost = (type,...args)=>postMessage({type, payload:args}); |
| const installAsyncProxy = function(){ |
| const toss = function(...args){throw new Error(args.join(' '))}; |
| if(globalThis.window === globalThis){ |
| 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."); |
| } |
|
|
| |
| |
| |
| |
| const state = Object.create(null); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| state.verbose = 1; |
|
|
| const loggers = { |
| 0:console.error.bind(console), |
| 1:console.warn.bind(console), |
| 2:console.log.bind(console) |
| }; |
| const logImpl = (level,...args)=>{ |
| if(state.verbose>level) loggers[level]("OPFS asyncer:",...args); |
| }; |
| const log = (...args)=>logImpl(2, ...args); |
| const warn = (...args)=>logImpl(1, ...args); |
| const error = (...args)=>logImpl(0, ...args); |
|
|
| |
| |
| |
| |
| |
| |
| |
| const __openFiles = Object.create(null); |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const __implicitLocks = new Set(); |
|
|
| |
| |
| |
| |
| |
| |
| const getResolvedPath = function(filename,splitIt){ |
| const p = new URL( |
| filename, 'file://irrelevant' |
| ).pathname; |
| return splitIt ? p.split('/').filter((v)=>!!v) : p; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| const getDirForFilename = async function f(absFilename, createDirs = false){ |
| const path = getResolvedPath(absFilename, true); |
| const filename = path.pop(); |
| let dh = state.rootDir; |
| for(const dirName of path){ |
| if(dirName){ |
| dh = await dh.getDirectoryHandle(dirName, {create: !!createDirs}); |
| } |
| } |
| return [dh, filename]; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const closeSyncHandle = async (fh)=>{ |
| if(fh.syncHandle){ |
| log("Closing sync handle for",fh.filenameAbs); |
| const h = fh.syncHandle; |
| delete fh.syncHandle; |
| delete fh.xLock; |
| __implicitLocks.delete(fh.fid); |
| return h.close(); |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const closeSyncHandleNoThrow = async (fh)=>{ |
| try{await closeSyncHandle(fh)} |
| catch(e){ |
| warn("closeSyncHandleNoThrow() ignoring:",e,fh); |
| } |
| }; |
|
|
| |
| const releaseImplicitLocks = async ()=>{ |
| if(__implicitLocks.size){ |
| |
| for(const fid of __implicitLocks){ |
| const fh = __openFiles[fid]; |
| await closeSyncHandleNoThrow(fh); |
| log("Auto-unlocked",fid,fh.filenameAbs); |
| } |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| const releaseImplicitLock = async (fh)=>{ |
| if(fh.releaseImplicitLocks && __implicitLocks.has(fh.fid)){ |
| return closeSyncHandleNoThrow(fh); |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| class GetSyncHandleError extends Error { |
| constructor(errorObject, ...msg){ |
| super([ |
| ...msg, ': '+errorObject.name+':', |
| errorObject.message |
| ].join(' '), { |
| cause: errorObject |
| }); |
| this.name = 'GetSyncHandleError'; |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| GetSyncHandleError.convertRc = (e,rc)=>{ |
| if( e instanceof GetSyncHandleError ){ |
| if( e.cause.name==='NoModificationAllowedError' |
| |
| |
| || (e.cause.name==='DOMException' |
| && 0===e.cause.message.indexOf('Access Handles cannot')) ){ |
| return state.sq3Codes.SQLITE_BUSY; |
| }else if( 'NotFoundError'===e.cause.name ){ |
| |
| |
| |
| |
| |
| return state.sq3Codes.SQLITE_CANTOPEN; |
| } |
| }else if( 'NotFoundError'===e?.name ){ |
| return state.sq3Codes.SQLITE_CANTOPEN; |
| } |
| return rc; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const getSyncHandle = async (fh,opName)=>{ |
| if(!fh.syncHandle){ |
| const t = performance.now(); |
| log("Acquiring sync handle for",fh.filenameAbs); |
| const maxTries = 6, |
| msBase = state.asyncIdleWaitTime * 2; |
| let i = 1, ms = msBase; |
| for(; true; ms = msBase * ++i){ |
| try { |
| |
| |
| |
| fh.syncHandle = await fh.fileHandle.createSyncAccessHandle(); |
| break; |
| }catch(e){ |
| if(i === maxTries){ |
| throw new GetSyncHandleError( |
| e, "Error getting sync handle for",opName+"().",maxTries, |
| "attempts failed.",fh.filenameAbs |
| ); |
| } |
| warn("Error getting sync handle for",opName+"(). Waiting",ms, |
| "ms and trying again.",fh.filenameAbs,e); |
| Atomics.wait(state.sabOPView, state.opIds.retry, 0, ms); |
| } |
| } |
| log("Got",opName+"() sync handle for",fh.filenameAbs, |
| 'in',performance.now() - t,'ms'); |
| if(!fh.xLock){ |
| __implicitLocks.add(fh.fid); |
| log("Acquired implicit lock for",opName+"()",fh.fid,fh.filenameAbs); |
| } |
| } |
| return fh.syncHandle; |
| }; |
|
|
| |
| |
| |
| |
| const storeAndNotify = (opName, value)=>{ |
| log(opName+"() => notify(",value,")"); |
| Atomics.store(state.sabOPView, state.opIds.rc, value); |
| Atomics.notify(state.sabOPView, state.opIds.rc); |
| }; |
|
|
| |
| |
| |
| const affirmNotRO = function(opName,fh){ |
| if(fh.readOnly) toss(opName+"(): File is read-only: "+fh.filenameAbs); |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| let flagAsyncShutdown = false; |
|
|
| |
| |
| |
| |
| const vfsAsyncImpls = { |
| 'opfs-async-shutdown': async ()=>{ |
| flagAsyncShutdown = true; |
| storeAndNotify('opfs-async-shutdown', 0); |
| }, |
| mkdir: async (dirname)=>{ |
| let rc = 0; |
| try { |
| await getDirForFilename(dirname+"/filepart", true); |
| }catch(e){ |
| state.s11n.storeException(2,e); |
| rc = state.sq3Codes.SQLITE_IOERR; |
| } |
| storeAndNotify('mkdir', rc); |
| }, |
| xAccess: async (filename)=>{ |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let rc = 0; |
| try{ |
| const [dh, fn] = await getDirForFilename(filename); |
| await dh.getFileHandle(fn); |
| }catch(e){ |
| state.s11n.storeException(2,e); |
| rc = state.sq3Codes.SQLITE_IOERR; |
| } |
| storeAndNotify('xAccess', rc); |
| }, |
| xClose: async function(fid){ |
| const opName = 'xClose'; |
| __implicitLocks.delete(fid); |
| const fh = __openFiles[fid]; |
| let rc = 0; |
| if(fh){ |
| delete __openFiles[fid]; |
| await closeSyncHandle(fh); |
| if(fh.deleteOnClose){ |
| try{ await fh.dirHandle.removeEntry(fh.filenamePart) } |
| catch(e){ warn("Ignoring dirHandle.removeEntry() failure of",fh,e) } |
| } |
| }else{ |
| state.s11n.serialize(); |
| rc = state.sq3Codes.SQLITE_NOTFOUND; |
| } |
| storeAndNotify(opName, rc); |
| }, |
| xDelete: async function(...args){ |
| const rc = await vfsAsyncImpls.xDeleteNoWait(...args); |
| storeAndNotify('xDelete', rc); |
| }, |
| xDeleteNoWait: async function(filename, syncDir = 0, recursive = false){ |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let rc = 0; |
| try { |
| while(filename){ |
| const [hDir, filenamePart] = await getDirForFilename(filename, false); |
| if(!filenamePart) break; |
| await hDir.removeEntry(filenamePart, {recursive}); |
| if(0x1234 !== syncDir) break; |
| recursive = false; |
| filename = getResolvedPath(filename, true); |
| filename.pop(); |
| filename = filename.join('/'); |
| } |
| }catch(e){ |
| state.s11n.storeException(2,e); |
| rc = state.sq3Codes.SQLITE_IOERR_DELETE; |
| } |
| return rc; |
| }, |
| xFileSize: async function(fid){ |
| const fh = __openFiles[fid]; |
| let rc = 0; |
| try{ |
| const sz = await (await getSyncHandle(fh,'xFileSize')).getSize(); |
| state.s11n.serialize(Number(sz)); |
| }catch(e){ |
| state.s11n.storeException(1,e); |
| rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR); |
| } |
| await releaseImplicitLock(fh); |
| storeAndNotify('xFileSize', rc); |
| }, |
| xLock: async function(fid, |
| lockType){ |
| const fh = __openFiles[fid]; |
| let rc = 0; |
| const oldLockType = fh.xLock; |
| fh.xLock = lockType; |
| if( !fh.syncHandle ){ |
| try { |
| await getSyncHandle(fh,'xLock'); |
| __implicitLocks.delete(fid); |
| }catch(e){ |
| state.s11n.storeException(1,e); |
| rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_LOCK); |
| fh.xLock = oldLockType; |
| } |
| } |
| storeAndNotify('xLock',rc); |
| }, |
| xOpen: async function(fid, filename, |
| flags, |
| opfsFlags){ |
| const opName = 'xOpen'; |
| const create = (state.sq3Codes.SQLITE_OPEN_CREATE & flags); |
| try{ |
| let hDir, filenamePart; |
| try { |
| [hDir, filenamePart] = await getDirForFilename(filename, !!create); |
| }catch(e){ |
| state.s11n.storeException(1,e); |
| storeAndNotify(opName, state.sq3Codes.SQLITE_NOTFOUND); |
| return; |
| } |
| if( state.opfsFlags.OPFS_UNLINK_BEFORE_OPEN & opfsFlags ){ |
| try{ |
| await hDir.removeEntry(filenamePart); |
| }catch(e){ |
| |
| |
| } |
| } |
| const hFile = await hDir.getFileHandle(filenamePart, {create}); |
| const fh = Object.assign(Object.create(null),{ |
| fid: fid, |
| filenameAbs: filename, |
| filenamePart: filenamePart, |
| dirHandle: hDir, |
| fileHandle: hFile, |
| sabView: state.sabFileBufView, |
| readOnly: !create && !!(state.sq3Codes.SQLITE_OPEN_READONLY & flags), |
| deleteOnClose: !!(state.sq3Codes.SQLITE_OPEN_DELETEONCLOSE & flags) |
| }); |
| fh.releaseImplicitLocks = |
| (opfsFlags & state.opfsFlags.OPFS_UNLOCK_ASAP) |
| || state.opfsFlags.defaultUnlockAsap; |
| __openFiles[fid] = fh; |
| storeAndNotify(opName, 0); |
| }catch(e){ |
| error(opName,e); |
| state.s11n.storeException(1,e); |
| storeAndNotify(opName, state.sq3Codes.SQLITE_IOERR); |
| } |
| }, |
| xRead: async function(fid,n,offset64){ |
| let rc = 0, nRead; |
| const fh = __openFiles[fid]; |
| try{ |
| nRead = (await getSyncHandle(fh,'xRead')).read( |
| fh.sabView.subarray(0, n), |
| {at: Number(offset64)} |
| ); |
| if(nRead < n){ |
| fh.sabView.fill(0, nRead, n); |
| rc = state.sq3Codes.SQLITE_IOERR_SHORT_READ; |
| } |
| }catch(e){ |
| error("xRead() failed",e,fh); |
| state.s11n.storeException(1,e); |
| rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_READ); |
| } |
| await releaseImplicitLock(fh); |
| storeAndNotify('xRead',rc); |
| }, |
| xSync: async function(fid,flags){ |
| const fh = __openFiles[fid]; |
| let rc = 0; |
| if(!fh.readOnly && fh.syncHandle){ |
| try { |
| await fh.syncHandle.flush(); |
| }catch(e){ |
| state.s11n.storeException(2,e); |
| rc = state.sq3Codes.SQLITE_IOERR_FSYNC; |
| } |
| } |
| storeAndNotify('xSync',rc); |
| }, |
| xTruncate: async function(fid,size){ |
| let rc = 0; |
| const fh = __openFiles[fid]; |
| try{ |
| affirmNotRO('xTruncate', fh); |
| await (await getSyncHandle(fh,'xTruncate')).truncate(size); |
| }catch(e){ |
| error("xTruncate():",e,fh); |
| state.s11n.storeException(2,e); |
| rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_TRUNCATE); |
| } |
| await releaseImplicitLock(fh); |
| storeAndNotify('xTruncate',rc); |
| }, |
| xUnlock: async function(fid, |
| lockType){ |
| let rc = 0; |
| const fh = __openFiles[fid]; |
| if( fh.syncHandle |
| && state.sq3Codes.SQLITE_LOCK_NONE===lockType |
| |
| ){ |
| try { await closeSyncHandle(fh) } |
| catch(e){ |
| state.s11n.storeException(1,e); |
| rc = state.sq3Codes.SQLITE_IOERR_UNLOCK; |
| } |
| } |
| storeAndNotify('xUnlock',rc); |
| }, |
| xWrite: async function(fid,n,offset64){ |
| let rc; |
| const fh = __openFiles[fid]; |
| try{ |
| affirmNotRO('xWrite', fh); |
| rc = ( |
| n === (await getSyncHandle(fh,'xWrite')) |
| .write(fh.sabView.subarray(0, n), |
| {at: Number(offset64)}) |
| ) ? 0 : state.sq3Codes.SQLITE_IOERR_WRITE; |
| }catch(e){ |
| error("xWrite():",e,fh); |
| state.s11n.storeException(1,e); |
| rc = GetSyncHandleError.convertRc(e,state.sq3Codes.SQLITE_IOERR_WRITE); |
| } |
| await releaseImplicitLock(fh); |
| storeAndNotify('xWrite',rc); |
| } |
| }; |
|
|
| const initS11n = ()=>{ |
| |
| |
| |
| |
| if(state.s11n) return state.s11n; |
| const textDecoder = new TextDecoder(), |
| textEncoder = new TextEncoder('utf-8'), |
| viewU8 = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize), |
| viewDV = new DataView(state.sabIO, state.sabS11nOffset, state.sabS11nSize); |
| state.s11n = Object.create(null); |
| const TypeIds = Object.create(null); |
| TypeIds.number = { id: 1, size: 8, getter: 'getFloat64', setter: 'setFloat64' }; |
| TypeIds.bigint = { id: 2, size: 8, getter: 'getBigInt64', setter: 'setBigInt64' }; |
| TypeIds.boolean = { id: 3, size: 4, getter: 'getInt32', setter: 'setInt32' }; |
| TypeIds.string = { id: 4 }; |
| const getTypeId = (v)=>( |
| TypeIds[typeof v] |
| || toss("Maintenance required: this value type cannot be serialized.",v) |
| ); |
| const getTypeIdById = (tid)=>{ |
| switch(tid){ |
| case TypeIds.number.id: return TypeIds.number; |
| case TypeIds.bigint.id: return TypeIds.bigint; |
| case TypeIds.boolean.id: return TypeIds.boolean; |
| case TypeIds.string.id: return TypeIds.string; |
| default: toss("Invalid type ID:",tid); |
| } |
| }; |
| state.s11n.deserialize = function(clear=false){ |
| const argc = viewU8[0]; |
| const rc = argc ? [] : null; |
| if(argc){ |
| const typeIds = []; |
| let offset = 1, i, n, v; |
| for(i = 0; i < argc; ++i, ++offset){ |
| typeIds.push(getTypeIdById(viewU8[offset])); |
| } |
| for(i = 0; i < argc; ++i){ |
| const t = typeIds[i]; |
| if(t.getter){ |
| v = viewDV[t.getter](offset, state.littleEndian); |
| offset += t.size; |
| }else{ |
| n = viewDV.getInt32(offset, state.littleEndian); |
| offset += 4; |
| v = textDecoder.decode(viewU8.slice(offset, offset+n)); |
| offset += n; |
| } |
| rc.push(v); |
| } |
| } |
| if(clear) viewU8[0] = 0; |
| |
| return rc; |
| }; |
| state.s11n.serialize = function(...args){ |
| if(args.length){ |
| |
| const typeIds = []; |
| let i = 0, offset = 1; |
| viewU8[0] = args.length & 0xff ; |
| for(; i < args.length; ++i, ++offset){ |
| |
| |
| typeIds.push(getTypeId(args[i])); |
| viewU8[offset] = typeIds[i].id; |
| } |
| for(i = 0; i < args.length; ++i) { |
| |
| |
| const t = typeIds[i]; |
| if(t.setter){ |
| viewDV[t.setter](offset, args[i], state.littleEndian); |
| offset += t.size; |
| }else{ |
| const s = textEncoder.encode(args[i]); |
| viewDV.setInt32(offset, s.byteLength, state.littleEndian); |
| offset += 4; |
| viewU8.set(s, offset); |
| offset += s.byteLength; |
| } |
| } |
| |
| }else{ |
| viewU8[0] = 0; |
| } |
| }; |
|
|
| state.s11n.storeException = state.asyncS11nExceptions |
| ? ((priority,e)=>{ |
| if(priority<=state.asyncS11nExceptions){ |
| state.s11n.serialize([e.name,': ',e.message].join("")); |
| } |
| }) |
| : ()=>{}; |
|
|
| return state.s11n; |
| }; |
|
|
| const waitLoop = async function f(){ |
| const opHandlers = Object.create(null); |
| for(let k of Object.keys(state.opIds)){ |
| const vi = vfsAsyncImpls[k]; |
| if(!vi) continue; |
| const o = Object.create(null); |
| opHandlers[state.opIds[k]] = o; |
| o.key = k; |
| o.f = vi; |
| } |
| while(!flagAsyncShutdown){ |
| try { |
| if('not-equal'!==Atomics.wait( |
| state.sabOPView, state.opIds.whichOp, 0, state.asyncIdleWaitTime |
| )){ |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| await releaseImplicitLocks(); |
| continue; |
| } |
| const opId = Atomics.load(state.sabOPView, state.opIds.whichOp); |
| Atomics.store(state.sabOPView, state.opIds.whichOp, 0); |
| const hnd = opHandlers[opId] ?? toss("No waitLoop handler for whichOp #",opId); |
| const args = state.s11n.deserialize( |
| true |
| |
| |
| ) || []; |
| |
| if(hnd.f) await hnd.f(...args); |
| else error("Missing callback for opId",opId); |
| }catch(e){ |
| error('in waitLoop():',e); |
| } |
| } |
| }; |
|
|
| navigator.storage.getDirectory().then(function(d){ |
| state.rootDir = d; |
| globalThis.onmessage = function({data}){ |
| switch(data.type){ |
| case 'opfs-async-init':{ |
| |
| const opt = data.args; |
| for(const k in opt) state[k] = opt[k]; |
| state.verbose = opt.verbose ?? 1; |
| state.sabOPView = new Int32Array(state.sabOP); |
| state.sabFileBufView = new Uint8Array(state.sabIO, 0, state.fileBufferSize); |
| state.sabS11nView = new Uint8Array(state.sabIO, state.sabS11nOffset, state.sabS11nSize); |
| Object.keys(vfsAsyncImpls).forEach((k)=>{ |
| if(!Number.isFinite(state.opIds[k])){ |
| toss("Maintenance required: missing state.opIds[",k,"]"); |
| } |
| }); |
| initS11n(); |
| log("init state",state); |
| wPost('opfs-async-inited'); |
| waitLoop(); |
| break; |
| } |
| case 'opfs-async-restart': |
| if(flagAsyncShutdown){ |
| warn("Restarting after opfs-async-shutdown. Might or might not work."); |
| flagAsyncShutdown = false; |
| waitLoop(); |
| } |
| break; |
| } |
| }; |
| wPost('opfs-async-loaded'); |
| }).catch((e)=>error("error initializing OPFS asyncer:",e)); |
| }; |
| if(!globalThis.SharedArrayBuffer){ |
| wPost('opfs-unavailable', "Missing SharedArrayBuffer API.", |
| "The server must emit the COOP/COEP response headers to enable that."); |
| }else if(!globalThis.Atomics){ |
| wPost('opfs-unavailable', "Missing Atomics API.", |
| "The server must emit the COOP/COEP response headers to enable that."); |
| }else if(!globalThis.FileSystemHandle || |
| !globalThis.FileSystemDirectoryHandle || |
| !globalThis.FileSystemFileHandle || |
| !globalThis.FileSystemFileHandle.prototype.createSyncAccessHandle || |
| !navigator?.storage?.getDirectory){ |
| wPost('opfs-unavailable',"Missing required OPFS APIs."); |
| }else{ |
| installAsyncProxy(); |
| } |
|
|