// Append-only shard event log. The WS ingest dispatcher calls append() for the // notable kinds; the public/admin read endpoints call list(). The DB layer only // sees an already-computed dedupe_key so INSERT IGNORE is idempotent across // WS-reconnect backfill. const crypto = require('crypto') const db = require('./shardEvents.db') const MAX_LIMIT = 1000 const DEFAULT_LIMIT = 100 // Stable stringify — keys sorted — so the dedupe hash is independent of the // property order the sidecar happened to serialize with. function stableStringify(value) { if (value === null || typeof value !== 'object') return JSON.stringify(value) if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]` const keys = Object.keys(value).sort() return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}` } // dedupe_key = sha1(kind + t + stable-json(payload)). Two identical events (same // kind, same timestamp, same body) collapse to one row. function dedupeKey(kind, t, payload) { return crypto.createHash('sha1').update(`${kind}|${t}|${stableStringify(payload)}`).digest('hex') } // Append one event. Returns true if a new row was inserted (false = deduped). async function append({ kind, t, bootId, payload }) { return db.insertIgnore({ kind, t, bootId, payload, dedupeKey: dedupeKey(kind, t, payload) }) } function normalizeLimit(limit) { const n = Number(limit) if (!Number.isFinite(n) || n <= 0) return DEFAULT_LIMIT return Math.min(Math.floor(n), MAX_LIMIT) } // Recent events, newest first. Each row's JSON payload is parsed back to an object. async function list({ kind, limit } = {}) { const rows = await db.list({ kind, limit: normalizeLimit(limit) }) return rows.map((row) => ({ id: row.id, kind: row.kind, t: row.t, bootId: row.boot_id || null, // mariadb returns JSON columns as strings on some versions; parse defensively. payload: typeof row.payload === 'string' ? JSON.parse(row.payload) : row.payload, createdAt: row.created_at, })) } module.exports = { append, list, dedupeKey }