feat(plugins): isolate device integrations
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import type { RuntimePhase, SourceMode as RuntimeSourceMode } from "@mission-core/plugin-sdk";
|
||||
|
||||
import type {
|
||||
AcquisitionState,
|
||||
XgridsAcquisition,
|
||||
XgridsK1State,
|
||||
XgridsOperation,
|
||||
} from "./api";
|
||||
|
||||
const TERMINAL_ACQUISITION_STATES = new Set<AcquisitionState>([
|
||||
"completed",
|
||||
"failed",
|
||||
"aborted",
|
||||
"interrupted",
|
||||
]);
|
||||
|
||||
const FAILED_OPERATION_STATUSES = new Set([
|
||||
"failed",
|
||||
"cancelled",
|
||||
"timed_out",
|
||||
"interrupted",
|
||||
]);
|
||||
|
||||
export type LiveStartPlan = "prepare" | "resume-prepared" | "already-running" | "blocked";
|
||||
|
||||
export function isTerminalAcquisitionState(
|
||||
state: AcquisitionState | null | undefined,
|
||||
): boolean {
|
||||
return state ? TERMINAL_ACQUISITION_STATES.has(state) : false;
|
||||
}
|
||||
|
||||
export function recoverableAcquisition(
|
||||
state: XgridsK1State | null | undefined,
|
||||
): XgridsAcquisition | null {
|
||||
const acquisition = state?.acquisition;
|
||||
return acquisition && !isTerminalAcquisitionState(acquisition.state) ? acquisition : null;
|
||||
}
|
||||
|
||||
export function isConfirmedLiveState(state: XgridsK1State | null | undefined): boolean {
|
||||
return state?.source_mode === "live" && state.acquisition?.state === "acquiring";
|
||||
}
|
||||
|
||||
export function isSourceRuntimeBusy(state: XgridsK1State | null | undefined): boolean {
|
||||
return state?.source_mode === "live" || state?.source_mode === "replay";
|
||||
}
|
||||
|
||||
export function confirmedRuntimeSourceMode(
|
||||
state: XgridsK1State | null | undefined,
|
||||
): RuntimeSourceMode {
|
||||
if (state?.source_mode === "replay") return "replay";
|
||||
if (isConfirmedLiveState(state)) return "live";
|
||||
return "idle";
|
||||
}
|
||||
|
||||
export function effectiveAcquisition(
|
||||
state: XgridsK1State | null | undefined,
|
||||
): XgridsAcquisition | null {
|
||||
if (state?.source_mode === "replay") return null;
|
||||
return state?.acquisition ?? null;
|
||||
}
|
||||
|
||||
export function liveStartPlan(state: XgridsK1State | null | undefined): LiveStartPlan {
|
||||
if (state?.source_mode === "replay") return "blocked";
|
||||
const acquisition = recoverableAcquisition(state);
|
||||
if (!acquisition) return state?.source_mode === "live" ? "blocked" : "prepare";
|
||||
if (acquisition.state === "prepared") return "resume-prepared";
|
||||
if (
|
||||
acquisition.state === "starting" ||
|
||||
acquisition.state === "awaiting_external_start" ||
|
||||
acquisition.state === "acquiring"
|
||||
) {
|
||||
return "already-running";
|
||||
}
|
||||
return "blocked";
|
||||
}
|
||||
|
||||
export function normalizeRuntimePhase(
|
||||
state: XgridsK1State | null | undefined,
|
||||
): RuntimePhase {
|
||||
const phase = state?.phase;
|
||||
const acquisitionState = effectiveAcquisition(state)?.state;
|
||||
if (acquisitionState === "failed" || acquisitionState === "interrupted") return "error";
|
||||
if (acquisitionState === "awaiting_external_start" || acquisitionState === "starting") {
|
||||
return "starting";
|
||||
}
|
||||
if (acquisitionState === "acquiring") return "streaming";
|
||||
if (
|
||||
acquisitionState === "awaiting_external_stop" ||
|
||||
acquisitionState === "stopping" ||
|
||||
acquisitionState === "finalizing"
|
||||
) {
|
||||
return "stopping";
|
||||
}
|
||||
if (acquisitionState === "prepared") return "connected";
|
||||
if (phase === "error") return "error";
|
||||
if (phase === "connected") return "connected";
|
||||
if (phase === "starting_live") return "starting";
|
||||
if (phase === "live") return "streaming";
|
||||
if (phase === "replay") return "replaying";
|
||||
if (phase === "stopping") return "stopping";
|
||||
if (["scanning", "device_selected", "provisioning", "connecting"].includes(phase ?? "")) {
|
||||
return "configuring";
|
||||
}
|
||||
return "idle";
|
||||
}
|
||||
|
||||
export function spatialSourceId(
|
||||
state: XgridsK1State | null | undefined,
|
||||
sourceUrl: string,
|
||||
): string | null {
|
||||
if (!sourceUrl) return null;
|
||||
if (state?.source_mode === "replay") return `replay:${sourceUrl}`;
|
||||
return state?.acquisition?.acquisition_id ?? sourceUrl;
|
||||
}
|
||||
|
||||
export function sourceStatusLabel(state: XgridsK1State | null | undefined): string {
|
||||
if (state?.source_mode === "replay") return "Повтор записи";
|
||||
if (isConfirmedLiveState(state)) return "Реальное время · данные подтверждены";
|
||||
if (state?.source_mode === "live") {
|
||||
if (state.acquisition?.state === "failed" || state.phase === "error") {
|
||||
return "Ошибка локального приёмника";
|
||||
}
|
||||
return "Ожидание реальных данных";
|
||||
}
|
||||
if (state?.acquisition?.state === "prepared") return "Приём подготовлен";
|
||||
return "Ожидание";
|
||||
}
|
||||
|
||||
export function operationByIdempotencyKey(
|
||||
state: XgridsK1State | null | undefined,
|
||||
action: string,
|
||||
idempotencyKey: string | null | undefined,
|
||||
): XgridsOperation | null {
|
||||
if (!idempotencyKey) return null;
|
||||
return (
|
||||
[...(state?.operations ?? [])]
|
||||
.reverse()
|
||||
.find(
|
||||
(operation) =>
|
||||
operation.action === action && operation.idempotency_key === idempotencyKey,
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
export function operationNeedsReconciliation(
|
||||
operation: XgridsOperation | null | undefined,
|
||||
): boolean {
|
||||
if (!operation || !FAILED_OPERATION_STATUSES.has(operation.status)) return false;
|
||||
return operation.error?.safe_to_retry !== true;
|
||||
}
|
||||
|
||||
function defaultUuid(): string {
|
||||
const cryptoApi = globalThis.crypto;
|
||||
if (!cryptoApi) {
|
||||
throw new Error("Web Crypto недоступен; безопасный идентификатор операции не создан.");
|
||||
}
|
||||
if (typeof cryptoApi.randomUUID === "function") return cryptoApi.randomUUID();
|
||||
const bytes = new Uint8Array(16);
|
||||
cryptoApi.getRandomValues(bytes);
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
const hex = [...bytes].map((value) => value.toString(16).padStart(2, "0"));
|
||||
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex
|
||||
.slice(6, 8)
|
||||
.join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
|
||||
}
|
||||
|
||||
export function provisioningIntentKey(
|
||||
current: string | null,
|
||||
createUuid: () => string = defaultUuid,
|
||||
): string {
|
||||
return current ?? `network-provision:${createUuid()}`;
|
||||
}
|
||||
Reference in New Issue
Block a user