import type { StorageState } from "../core/types.js"; import type { Storage } from "./interface.js"; import { compress, decompress, isCompressed } from "./compress.js"; import { encodeAllEmbeddings, decodeAllEmbeddings } from "./embeddings.js"; const STATE_KEY = "ei_state"; const BACKUP_KEY = "ei_state_backup"; export class LocalStorage implements Storage { getDataPath(): string { return ""; } async isAvailable(): Promise { try { const testKey = "__ei_storage_test__"; globalThis.localStorage.setItem(testKey, "1"); globalThis.localStorage.removeItem(testKey); return true; } catch { return false; } } async save(state: StorageState): Promise { state.timestamp = new Date().toISOString(); try { const json = JSON.stringify(encodeAllEmbeddings(state)); const payload = await compress(json); globalThis.localStorage.setItem(STATE_KEY, payload); } catch (e) { if (this.isQuotaError(e)) { throw new Error("STORAGE_SAVE_FAILED: localStorage quota exceeded"); } throw e; } } async load(): Promise { const current = globalThis.localStorage?.getItem(STATE_KEY); if (current) { try { const json = isCompressed(current) ? await decompress(current) : current; return decodeAllEmbeddings(JSON.parse(json) as StorageState); } catch { return null; } } return null; } /** * Move current state to backup location and clear primary state. * Used after successful remote sync to signal "no local state to load" on next launch. * Backup can be restored manually if remote pull fails. */ async moveToBackup(): Promise { const current = globalThis.localStorage?.getItem(STATE_KEY); if (current) { // Remove primary first so backup write doesn't double-count against quota. globalThis.localStorage.removeItem(STATE_KEY); globalThis.localStorage.setItem(BACKUP_KEY, current); } } /** * Read backup state without removing it. * Used to peek sync credentials from a previous session's backup. */ async loadBackup(): Promise { const backup = globalThis.localStorage?.getItem(BACKUP_KEY); if (backup) { try { const json = isCompressed(backup) ? await decompress(backup) : backup; return decodeAllEmbeddings(JSON.parse(json) as StorageState); } catch { return null; } } return null; } private isQuotaError(e: unknown): boolean { return ( e instanceof DOMException && (e.name === "QuotaExceededError" || e.name === "NS_ERROR_DOM_QUOTA_REACHED") ); } /** No-op in browser — rolling backups are TUI-only (filesystem required). */ async saveRollingBackup(_state: StorageState, _maxBackups: number): Promise { // Intentional no-op: localStorage has no directory/file concept. // The Processor gates this call with `this.isTUI` so it never runs in the browser. } }