diff options
Diffstat (limited to 'src/state.ts')
| -rw-r--r-- | src/state.ts | 90 |
1 files changed, 90 insertions, 0 deletions
diff --git a/src/state.ts b/src/state.ts new file mode 100644 index 0000000..c5621fe --- /dev/null +++ b/src/state.ts @@ -0,0 +1,90 @@ +import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; + +/** + * Small service-state file kept next to the Matter fabric storage, with one + * entry per commissioned node (keyed by node ID as a decimal string). Written + * atomically (temp file + rename) so a power loss mid-write never corrupts it. + */ +export interface NodeState { + lastSuccessfulConnection: string | null; + lastSuccessfulSync: string | null; + lastAttemptedSync: string | null; + lastError: string | null; +} + +export interface ServiceState { + nodes: Record<string, NodeState>; +} + +export const EMPTY_NODE_STATE: NodeState = { + lastSuccessfulConnection: null, + lastSuccessfulSync: null, + lastAttemptedSync: null, + lastError: null, +}; + +export function serviceStatePath(storagePath: string): string { + return join(storagePath, "service-state.json"); +} + +export function readServiceState(path: string): ServiceState { + let raw: string; + try { + raw = readFileSync(path, "utf8"); + } catch { + return { nodes: {} }; + } + try { + const parsed = JSON.parse(raw) as Partial<ServiceState>; + const nodes: Record<string, NodeState> = {}; + if (typeof parsed.nodes === "object" && parsed.nodes !== null) { + for (const [nodeId, value] of Object.entries(parsed.nodes)) { + if (!/^\d+$/.test(nodeId) || typeof value !== "object" || value === null) continue; + const entry = value as Partial<NodeState>; + nodes[nodeId] = { + lastSuccessfulConnection: stringOrNull(entry.lastSuccessfulConnection), + lastSuccessfulSync: stringOrNull(entry.lastSuccessfulSync), + lastAttemptedSync: stringOrNull(entry.lastAttemptedSync), + lastError: stringOrNull(entry.lastError), + }; + } + } + return { nodes }; + } catch { + // A corrupt state file is not fatal; it only holds status metadata. + return { nodes: {} }; + } +} + +export function writeServiceState(path: string, state: ServiceState): void { + mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); + const temp = `${path}.tmp`; + writeFileSync(temp, JSON.stringify(state, null, 2) + "\n", { mode: 0o600 }); + renameSync(temp, path); +} + +export function nodeState(state: ServiceState, nodeId: bigint): NodeState { + return state.nodes[nodeId.toString()] ?? { ...EMPTY_NODE_STATE }; +} + +/** Merges a patch into one node's entry and persists the result. */ +export function updateNodeState(path: string, nodeId: bigint, patch: Partial<NodeState>): ServiceState { + const state = readServiceState(path); + const key = nodeId.toString(); + state.nodes[key] = { ...(state.nodes[key] ?? EMPTY_NODE_STATE), ...patch }; + writeServiceState(path, state); + return state; +} + +/** Drops one node's entry (after decommissioning) and persists the result. */ +export function removeNodeState(path: string, nodeId: bigint): ServiceState { + const state = readServiceState(path); + delete state.nodes[nodeId.toString()]; + writeServiceState(path, state); + return state; +} + +function stringOrNull(value: unknown): string | null { + return typeof value === "string" ? value : null; +} |
