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; } 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; const nodes: Record = {}; 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; 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): 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; }