| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | "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(); |
| | } |
| |
|