import { createWorker, isSafariMobile } from '../utils'; import { OPFS_UTILS_WORKER_CODE } from '../workers-code/generated'; import type { StorageBackend } from './index'; export class OPFSBackend implements StorageBackend { isSupported(): boolean { return ( typeof navigator !== 'undefined' && 'storage' in navigator && !!navigator.storage?.getDirectory ); } async read(key: string): Promise { try { const cacheDir = await getCacheDir(); const fileHandle = await cacheDir.getFileHandle(key); return await fileHandle.getFile(); } catch (e) { // NotFoundError or similar return null; } } async write(key: string, stream: ReadableStream): Promise { const writable = await openWritable(key); await writable.truncate(0); const reader = stream.getReader(); try { while (true) { const { done, value } = await reader.read(); if (done) break; await writable.write(value); } } finally { await writable.close(); } } async getSize(key: string): Promise { try { const cacheDir = await getCacheDir(); const fileHandle = await cacheDir.getFileHandle(key); const file = await fileHandle.getFile(); return file.size; } catch (e) { return -1; } } async list(): Promise> { const cacheDir = await getCacheDir(); const result: Array<{ key: string; size: number }> = []; // @ts-ignore for await (const [name, handle] of cacheDir.entries()) { if (handle.kind === 'file') { const file = await (handle as FileSystemFileHandle).getFile(); result.push({ key: name, size: file.size }); } } return result; } async delete(key: string): Promise { try { const cacheDir = await getCacheDir(); await cacheDir.removeEntry(key); } catch (e: any) { if (e?.name !== 'NotFoundError') throw e; } } } async function getCacheDir(): Promise { const opfsRoot = await navigator.storage.getDirectory(); return opfsRoot.getDirectoryHandle('cache', { create: true }); } async function openWritable(fileName: string): Promise<{ truncate(offset: number): Promise; write(value: Uint8Array): Promise; close(): Promise; }> { const worker = createWorker(OPFS_UTILS_WORKER_CODE); let pResolve: (v: any) => void; let pReject: (v: any) => void; worker.onmessage = (e: MessageEvent) => { if (e.data.ok) pResolve(null); else if (e.data.err) pReject(e.data.err); }; worker.onerror = (e) => pReject?.(e.message ?? e); const workerExec = ( data: | { action: 'open'; filename: string } | { action: 'write'; buf: Uint8Array } | { action: 'close' } ) => new Promise((resolve, reject) => { pResolve = resolve; pReject = reject; worker.postMessage( data, isSafariMobile() ? undefined : { transfer: 'buf' in data && data.buf ? [data.buf.buffer] : [] } ); }); await workerExec({ action: 'open', filename: fileName }); return { truncate: async () => { /* worker's openFile already calls truncate(0) on open */ }, write: (value) => workerExec({ action: 'write', buf: value }), close: async () => { await workerExec({ action: 'close' }); worker.terminate(); }, }; }