diff --git a/demo/benchmark.ts b/demo/benchmark.ts index 2859e8a..76b02da 100644 --- a/demo/benchmark.ts +++ b/demo/benchmark.ts @@ -21,11 +21,17 @@ const writeData = Array(1000) let startTime = performance.now(); let testFileHandle = await root.getFileHandle(fileName, { create: true }); -const writer1 = await testFileHandle.createWritable(); -for (const d of writeData) { - await writer1.write(d); +if (testFileHandle.createWritable == null) { + updateCost('built-in-writer-cost', true); +} else { + const writer1 = await testFileHandle.createWritable(); + for (const d of writeData) { + await writer1.write(d); + } + updateCost('built-in-writer-cost'); + await writer1.truncate(0); + await writer1.close(); } -updateCost('built-in-writer-cost'); startTime = performance.now(); let tx = db.transaction('data', 'readwrite'); @@ -37,9 +43,6 @@ for (const d of writeData) { await tx.done; updateCost('indexeddb-write-cost'); -await writer1.truncate(0); -await writer1.close(); - startTime = performance.now(); const writer2 = await file(fileName).createWriter(); for (const d of writeData) { @@ -97,8 +100,12 @@ updateCost('opfs-tools-read-all-cost'); getElById('status').remove(); -function updateCost(id: string) { - getElById(id).textContent = `${~~(performance.now() - startTime)}ms`; +function updateCost(id: string, unsupported = false) { + if (unsupported) { + getElById(id).textContent = 'unsupported'; + } else { + getElById(id).textContent = `${~~(performance.now() - startTime)}ms`; + } } // clear diff --git a/src/access-worker.ts b/src/access-worker.ts index 5f6ecaf..4c38815 100644 --- a/src/access-worker.ts +++ b/src/access-worker.ts @@ -1,11 +1,5 @@ -interface FileSystemSyncAccessHandle { - read: (container: ArrayBuffer, opts: { at: number }) => number; - write: (data: ArrayBuffer | ArrayBufferView, opts?: { at: number }) => number; - flush: () => void; - close: () => void; - truncate: (newSize: number) => void; - getSize: () => number; -} +import { FileSystemSyncAccessHandle } from './common'; +import OPFSWorker from './opfs-worker?worker&inline'; type Async = F extends (...args: infer Params) => infer R ? (...args: Params) => Promise @@ -21,11 +15,10 @@ export type OPFSWorkerAccessHandle = { }; export async function createOPFSAccess( - filePath: string, - fileHandle: FileSystemFileHandle + filePath: string ): Promise { const postMsg = getWorkerMsger(); - await postMsg('register', { filePath, fileHandle }); + await postMsg('register', { filePath }); return { read: async (offset, size) => (await postMsg('read', { @@ -78,9 +71,7 @@ function getWorkerMsger() { } function create() { - const blob = new Blob([`(${opfsWorkerSetupStr})()`]); - const url = URL.createObjectURL(blob); - const worker = new Worker(url); + const worker = new OPFSWorker(); let cbId = 0; let cbFns: Record = {}; @@ -126,61 +117,3 @@ function getWorkerMsger() { }; } } - -const opfsWorkerSetupStr = ((): void => { - const fileAccesserMap: Record = {}; - - self.onmessage = async (e) => { - const { evtType, args } = e.data; - - let accessHandle = fileAccesserMap[args.filePath]; - - try { - let returnVal; - const trans: Transferable[] = []; - if (evtType === 'register') { - accessHandle = await args.fileHandle.createSyncAccessHandle(); - fileAccesserMap[args.filePath] = accessHandle; - } else if (evtType === 'close') { - accessHandle.close(); - delete fileAccesserMap[args.filePath]; - } else if (evtType === 'truncate') { - accessHandle.truncate(args.newSize); - } else if (evtType === 'write') { - const { data, opts } = e.data.args; - returnVal = accessHandle.write(data, opts); - } else if (evtType === 'read') { - const { offset, size } = e.data.args; - const buf = new ArrayBuffer(size); - const readLen = accessHandle.read(buf, { at: offset }); - returnVal = - readLen === size - ? buf - : // @ts-expect-error transfer support by chrome 114 - buf.transfer?.(readLen) ?? buf.slice(0, readLen); - trans.push(returnVal); - } else if (evtType === 'getSize') { - returnVal = accessHandle.getSize(); - } else if (evtType === 'flush') { - accessHandle.flush(); - } - - self.postMessage( - { - evtType: 'callback', - cbId: e.data.cbId, - returnVal, - }, - // @ts-expect-error - trans - ); - } catch (error) { - const err = error as Error; - self.postMessage({ - evtType: 'throwError', - cbId: e.data.cbId, - errMsg: err.name + ': ' + err.message + '\n' + JSON.stringify(e.data), - }); - } - }; -}).toString(); diff --git a/src/common.ts b/src/common.ts index 379bfe7..92f89aa 100644 --- a/src/common.ts +++ b/src/common.ts @@ -1,3 +1,12 @@ +export interface FileSystemSyncAccessHandle { + read: (container: ArrayBuffer, opts: { at: number }) => number; + write: (data: ArrayBuffer | ArrayBufferView, opts?: { at: number }) => number; + flush: () => void; + close: () => void; + truncate: (newSize: number) => void; + getSize: () => number; +} + export function parsePath(path: string) { if (path === '/') return { parent: null, name: '' }; diff --git a/src/file.ts b/src/file.ts index ebd39b3..be3878a 100644 --- a/src/file.ts +++ b/src/file.ts @@ -108,12 +108,7 @@ export class OPFSFileWrap { return (accPromise = new Promise(async (resolve, reject) => { try { - const fh = (await getFSHandle(this.#path, { - create: true, - isFile: true, - })) as FileSystemFileHandle; - - const accHandle = await createOPFSAccess(this.#path, fh); + const accHandle = await createOPFSAccess(this.#path); resolve([ accHandle, async () => { diff --git a/src/opfs-worker.ts b/src/opfs-worker.ts new file mode 100644 index 0000000..4bcaa10 --- /dev/null +++ b/src/opfs-worker.ts @@ -0,0 +1,63 @@ +import { FileSystemSyncAccessHandle, getFSHandle } from './common'; + +const fileAccesserMap: Record = {}; + +self.onmessage = async (e) => { + const { evtType, args } = e.data; + + let accessHandle = fileAccesserMap[args.filePath]; + + try { + let returnVal; + const trans: Transferable[] = []; + if (evtType === 'register') { + const fh = + args.fileHandle ?? + ((await getFSHandle(args.filePath, { + create: true, + isFile: true, + })) as FileSystemFileHandle); + accessHandle = await fh.createSyncAccessHandle(); + fileAccesserMap[args.filePath] = accessHandle; + } else if (evtType === 'close') { + accessHandle.close(); + delete fileAccesserMap[args.filePath]; + } else if (evtType === 'truncate') { + accessHandle.truncate(args.newSize); + } else if (evtType === 'write') { + const { data, opts } = e.data.args; + returnVal = accessHandle.write(data, opts); + } else if (evtType === 'read') { + const { offset, size } = e.data.args; + const buf = new ArrayBuffer(size); + const readLen = accessHandle.read(buf, { at: offset }); + returnVal = + readLen === size + ? buf + : // @ts-expect-error transfer support by chrome 114 + buf.transfer?.(readLen) ?? buf.slice(0, readLen); + trans.push(returnVal); + } else if (evtType === 'getSize') { + returnVal = accessHandle.getSize(); + } else if (evtType === 'flush') { + accessHandle.flush(); + } + + self.postMessage( + { + evtType: 'callback', + cbId: e.data.cbId, + returnVal, + }, + // @ts-expect-error + trans + ); + } catch (error) { + const err = error as Error; + self.postMessage({ + evtType: 'throwError', + cbId: e.data.cbId, + errMsg: err.name + ': ' + err.message + '\n' + JSON.stringify(e.data), + }); + } +};