import { Platform, usePlatform } from "@/context/platform" import { makePersisted, type AsyncStorage, type SyncStorage } from "@solid-primitives/storage" import { checksum } from "@opencode-ai/util/encode" import { createResource, type Accessor } from "solid-js" import type { SetStoreFunction, Store } from "solid-js/store" type InitType = Promise | string | null type PersistedWithReady = [Store, SetStoreFunction, InitType, Accessor] type PersistTarget = { storage?: string key: string legacy?: string[] migrate?: (value: unknown) => unknown } const LEGACY_STORAGE = "default.dat" const GLOBAL_STORAGE = "opencode.global.dat" const LOCAL_PREFIX = "opencode." const fallback = new Map() const CACHE_MAX_ENTRIES = 500 const CACHE_MAX_BYTES = 8 * 1024 * 1024 type CacheEntry = { value: string; bytes: number } const cache = new Map() const cacheTotal = { bytes: 0 } function cacheDelete(key: string) { const entry = cache.get(key) if (!entry) return cacheTotal.bytes -= entry.bytes cache.delete(key) } function cachePrune() { for (;;) { if (cache.size <= CACHE_MAX_ENTRIES && cacheTotal.bytes <= CACHE_MAX_BYTES) return const oldest = cache.keys().next().value as string | undefined if (!oldest) return cacheDelete(oldest) } } function cacheSet(key: string, value: string) { const bytes = value.length * 2 if (bytes > CACHE_MAX_BYTES) { cacheDelete(key) return } const entry = cache.get(key) if (entry) cacheTotal.bytes -= entry.bytes cache.delete(key) cache.set(key, { value, bytes }) cacheTotal.bytes += bytes cachePrune() } function cacheGet(key: string) { const entry = cache.get(key) if (!entry) return cache.delete(key) cache.set(key, entry) return entry.value } function fallbackDisabled(scope: string) { return fallback.get(scope) === true } function fallbackSet(scope: string) { fallback.set(scope, true) } function quota(error: unknown) { if (error instanceof DOMException) { if (error.name === "QuotaExceededError") return true if (error.name === "NS_ERROR_DOM_QUOTA_REACHED") return true if (error.name === "QUOTA_EXCEEDED_ERR") return true if (error.code === 22 || error.code === 1014) return true return false } if (!error || typeof error !== "object") return false const name = (error as { name?: string }).name if (name === "QuotaExceededError" || name === "NS_ERROR_DOM_QUOTA_REACHED") return true if (name && /quota/i.test(name)) return true const code = (error as { code?: number }).code if (code === 22 || code === 1014) return true const message = (error as { message?: string }).message if (typeof message !== "string") return false if (/quota/i.test(message)) return true return false } type Evict = { key: string; size: number } function evict(storage: Storage, keep: string, value: string) { const total = storage.length const indexes = Array.from({ length: total }, (_, index) => index) const items: Evict[] = [] for (const index of indexes) { const name = storage.key(index) if (!name) continue if (!name.startsWith(LOCAL_PREFIX)) continue if (name === keep) continue const stored = storage.getItem(name) items.push({ key: name, size: stored?.length ?? 0 }) } items.sort((a, b) => b.size - a.size) for (const item of items) { storage.removeItem(item.key) cacheDelete(item.key) try { storage.setItem(keep, value) cacheSet(keep, value) return true } catch (error) { if (!quota(error)) throw error } } return false } function write(storage: Storage, key: string, value: string) { try { storage.setItem(key, value) cacheSet(key, value) return true } catch (error) { if (!quota(error)) throw error } try { storage.removeItem(key) cacheDelete(key) storage.setItem(key, value) cacheSet(key, value) return true } catch (error) { if (!quota(error)) throw error } const ok = evict(storage, key, value) return ok } function snapshot(value: unknown) { return JSON.parse(JSON.stringify(value)) as unknown } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value) } function merge(defaults: unknown, value: unknown): unknown { if (value === undefined) return defaults if (value === null) return value if (Array.isArray(defaults)) { if (Array.isArray(value)) return value return defaults } if (isRecord(defaults)) { if (!isRecord(value)) return defaults const result: Record = { ...defaults } for (const key of Object.keys(value)) { if (key in defaults) { result[key] = merge((defaults as Record)[key], (value as Record)[key]) } else { result[key] = (value as Record)[key] } } return result } return value } function parse(value: string) { try { return JSON.parse(value) as unknown } catch { return undefined } } function normalize(defaults: unknown, raw: string, migrate?: (value: unknown) => unknown) { const parsed = parse(raw) if (parsed === undefined) return const migrated = migrate ? migrate(parsed) : parsed const merged = merge(defaults, migrated) return JSON.stringify(merged) } function workspaceStorage(dir: string) { const head = dir.slice(0, 12) || "workspace" const sum = checksum(dir) ?? "0" return `opencode.workspace.${head}.${sum}.dat` } function localStorageWithPrefix(prefix: string): SyncStorage { const base = `${prefix}:` const scope = `prefix:${prefix}` const item = (key: string) => base + key return { getItem: (key) => { const name = item(key) const cached = cacheGet(name) if (fallbackDisabled(scope)) return cached ?? null const stored = (() => { try { return localStorage.getItem(name) } catch { fallbackSet(scope) return null } })() if (stored === null) return cached ?? null cacheSet(name, stored) return stored }, setItem: (key, value) => { const name = item(key) if (fallbackDisabled(scope)) return try { if (write(localStorage, name, value)) return } catch { fallbackSet(scope) return } fallbackSet(scope) }, removeItem: (key) => { const name = item(key) cacheDelete(name) if (fallbackDisabled(scope)) return try { localStorage.removeItem(name) } catch { fallbackSet(scope) } }, } } function localStorageDirect(): SyncStorage { const scope = "direct" return { getItem: (key) => { const cached = cacheGet(key) if (fallbackDisabled(scope)) return cached ?? null const stored = (() => { try { return localStorage.getItem(key) } catch { fallbackSet(scope) return null } })() if (stored === null) return cached ?? null cacheSet(key, stored) return stored }, setItem: (key, value) => { if (fallbackDisabled(scope)) return try { if (write(localStorage, key, value)) return } catch { fallbackSet(scope) return } fallbackSet(scope) }, removeItem: (key) => { cacheDelete(key) if (fallbackDisabled(scope)) return try { localStorage.removeItem(key) } catch { fallbackSet(scope) } }, } } export const PersistTesting = { localStorageDirect, localStorageWithPrefix, normalize, } export const Persist = { global(key: string, legacy?: string[]): PersistTarget { return { storage: GLOBAL_STORAGE, key, legacy } }, workspace(dir: string, key: string, legacy?: string[]): PersistTarget { return { storage: workspaceStorage(dir), key: `workspace:${key}`, legacy } }, session(dir: string, session: string, key: string, legacy?: string[]): PersistTarget { return { storage: workspaceStorage(dir), key: `session:${session}:${key}`, legacy } }, scoped(dir: string, session: string | undefined, key: string, legacy?: string[]): PersistTarget { if (session) return Persist.session(dir, session, key, legacy) return Persist.workspace(dir, key, legacy) }, } export function removePersisted(target: { storage?: string; key: string }, platform?: Platform) { const isDesktop = platform?.platform === "desktop" && !!platform.storage if (isDesktop) { return platform.storage?.(target.storage)?.removeItem(target.key) } if (!target.storage) { localStorageDirect().removeItem(target.key) return } localStorageWithPrefix(target.storage).removeItem(target.key) } export function persisted( target: string | PersistTarget, store: [Store, SetStoreFunction], ): PersistedWithReady { const platform = usePlatform() const config: PersistTarget = typeof target === "string" ? { key: target } : target const defaults = snapshot(store[0]) const legacy = config.legacy ?? [] const isDesktop = platform.platform === "desktop" && !!platform.storage const currentStorage = (() => { if (isDesktop) return platform.storage?.(config.storage) if (!config.storage) return localStorageDirect() return localStorageWithPrefix(config.storage) })() const legacyStorage = (() => { if (!isDesktop) return localStorageDirect() if (!config.storage) return platform.storage?.() return platform.storage?.(LEGACY_STORAGE) })() const storage = (() => { if (!isDesktop) { const current = currentStorage as SyncStorage const legacyStore = legacyStorage as SyncStorage const api: SyncStorage = { getItem: (key) => { const raw = current.getItem(key) if (raw !== null) { const next = normalize(defaults, raw, config.migrate) if (next === undefined) { current.removeItem(key) return null } if (raw !== next) current.setItem(key, next) return next } for (const legacyKey of legacy) { const legacyRaw = legacyStore.getItem(legacyKey) if (legacyRaw === null) continue const next = normalize(defaults, legacyRaw, config.migrate) if (next === undefined) { legacyStore.removeItem(legacyKey) continue } current.setItem(key, next) legacyStore.removeItem(legacyKey) return next } return null }, setItem: (key, value) => { current.setItem(key, value) }, removeItem: (key) => { current.removeItem(key) }, } return api } const current = currentStorage as AsyncStorage const legacyStore = legacyStorage as AsyncStorage | undefined const api: AsyncStorage = { getItem: async (key) => { const raw = await current.getItem(key) if (raw !== null) { const next = normalize(defaults, raw, config.migrate) if (next === undefined) { await current.removeItem(key).catch(() => undefined) return null } if (raw !== next) await current.setItem(key, next) return next } if (!legacyStore) return null for (const legacyKey of legacy) { const legacyRaw = await legacyStore.getItem(legacyKey) if (legacyRaw === null) continue const next = normalize(defaults, legacyRaw, config.migrate) if (next === undefined) { await legacyStore.removeItem(legacyKey).catch(() => undefined) continue } await current.setItem(key, next) await legacyStore.removeItem(legacyKey) return next } return null }, setItem: async (key, value) => { await current.setItem(key, value) }, removeItem: async (key) => { await current.removeItem(key) }, } return api })() const [state, setState, init] = makePersisted(store, { name: config.key, storage }) const isAsync = init instanceof Promise const [ready] = createResource( () => init, async (initValue) => { if (initValue instanceof Promise) await initValue return true }, { initialValue: !isAsync }, ) return [state, setState, init, () => ready() === true] }