Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
128 lines
4.7 KiB
TypeScript
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;
|
|
}
|