57 lines
2.3 KiB
TypeScript
57 lines
2.3 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;
|
|
}
|
|
|
|
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 && 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;
|
|
}
|