Files
NODEDC_MISSION_CORE/plugins/xgrids-k1/frontend/src/stateOrdering.ts
T
DCCONSTRUCTIONS 0ca7316a24 wip(k1): checkpoint connection recovery rewrite
Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
2026-08-14 14:57:50 +03:00

128 lines
4.7 KiB
TypeScript

// Ordering rules remain plugin-private because revisions are vendor runtime state.
import type { XgridsCameraPreviewState, XgridsK1State } from "./api";
function monotonicInteger(value: number | null | undefined): number | null {
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
}
function deviceSessionScope(state: XgridsK1State): string | null {
const sessionId = state.device_session?.device_session_id;
return typeof sessionId === "string" && sessionId.trim() ? sessionId : null;
}
interface RuntimeSnapshotStamp {
startedAtMonotonicNs: bigint | null;
startedAtEpochMs: number | null;
runtimeId: string;
revision: number;
}
function monotonicNanoseconds(value: string | null | undefined): bigint | null {
if (typeof value !== "string" || !/^(0|[1-9][0-9]*)$/.test(value)) return null;
try {
return BigInt(value);
} catch {
return null;
}
}
function runtimeSnapshotStamp(state: XgridsK1State): RuntimeSnapshotStamp | null {
const startedAt = state.snapshot_runtime_started_at_utc;
const startedAtMonotonicNs = monotonicNanoseconds(
state.snapshot_runtime_started_monotonic_ns,
);
const runtimeId = state.snapshot_runtime_id;
const revision = monotonicInteger(state.snapshot_revision);
if (
typeof runtimeId !== "string"
|| !runtimeId.trim()
|| revision === null
) {
return null;
}
const parsedEpochMs = typeof startedAt === "string" ? Date.parse(startedAt) : Number.NaN;
const startedAtEpochMs = Number.isFinite(parsedEpochMs) ? parsedEpochMs : null;
if (startedAtMonotonicNs === null && startedAtEpochMs === null) return null;
return { startedAtMonotonicNs, startedAtEpochMs, runtimeId, revision };
}
function stampedSnapshotIsAtLeastAsNew(
current: RuntimeSnapshotStamp,
incoming: RuntimeSnapshotStamp,
): boolean {
if (incoming.runtimeId === current.runtimeId) {
return incoming.revision >= current.revision;
}
if (incoming.startedAtMonotonicNs !== null || current.startedAtMonotonicNs !== null) {
if (incoming.startedAtMonotonicNs === null) return false;
if (current.startedAtMonotonicNs === null) return true;
return incoming.startedAtMonotonicNs > current.startedAtMonotonicNs;
}
if (
incoming.startedAtEpochMs !== null
&& current.startedAtEpochMs !== null
&& incoming.startedAtEpochMs !== current.startedAtEpochMs
) {
return incoming.startedAtEpochMs > current.startedAtEpochMs;
}
// Legacy UTC-only process identities with equal timestamps cannot be
// ordered safely. Keep the already accepted authority.
return false;
}
function cameraSnapshotIsAtLeastAsNew(
current: XgridsCameraPreviewState,
incoming: XgridsCameraPreviewState,
): boolean {
const currentRevision = monotonicInteger(current.revision);
const incomingRevision = monotonicInteger(incoming.revision);
if (currentRevision !== null || incomingRevision !== null) {
if (incomingRevision === null) return false;
if (currentRevision === null) return true;
if (incomingRevision !== currentRevision) return incomingRevision > currentRevision;
}
const currentGeneration = monotonicInteger(current.generation);
const incomingGeneration = monotonicInteger(incoming.generation);
if (currentGeneration === null) return true;
if (incomingGeneration === null) return false;
return incomingGeneration >= currentGeneration;
}
/**
* Select an authoritative runtime snapshot without allowing asynchronous REST,
* mutation, or event responses to roll camera preview state backwards.
*
* Revision is the primary ordering key. In particular, a stop snapshot with a
* newer revision and a null generation is valid and must replace the active
* generation. Generation is only a tie-breaker when revisions are equal or
* absent.
*/
export function selectMonotonicXgridsState(
current: XgridsK1State | null,
incoming: XgridsK1State,
): XgridsK1State {
if (!current) return incoming;
const currentStamp = runtimeSnapshotStamp(current);
const incomingStamp = runtimeSnapshotStamp(incoming);
if (currentStamp || incomingStamp) {
if (!currentStamp) return incoming;
if (!incomingStamp) return current;
return stampedSnapshotIsAtLeastAsNew(currentStamp, incomingStamp)
? incoming
: current;
}
if (current && deviceSessionScope(current) !== deviceSessionScope(incoming)) {
return incoming;
}
if (!current?.camera_preview) return incoming;
if (!incoming.camera_preview) return current;
// Backend snapshots are atomic. Reject the whole stale response instead of
// combining camera/catalog/metrics fields captured at different moments.
return cameraSnapshotIsAtLeastAsNew(current.camera_preview, incoming.camera_preview)
? incoming
: current;
}