| 1 | import type { RemoteSessionView } from "./types"; |
| 2 | |
| 3 | const REMOTE_SESSIONS_CACHE_PREFIX = "projectTree:remoteSessions:"; |
| 4 | const REMOTE_SESSION_CACHE_VERSION = 1; |
| 5 | export const REMOTE_SESSION_CACHE_TTL_MS = 5 * 60 * 1000; |
| 6 | |
| 7 | interface RemoteSessionCacheEnvelope { |
| 8 | version: typeof REMOTE_SESSION_CACHE_VERSION; |
| 9 | savedAt: number; |
| 10 | rows: RemoteSessionView[]; |
| 11 | } |
| 12 | |
| 13 | function cacheKey(groupKey: string): string { |
| 14 | return REMOTE_SESSIONS_CACHE_PREFIX + groupKey; |
| 15 | } |
| 16 | |
| 17 | function parseRows(value: unknown): RemoteSessionView[] | undefined { |
| 18 | if (!Array.isArray(value)) return undefined; |
| 19 | const rows: RemoteSessionView[] = []; |
| 20 | for (const row of value) { |
| 21 | if (!row || typeof row !== "object") return undefined; |
| 22 | const candidate = row as Partial<RemoteSessionView>; |
| 23 | if (typeof candidate.name !== "string") return undefined; |
| 24 | const valid = (candidate.title === undefined || typeof candidate.title === "string") |
| 25 | && (candidate.turns === undefined || typeof candidate.turns === "number" && Number.isFinite(candidate.turns)) |
| 26 | && (candidate.path === undefined || typeof candidate.path === "string") |
| 27 | && (candidate.current === undefined || typeof candidate.current === "boolean") |
| 28 | && (candidate.running === undefined || typeof candidate.running === "boolean") |
| 29 | && (candidate.lastActivityAt === undefined |
| 30 | || typeof candidate.lastActivityAt === "number" && Number.isFinite(candidate.lastActivityAt)) |
| 31 | && (candidate.pinned === undefined || typeof candidate.pinned === "boolean"); |
| 32 | if (!valid) return undefined; |
| 33 | rows.push({ |
| 34 | ...candidate, |
| 35 | name: candidate.name, |
| 36 | title: candidate.title ?? "", |
| 37 | turns: candidate.turns ?? 0, |
| 38 | }); |
| 39 | } |
| 40 | return rows; |
| 41 | } |
| 42 | |
| 43 | export function removeRemoteSessionCache(groupKey: string): void { |
| 44 | try { |
| 45 | if (typeof localStorage !== "undefined") localStorage.removeItem(cacheKey(groupKey)); |
| 46 | } catch { |
| 47 | // localStorage is an optional optimistic-paint cache. |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | export function saveRemoteSessionCache(groupKey: string, rows: RemoteSessionView[], now = Date.now()): void { |
| 52 | try { |
| 53 | if (typeof localStorage === "undefined") return; |
| 54 | const durableRows = rows.filter((row) => row.name.trim() !== ""); |
| 55 | const envelope: RemoteSessionCacheEnvelope = { |
| 56 | version: REMOTE_SESSION_CACHE_VERSION, |
| 57 | savedAt: now, |
| 58 | rows: durableRows, |
| 59 | }; |
| 60 | localStorage.setItem(cacheKey(groupKey), JSON.stringify(envelope)); |
| 61 | } catch { |
| 62 | // localStorage can be unavailable or full; live state remains authoritative. |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | export function loadRemoteSessionCache(groupKey: string, now = Date.now()): RemoteSessionView[] { |
| 67 | try { |
| 68 | if (typeof localStorage === "undefined") return []; |
| 69 | const raw = localStorage.getItem(cacheKey(groupKey)); |
| 70 | if (!raw) return []; |
| 71 | const parsed = JSON.parse(raw) as Partial<RemoteSessionCacheEnvelope> | null; |
| 72 | const rows = parseRows(parsed?.rows); |
| 73 | const fresh = parsed?.version === REMOTE_SESSION_CACHE_VERSION |
| 74 | && typeof parsed.savedAt === "number" |
| 75 | && parsed.savedAt <= now |
| 76 | && now - parsed.savedAt <= REMOTE_SESSION_CACHE_TTL_MS |
| 77 | && rows !== undefined; |
| 78 | if (!fresh) { |
| 79 | removeRemoteSessionCache(groupKey); |
| 80 | return []; |
| 81 | } |
| 82 | return rows; |
| 83 | } catch { |
| 84 | removeRemoteSessionCache(groupKey); |
| 85 | return []; |
| 86 | } |
| 87 | } |
| 88 |