Files
pikasTech-HWLAB/internal/cloud/hwpod-operation-ledger.ts
T
2026-07-17 04:48:32 +02:00

109 lines
4.4 KiB
TypeScript

type HwpodOperationLedgerRecord = {
planId: string;
nodeId: string;
operation: Record<string, unknown>;
status: "started" | "accepted" | "completed";
result: any | null;
updatedAt: string;
};
export function createHwpodOperationLedger(options: any = {}) {
const runtimeStore = options.runtimeStore;
const now = options.now ?? (() => new Date().toISOString());
const retentionMs = positiveInteger(options.retentionMs, 0);
if (!runtimeStore || typeof runtimeStore.query !== "function") return createMemoryHwpodOperationLedger({ now, retentionMs });
async function get(planId: string) {
const result = await runtimeStore.query(
"SELECT operation_json FROM hardware_operations WHERE id = $1 AND operation_type = 'hwpod.node-ops' LIMIT 1",
[planId]
);
return result.rows?.[0] ? parseRecord(result.rows[0].operation_json) : null;
}
async function write(record: HwpodOperationLedgerRecord) {
await cleanup();
await runtimeStore.query(
`INSERT INTO hardware_operations (id, project_id, requested_by, operation_type, operation_json, status, requested_at, updated_at)
VALUES ($1, NULL, 'hwlab-cloud-api', 'hwpod.node-ops', $2, $3, $4, $4)
ON CONFLICT (id) DO UPDATE SET operation_json = EXCLUDED.operation_json, status = EXCLUDED.status, updated_at = EXCLUDED.updated_at`,
[record.planId, JSON.stringify(record), record.status, record.updatedAt]
);
return record;
}
async function start(input: Omit<HwpodOperationLedgerRecord, "status" | "result" | "updatedAt">) {
const existing = await get(input.planId);
return existing ?? write({ ...input, status: "started", result: null, updatedAt: now() });
}
async function accept(planId: string) {
const existing = await get(planId);
return existing ? write({ ...existing, status: "accepted", updatedAt: now() }) : null;
}
async function complete(planId: string, result: any) {
const existing = await get(planId);
return existing ? write({ ...existing, status: "completed", result, updatedAt: now() }) : null;
}
async function cleanup() {
if (!retentionMs) return;
await runtimeStore.query(
"DELETE FROM hardware_operations WHERE operation_type = 'hwpod.node-ops' AND updated_at < $1",
[new Date(Date.now() - retentionMs).toISOString()]
);
}
return { get, start, accept, complete, retentionMs };
}
export function createMemoryHwpodOperationLedger(options: any = {}) {
const now = options.now ?? (() => new Date().toISOString());
const retentionMs = positiveInteger(options.retentionMs, 0);
const records = options.records ?? new Map<string, HwpodOperationLedgerRecord>();
function cleanup() {
if (!retentionMs) return;
const cutoff = Date.now() - retentionMs;
for (const [planId, record] of records) if (Date.parse(record.updatedAt) < cutoff) records.delete(planId);
}
async function get(planId: string) { cleanup(); return records.get(planId) ?? null; }
async function start(input: Omit<HwpodOperationLedgerRecord, "status" | "result" | "updatedAt">) {
cleanup();
const existing = records.get(input.planId);
if (existing) return existing;
const record = { ...input, status: "started", result: null, updatedAt: now() } satisfies HwpodOperationLedgerRecord;
records.set(input.planId, record);
return record;
}
async function accept(planId: string) {
const existing = records.get(planId);
if (!existing) return null;
const record = { ...existing, status: "accepted", updatedAt: now() } satisfies HwpodOperationLedgerRecord;
records.set(planId, record);
return record;
}
async function complete(planId: string, result: any) {
const existing = records.get(planId);
if (!existing) return null;
const record = { ...existing, status: "completed", result, updatedAt: now() } satisfies HwpodOperationLedgerRecord;
records.set(planId, record);
return record;
}
return { get, start, accept, complete, retentionMs, records };
}
function parseRecord(value: unknown): HwpodOperationLedgerRecord | null {
try {
const parsed = typeof value === "string" ? JSON.parse(value) : value;
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as HwpodOperationLedgerRecord : null;
} catch {
return null;
}
}
function positiveInteger(value: unknown, fallback: number) {
const parsed = Number.parseInt(String(value ?? ""), 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}