import type { SyncEvent } from './types.js'; import { parseEventFilename } from './utils.js'; const EVENTS_DIR = 'events'; export class FolderStore { private dirHandle: FileSystemDirectoryHandle | null = null; // ── Handle management ────────────────────────────────────── get hasHandle(): boolean { return this.dirHandle !== null; } getHandle(): FileSystemDirectoryHandle | null { return this.dirHandle; } setHandle(handle: FileSystemDirectoryHandle): void { this.dirHandle = handle; } async selectFolder(): Promise { if (typeof window.showDirectoryPicker !== 'function') { throw new Error( 'File System Access API is not supported in this browser. ' + 'Use a Chromium-based browser (Chrome, Edge, Brave, etc.).', ); } this.dirHandle = await window.showDirectoryPicker({ mode: 'readwrite', }); // Ensure events directory exists await this.dirHandle.getDirectoryHandle(EVENTS_DIR, { create: true }); return this.dirHandle; } // ── Permission checks ───────────────────────────────────── async hasPermission(): Promise { if (!this.dirHandle) return false; try { const perm = await this.dirHandle.queryPermission({ mode: 'readwrite' }); return perm === 'granted'; } catch { return false; } } async requestPermission(): Promise { if (!this.dirHandle) return false; try { const perm = await this.dirHandle.requestPermission({ mode: 'readwrite', }); return perm === 'granted'; } catch { return false; } } // ── Write event file ────────────────────────────────────── async writeEvent(filename: string, event: SyncEvent): Promise { const eventsDir = await this.getEventsDir(); const fileHandle = await eventsDir.getFileHandle(filename, { create: true, }); const writable = await fileHandle.createWritable(); try { await writable.write(JSON.stringify(event, null, 2)); } finally { await writable.close(); } } // ── Scan events ──────────────────────────────────────────── async scanEventFilenames(): Promise { const eventsDir = await this.getEventsDir(); const names: string[] = []; for await (const [name, handle] of eventsDir.entries()) { if (handle.kind === 'file' && name.endsWith('.json')) { if (parseEventFilename(name)) { names.push(name); } } } // Sort by (timestamp, full filename) for deterministic ordering names.sort((a, b) => { const pa = parseEventFilename(a)!; const pb = parseEventFilename(b)!; if (pa.ts !== pb.ts) return pa.ts - pb.ts; return a.localeCompare(b); }); return names; } async readEventFile(filename: string): Promise { const eventsDir = await this.getEventsDir(); try { const fh = await eventsDir.getFileHandle(filename); const file = await fh.getFile(); const text = await file.text(); return JSON.parse(text) as SyncEvent; } catch { return null; } } // ── Data files (SQLite database persistence) ──────────────── async writeDataFile(filename: string, data: Uint8Array): Promise { const dataDir = await this.getDataDir(); const fh = await dataDir.getFileHandle(filename, { create: true }); const writable = await fh.createWritable(); try { await writable.write(data.buffer as ArrayBuffer); } finally { await writable.close(); } } async readDataFile(filename: string): Promise { try { const dataDir = await this.getDataDir(); const fh = await dataDir.getFileHandle(filename); const file = await fh.getFile(); const buf = await file.arrayBuffer(); return new Uint8Array(buf); } catch { return null; } } // ── Internals ────────────────────────────────────────────── private async getEventsDir(): Promise { if (!this.dirHandle) { throw new Error('No folder selected. Call selectFolder() first.'); } return this.dirHandle.getDirectoryHandle(EVENTS_DIR, { create: true }); } private async getDataDir(): Promise { if (!this.dirHandle) { throw new Error('No folder selected. Call selectFolder() first.'); } return this.dirHandle.getDirectoryHandle('data', { create: true }); } }