1819 lines
63 KiB
TypeScript
1819 lines
63 KiB
TypeScript
import type { RuntimePhase, SourceMode as RuntimeSourceMode } from "@mission-core/plugin-sdk";
|
|
|
|
import type {
|
|
AcquisitionState,
|
|
BleDevice,
|
|
XgridsApplicationControlPhase,
|
|
XgridsAcquisition,
|
|
XgridsConnectionAttempt,
|
|
XgridsK1State,
|
|
XgridsOperation,
|
|
ReopenRetiredPhysicalReconciliationRequest,
|
|
XgridsConnectionMode,
|
|
XgridsConnectionReconfiguration,
|
|
XgridsConnectionPolicyAction,
|
|
XgridsConnectionPolicyDecision,
|
|
} 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 type LocalReceiverStopPlan =
|
|
| { kind: "acquisition"; acquisitionId: string }
|
|
| { kind: "compatibility" };
|
|
export type ControlSessionEntryPlan =
|
|
| "open"
|
|
| "continue"
|
|
| "failed"
|
|
| "duplicate-open";
|
|
|
|
export function controlSessionEntryPlan(
|
|
phase: XgridsApplicationControlPhase,
|
|
openedByCurrentOperatorAction: boolean,
|
|
canOpen: boolean,
|
|
): ControlSessionEntryPlan {
|
|
if (phase === "failed") {
|
|
return !openedByCurrentOperatorAction && canOpen ? "open" : "failed";
|
|
}
|
|
if (["idle", "closed", "completed"].includes(phase)) {
|
|
return openedByCurrentOperatorAction ? "duplicate-open" : "open";
|
|
}
|
|
return "continue";
|
|
}
|
|
|
|
export function isTerminalAcquisitionState(
|
|
state: AcquisitionState | null | undefined,
|
|
): boolean {
|
|
return state ? TERMINAL_ACQUISITION_STATES.has(state) : false;
|
|
}
|
|
|
|
/**
|
|
* Select the exact local runtime cleanup target without inheriting a retained
|
|
* terminal acquisition into a replay session. Terminal acquisition state is
|
|
* actionable only while its backend-owned local cleanup remains pending.
|
|
*/
|
|
export function localReceiverStopPlan(
|
|
state: XgridsK1State | null | undefined,
|
|
): LocalReceiverStopPlan {
|
|
const acquisition = state?.acquisition;
|
|
const acquisitionId = acquisition?.acquisition_id?.trim();
|
|
const liveOrIdleRuntime = state?.source_mode === "live" || state?.source_mode === "idle";
|
|
if (
|
|
acquisition
|
|
&& acquisitionId
|
|
&& liveOrIdleRuntime
|
|
&& (
|
|
!isTerminalAcquisitionState(acquisition.state)
|
|
|| acquisition.cleanup_pending === true
|
|
)
|
|
) {
|
|
return { kind: "acquisition", acquisitionId };
|
|
}
|
|
return { kind: "compatibility" };
|
|
}
|
|
|
|
export function isProvenLocalReceiverInactive(
|
|
state: XgridsK1State | null | undefined,
|
|
): state is XgridsK1State {
|
|
const acquisition = state?.acquisition;
|
|
const acquisitionReleased = Boolean(
|
|
acquisition
|
|
&& isTerminalAcquisitionState(acquisition.state)
|
|
&& acquisition.cleanup_pending === false,
|
|
);
|
|
return Boolean(
|
|
state?.source_mode === "idle"
|
|
&& (!acquisition || acquisitionReleased),
|
|
);
|
|
}
|
|
|
|
export function isReleasedTerminalAcquisitionFailure(
|
|
state: XgridsK1State | null | undefined,
|
|
): boolean {
|
|
const controlState = state?.application_control_session?.state;
|
|
const acquisition = state?.acquisition;
|
|
return Boolean(
|
|
state?.source_mode === "idle"
|
|
&& acquisition
|
|
&& ["failed", "interrupted"].includes(acquisition.state)
|
|
&& acquisition.cleanup_pending === false
|
|
&& [
|
|
"idle",
|
|
"connection-ready",
|
|
"active-recovery-requested",
|
|
"scanning",
|
|
"completed",
|
|
"closed",
|
|
].includes(controlState ?? ""),
|
|
);
|
|
}
|
|
|
|
export function shouldSurfaceRuntimeActionError(
|
|
action: string,
|
|
state: XgridsK1State | null | undefined,
|
|
): boolean {
|
|
return !(
|
|
["control", "live", "stop", "abort"].includes(action)
|
|
&& isReleasedTerminalAcquisitionFailure(state)
|
|
);
|
|
}
|
|
|
|
export function shouldRenderSpatialControls(
|
|
state: XgridsK1State | null | undefined,
|
|
): boolean {
|
|
const acquisition = state?.acquisition;
|
|
if (!acquisition || state?.source_mode === "replay") return false;
|
|
return (
|
|
!isTerminalAcquisitionState(acquisition.state) ||
|
|
acquisition.cleanup_pending === true ||
|
|
requiresCanonicalStopAfterTerminalLocalFailure(state)
|
|
);
|
|
}
|
|
|
|
export function requiresCanonicalStopAfterTerminalLocalFailure(
|
|
state: XgridsK1State | null | undefined,
|
|
): boolean {
|
|
const acquisition = state?.acquisition;
|
|
const control = state?.application_control_session;
|
|
return Boolean(
|
|
acquisition
|
|
&& isTerminalAcquisitionState(acquisition.state)
|
|
&& isSoftwareCommandedAcquisition(state)
|
|
&& control?.state === "scanning"
|
|
&& control.can_stop === true
|
|
);
|
|
}
|
|
|
|
export interface PhysicalStopIntentCheckpoint {
|
|
snapshotRuntimeId: string;
|
|
acquisitionId: string;
|
|
deviceId: string;
|
|
deviceSessionId: string;
|
|
controlSessionGeneration: number;
|
|
controlStateRevision: number;
|
|
}
|
|
|
|
function positiveInteger(value: unknown): value is number {
|
|
return Number.isSafeInteger(value) && (value as number) >= 1;
|
|
}
|
|
|
|
/**
|
|
* Capture the exact backend authority consumed by one physical STOP intent.
|
|
* Snapshot identity is retained when available, while the control-session CAS
|
|
* is mandatory: a presentation-only error dismissal must never manufacture a
|
|
* fresh command intent against the same control checkpoint.
|
|
*/
|
|
export function physicalStopIntentCheckpoint(
|
|
state: XgridsK1State | null | undefined,
|
|
): PhysicalStopIntentCheckpoint | null {
|
|
const control = state?.application_control_session;
|
|
const acquisition = state?.acquisition;
|
|
const acquisitionId = acquisition?.acquisition_id?.trim();
|
|
const deviceId = acquisition?.device_id?.trim();
|
|
const deviceSessionId = acquisition?.device_session_id?.trim();
|
|
const runtimeId = state?.snapshot_runtime_id?.trim();
|
|
if (
|
|
!control
|
|
|| !acquisitionId
|
|
|| !deviceId
|
|
|| !deviceSessionId
|
|
|| !runtimeId
|
|
|| !positiveInteger(control.session_generation)
|
|
|| !positiveInteger(control.state_revision)
|
|
|| control.state !== "scanning"
|
|
|| control.can_stop !== true
|
|
|| !connectionPolicyAllows(state, "stop-acquisition")
|
|
) return null;
|
|
return {
|
|
snapshotRuntimeId: runtimeId,
|
|
acquisitionId,
|
|
deviceId,
|
|
deviceSessionId,
|
|
controlSessionGeneration: control.session_generation,
|
|
controlStateRevision: control.state_revision,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A spent physical STOP may be released only by an already-accepted exact
|
|
* STOP-authoritative runtime replacement, a distinct acquisition target, or
|
|
* an exact control-session CAS transition. A same-runtime polling snapshot
|
|
* whose target and control CAS stayed fixed is not new physical-command
|
|
* authority.
|
|
*/
|
|
export function authoritativeStateSupersedesPhysicalStopIntent(
|
|
spent: PhysicalStopIntentCheckpoint | null | undefined,
|
|
state: XgridsK1State | null | undefined,
|
|
): boolean {
|
|
if (!spent) return false;
|
|
const current = physicalStopIntentCheckpoint(state);
|
|
if (!current) return false;
|
|
const sameRuntime = current.snapshotRuntimeId === spent.snapshotRuntimeId;
|
|
if (!sameRuntime) {
|
|
// Snapshot ordering is resolved before this helper is called. A different
|
|
// accepted runtime with a complete exact STOP gate is fresh authority even
|
|
// when its process-local CAS counters restarted.
|
|
return true;
|
|
}
|
|
const sameTarget =
|
|
current.acquisitionId === spent.acquisitionId
|
|
&& current.deviceId === spent.deviceId
|
|
&& current.deviceSessionId === spent.deviceSessionId;
|
|
if (!sameTarget) {
|
|
// A newly accepted acquisition/device/session tuple is a distinct command
|
|
// target. The caller has already admitted this state monotonically.
|
|
return true;
|
|
}
|
|
const controlCasAdvanced =
|
|
current.controlSessionGeneration > spent.controlSessionGeneration
|
|
|| (
|
|
current.controlSessionGeneration === spent.controlSessionGeneration
|
|
&& current.controlStateRevision > spent.controlStateRevision
|
|
);
|
|
if (!controlCasAdvanced) return false;
|
|
// Within one runtime the exact control CAS must advance; observation-only
|
|
// polls remain locked regardless of their snapshot observation revision.
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Admit one physical STOP button only from the exact current control proof.
|
|
* A failed action spends that browser intent independently of its dismissible
|
|
* presentation error: the operator may still finish the host receiver
|
|
* locally, but the UI must not create a fresh physical STOP mutation from the
|
|
* same accepted snapshot/control CAS.
|
|
*/
|
|
export function canIssueCanonicalStop(
|
|
state: XgridsK1State | null | undefined,
|
|
physicalStopIntentSpent: boolean | null | undefined,
|
|
): boolean {
|
|
const control = state?.application_control_session;
|
|
return Boolean(
|
|
!physicalStopIntentSpent
|
|
&& physicalStopIntentCheckpoint(state)
|
|
&& control?.state === "scanning"
|
|
&& control.can_stop === true
|
|
&& connectionPolicyAllows(state, "stop-acquisition"),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A successful read-only Verify may truthfully end in SCANNING rather than
|
|
* connection-ready. That state grants exactly one explicit STOP, never START
|
|
* or provisioning authority.
|
|
*/
|
|
export function isRecoveredPhysicalScanning(
|
|
state: XgridsK1State | null | undefined,
|
|
expectedMode?: XgridsConnectionMode,
|
|
): boolean {
|
|
const control = state?.application_control_session;
|
|
const physical = control?.physical_command ?? state?.physical_command;
|
|
const mode = state?.active_connection_mode ?? state?.connection_mode;
|
|
return Boolean(
|
|
control?.state === "scanning"
|
|
&& control.can_stop === true
|
|
&& physical?.requires_reconciliation !== true
|
|
&& physical?.resolved_active_recovery_required === true
|
|
&& physical.observed_session_state === "scanning"
|
|
&& (!expectedMode || mode === expectedMode)
|
|
&& (!expectedMode || currentAppliedConnectionTopology(state, expectedMode)?.status === "active")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A STOP that was accepted before the host path disappeared is completed by
|
|
* the backend from its durable command ledger. The connection screen must
|
|
* wait for that cleanup instead of turning the condition into another device
|
|
* action (Scan, START, STOP, or Wi-Fi provisioning).
|
|
*/
|
|
export function isPhysicalStopRecoverySettling(
|
|
state: XgridsK1State | null | undefined,
|
|
): boolean {
|
|
const control = state?.application_control_session;
|
|
const physical = control?.physical_command ?? state?.physical_command;
|
|
const record = physical?.record;
|
|
const action = record?.action;
|
|
const resolution = record?.resolution;
|
|
const acquisition = state?.acquisition;
|
|
const acquisitionStillSettling = Boolean(
|
|
acquisition
|
|
&& (
|
|
!isTerminalAcquisitionState(acquisition.state)
|
|
|| acquisition.cleanup_pending === true
|
|
),
|
|
);
|
|
const stopOperationStillSettling = Boolean(
|
|
state?.operations?.some((operation) =>
|
|
operation.action === "acquisition.stop"
|
|
&& ["accepted", "running", "operator_action_required"].includes(operation.status)
|
|
),
|
|
);
|
|
return Boolean(
|
|
action === "stop"
|
|
&& resolution !== "stop-standby-observed"
|
|
&& (physical?.requires_reconciliation === true || physical?.status === "unresolved")
|
|
&& (acquisitionStillSettling || stopOperationStillSettling),
|
|
);
|
|
}
|
|
|
|
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 Boolean(
|
|
state?.source_mode === "live"
|
|
&& state.acquisition?.state === "acquiring"
|
|
&& hasAuthoritativeData(state),
|
|
);
|
|
}
|
|
|
|
export function isSourceRuntimeBusy(state: XgridsK1State | null | undefined): boolean {
|
|
return state?.source_mode === "live" || state?.source_mode === "replay";
|
|
}
|
|
|
|
export function isVendorWriteCapable(
|
|
state: XgridsK1State | null | undefined,
|
|
): boolean {
|
|
return (
|
|
state?.compatibility?.vendor_writes_enabled === true &&
|
|
state.compatibility.permitted_mode === "active-control"
|
|
);
|
|
}
|
|
|
|
export function isSoftwareCommandedAcquisition(
|
|
state: XgridsK1State | null | undefined,
|
|
): boolean {
|
|
return isVendorWriteCapable(state) && state?.acquisition?.control_mode === "plugin-commanded";
|
|
}
|
|
|
|
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;
|
|
const releasedFailure = isReleasedTerminalAcquisitionFailure(state);
|
|
if (
|
|
!releasedFailure
|
|
&& (acquisitionState === "failed" || acquisitionState === "interrupted")
|
|
) return "error";
|
|
if (acquisitionState === "awaiting_external_start" || acquisitionState === "starting") {
|
|
return "starting";
|
|
}
|
|
if (acquisitionState === "acquiring") {
|
|
return isConfirmedLiveState(state) ? "streaming" : "starting";
|
|
}
|
|
if (
|
|
acquisitionState === "awaiting_external_stop" ||
|
|
acquisitionState === "stopping" ||
|
|
acquisitionState === "finalizing"
|
|
) {
|
|
return "stopping";
|
|
}
|
|
if (acquisitionState === "prepared") {
|
|
const topology = currentAppliedConnectionTopology(state);
|
|
return topology && topology.status !== "configured-offline"
|
|
? "connected"
|
|
: "configuring";
|
|
}
|
|
if (phase === "error") return releasedFailure ? "idle" : "error";
|
|
if (phase === "connected") {
|
|
const topology = currentAppliedConnectionTopology(state);
|
|
return topology && topology.status !== "configured-offline"
|
|
? "connected"
|
|
: "configuring";
|
|
}
|
|
if (phase === "starting_live") return "starting";
|
|
if (phase === "live") return isConfirmedLiveState(state) ? "streaming" : "starting";
|
|
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 "Ошибка локального приёмника";
|
|
}
|
|
if (state.connection_supervisor?.observed.data_plane.state === "lost") {
|
|
return "Поток данных потерян";
|
|
}
|
|
if (state.connection_supervisor?.observed.data_plane.state === "stalled") {
|
|
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;
|
|
}
|
|
|
|
export function operationAllowsFreshProvisioningIntent(
|
|
operation: XgridsOperation | null | undefined,
|
|
): boolean {
|
|
return Boolean(
|
|
operation
|
|
&& FAILED_OPERATION_STATUSES.has(operation.status)
|
|
&& operation.error?.safe_to_retry === true
|
|
&& operation.error.side_effect_status === "none",
|
|
);
|
|
}
|
|
|
|
const FRESH_CANDIDATE_RETRY_REASON_CODES = new Set([
|
|
"BleakDeviceNotFoundError",
|
|
"network-provision-candidate-not-fresh",
|
|
"network-provision-candidate-changed",
|
|
"network-provision-discovery-generation-conflict",
|
|
]);
|
|
|
|
/**
|
|
* These failures are proven pre-write rejections caused only by an expired
|
|
* Bluetooth capture. One explicit network-submit click may refresh discovery
|
|
* once and then continue with the exact returned generation. A second failure
|
|
* is terminal for that click; this predicate never authorizes an unbounded
|
|
* retry or a repeat after an ambiguous/device-write outcome.
|
|
*/
|
|
export function provisioningFailureRequiresFreshCandidate(
|
|
reasonCode: string | null | undefined,
|
|
): boolean {
|
|
return typeof reasonCode === "string"
|
|
&& FRESH_CANDIDATE_RETRY_REASON_CODES.has(reasonCode);
|
|
}
|
|
|
|
export function readOnlyVerificationClearedReconciliation(
|
|
previousState: XgridsK1State | null | undefined,
|
|
nextState: XgridsK1State | null | undefined,
|
|
verifiedDeviceId: string | null | undefined,
|
|
): boolean {
|
|
const previousFence = previousState?.network_write_reconciliation;
|
|
const previousOperationId = previousFence?.operation_id?.trim();
|
|
const nextLedger = nextState?.network_mutation_ledger;
|
|
if (
|
|
!verifiedDeviceId
|
|
|| !previousOperationId
|
|
|| transportRefEquivalenceKey(previousFence?.transport_ref)
|
|
!== transportRefEquivalenceKey(verifiedDeviceId)
|
|
// An omitted field or a replacement unresolved fence is never proof that
|
|
// this durable operation was reconciled.
|
|
|| nextState?.network_write_reconciliation !== null
|
|
|| !nextLedger
|
|
|| nextLedger.mutation_allowed !== true
|
|
) {
|
|
return false;
|
|
}
|
|
const sameOperationResolved = Boolean(
|
|
nextLedger.status === "resolved"
|
|
&& nextLedger.operation_id === previousOperationId
|
|
&& nextLedger.stage === "resolved"
|
|
&& nextLedger.resolution !== null,
|
|
);
|
|
const operationExplicitlyAbsent = Boolean(
|
|
nextLedger.status === "empty"
|
|
&& nextLedger.operation_id === null
|
|
&& nextLedger.stage === null,
|
|
);
|
|
return sameOperationResolved || operationExplicitlyAbsent;
|
|
}
|
|
|
|
export function provisioningCandidateById(
|
|
devices: readonly BleDevice[],
|
|
selectedDeviceId: string,
|
|
): BleDevice | null {
|
|
const selectedKey = transportRefEquivalenceKey(selectedDeviceId);
|
|
if (!selectedKey) return null;
|
|
return devices.find(
|
|
(device) => transportRefEquivalenceKey(device.device_id) === selectedKey,
|
|
) ?? null;
|
|
}
|
|
|
|
export function currentDeviceTransportRef(
|
|
state: XgridsK1State | null | undefined,
|
|
): string | null {
|
|
const recoveryRef = state?.current_device_recovery?.transport_ref?.trim();
|
|
if (recoveryRef) return recoveryRef;
|
|
const selectedRef = state?.selected_device_id?.trim();
|
|
return selectedRef || null;
|
|
}
|
|
|
|
export interface LocallyInitiatedBleSessionTarget {
|
|
transportRef: string;
|
|
connectionMode: XgridsConnectionMode;
|
|
deviceSessionId: string;
|
|
key: string;
|
|
}
|
|
|
|
export interface LocalBleSessionBindingConstraints {
|
|
/** Bind only the exact session accepted by the explicit connect response. */
|
|
requiredSessionKey?: string | null;
|
|
}
|
|
|
|
export function bleSessionTargetForTransport(
|
|
state: XgridsK1State | null | undefined,
|
|
transportRef: string | null | undefined,
|
|
selectedConnectionMode: XgridsConnectionMode,
|
|
): LocallyInitiatedBleSessionTarget | null {
|
|
const expectedTransportRef = transportRef?.trim();
|
|
const backendTransportRef = state?.current_device_recovery?.transport_ref?.trim()
|
|
|| state?.selected_device_id?.trim();
|
|
const connectionMode = state?.current_device_recovery?.connection_mode
|
|
?? state?.connection_mode;
|
|
const deviceSessionId = state?.device_session?.device_session_id?.trim();
|
|
if (
|
|
!expectedTransportRef
|
|
|| !backendTransportRef
|
|
|| expectedTransportRef !== backendTransportRef
|
|
|| !deviceSessionId
|
|
|| connectionMode !== selectedConnectionMode
|
|
) {
|
|
return null;
|
|
}
|
|
return {
|
|
transportRef: backendTransportRef,
|
|
connectionMode,
|
|
deviceSessionId,
|
|
key: `${deviceSessionId}:${connectionMode}:${backendTransportRef}`,
|
|
};
|
|
}
|
|
|
|
export function acceptedBleSessionKeyAfterConnect(
|
|
state: XgridsK1State | null | undefined,
|
|
transportRef: string | null | undefined,
|
|
selectedConnectionMode: XgridsConnectionMode,
|
|
sessionKeyBeforeConnect: string | null | undefined,
|
|
): string | null {
|
|
const target = bleSessionTargetForTransport(
|
|
state,
|
|
transportRef,
|
|
selectedConnectionMode,
|
|
);
|
|
if (!target || target.key === sessionKeyBeforeConnect) return null;
|
|
return target.key;
|
|
}
|
|
|
|
/**
|
|
* Bind backend session state only to a connection initiated by this UI
|
|
* instance. A browser refresh has no local device id and therefore never
|
|
* turns an existing backend session into an implicit operator selection.
|
|
*/
|
|
export function locallyInitiatedBleSessionTarget(
|
|
state: XgridsK1State | null | undefined,
|
|
locallyInitiatedDeviceId: string | null | undefined,
|
|
selectedConnectionMode: XgridsConnectionMode,
|
|
constraints: LocalBleSessionBindingConstraints = {},
|
|
): LocallyInitiatedBleSessionTarget | null {
|
|
const localRef = locallyInitiatedDeviceId?.trim();
|
|
if (!localRef) return null;
|
|
const target = bleSessionTargetForTransport(
|
|
state,
|
|
localRef,
|
|
selectedConnectionMode,
|
|
);
|
|
if (!target) return null;
|
|
if (
|
|
constraints.requiredSessionKey
|
|
&& constraints.requiredSessionKey !== target.key
|
|
) return null;
|
|
return target;
|
|
}
|
|
|
|
export interface RetainedBleRecoveryTarget {
|
|
transportRef: string;
|
|
connectionMode: NonNullable<XgridsK1State["connection_mode"]> | null;
|
|
gattValidatedRecently: boolean;
|
|
}
|
|
|
|
export function retainedBleRecoveryTarget(
|
|
state: XgridsK1State | null | undefined,
|
|
): RetainedBleRecoveryTarget | null {
|
|
const recovery = state?.current_device_recovery;
|
|
const transportRef = recovery?.transport_ref?.trim();
|
|
if (
|
|
!transportRef
|
|
|| recovery?.handle_retained !== true
|
|
|| recovery.advertised_now === true
|
|
) {
|
|
return null;
|
|
}
|
|
return {
|
|
transportRef,
|
|
connectionMode: recovery.connection_mode ?? null,
|
|
gattValidatedRecently: recovery.gatt_validated_recently === true,
|
|
};
|
|
}
|
|
|
|
export function connectionPolicyDecision(
|
|
state: XgridsK1State | null | undefined,
|
|
action: XgridsConnectionPolicyAction,
|
|
): XgridsConnectionPolicyDecision | null {
|
|
const policy = state?.connection_policy;
|
|
if (
|
|
policy?.schema_version !== "missioncore.xgrids-k1-connection-policy/v1"
|
|
|| policy.facts.retained_context_is_presence !== false
|
|
) {
|
|
return null;
|
|
}
|
|
return policy.actions[action] ?? null;
|
|
}
|
|
|
|
export function connectionPolicyAllows(
|
|
state: XgridsK1State | null | undefined,
|
|
action: XgridsConnectionPolicyAction,
|
|
): boolean {
|
|
const decision = connectionPolicyDecision(state, action);
|
|
return Boolean(
|
|
decision?.allowed === true
|
|
&& decision.automatic_retry === false
|
|
&& state?.connection_policy?.allowed_actions.includes(action),
|
|
);
|
|
}
|
|
|
|
export interface ProvisioningNetworkStepDisclosure {
|
|
/** A concrete K1 has been admitted or selected by this operator flow. */
|
|
deviceExplicitlySelectedOrAdmitted: boolean;
|
|
/** A network/connection intent, rather than discovery alone, has started. */
|
|
networkIntentStarted: boolean;
|
|
}
|
|
|
|
/**
|
|
* Keep Bluetooth discovery entirely inside step 02.
|
|
*
|
|
* Raw controller settlement is deliberately not an input: an old scan,
|
|
* reconfiguration, retirement, or physical-cleanup promise may still be
|
|
* unwinding after its authority has gone stale, but that does not mean the
|
|
* operator has reached the network step for the current device choice.
|
|
*/
|
|
export function shouldRevealProvisioningNetworkStep({
|
|
deviceExplicitlySelectedOrAdmitted,
|
|
networkIntentStarted,
|
|
}: ProvisioningNetworkStepDisclosure): boolean {
|
|
return deviceExplicitlySelectedOrAdmitted || networkIntentStarted;
|
|
}
|
|
|
|
export function activeConnectionReconfiguration(
|
|
state: XgridsK1State | null | undefined,
|
|
): XgridsConnectionReconfiguration | null {
|
|
const reconfiguration = state?.connection_reconfiguration;
|
|
if (
|
|
!reconfiguration
|
|
|| reconfiguration.schema_version
|
|
!== "missioncore.xgrids-k1-connection-reconfiguration/v1"
|
|
|| reconfiguration.intent === null
|
|
|| reconfiguration.status === "idle"
|
|
) {
|
|
return null;
|
|
}
|
|
return reconfiguration;
|
|
}
|
|
|
|
/**
|
|
* A network-change intent is pinned to the exact device and mode which were
|
|
* current when the operator opened it. Selecting another advertisement must
|
|
* not turn that intent into a different-device network write.
|
|
*/
|
|
export function reconfigurationAllowsFreshDevice(
|
|
reconfiguration: XgridsConnectionReconfiguration | null,
|
|
deviceId: string,
|
|
connectionMode: XgridsConnectionMode,
|
|
): boolean {
|
|
if (!reconfiguration || !deviceId.trim()) return false;
|
|
if (
|
|
connectionMode !== "bridge"
|
|
|| reconfiguration.required_connection_mode !== "bridge"
|
|
) return false;
|
|
if (reconfiguration.intent === "select-device") return true;
|
|
return Boolean(
|
|
reconfiguration.intent === "change-network"
|
|
&& transportRefEquivalenceKey(reconfiguration.required_transport_ref)
|
|
=== transportRefEquivalenceKey(deviceId)
|
|
&& reconfiguration.required_connection_mode === connectionMode,
|
|
);
|
|
}
|
|
|
|
export function readOnlyObservationShowsNetworkUnavailable(
|
|
state: XgridsK1State | null | undefined,
|
|
): boolean {
|
|
const verification = state?.connection_verification;
|
|
if (!verification) return false;
|
|
return [
|
|
"device-network-applied-host-failed",
|
|
"host-route-mismatch",
|
|
"endpoint-unreachable",
|
|
"unreachable",
|
|
].includes(verification.status) || Boolean(
|
|
verification.lease_state === "configured-unverified"
|
|
&& verification.network_reachability === "unreachable",
|
|
) || Boolean(
|
|
verification.status === "device-network-applied"
|
|
&& verification.lease_state === "configured-unverified"
|
|
&& verification.reason_code === "endpoint-target-unconfigured",
|
|
);
|
|
}
|
|
|
|
const READ_ONLY_NETWORK_UNAVAILABLE_REASON_CODES = new Set([
|
|
"connection-verify-address-unavailable",
|
|
"connection-verify-connection-missing",
|
|
"connection-verify-route-mismatch",
|
|
"connection-verify-mqtt-unreachable",
|
|
"configured-endpoint-unavailable",
|
|
"endpoint-target-unconfigured",
|
|
]);
|
|
|
|
/**
|
|
* Only failures which prove that the selected device cannot use the current
|
|
* Bridge topology may lead from read-only adoption to explicit credentials.
|
|
* BLE, identity, lifecycle and CAS failures deliberately stay outside this
|
|
* allowlist.
|
|
*/
|
|
export function readOnlyFailureShowsNetworkUnavailable(
|
|
reasonCode: string | null | undefined,
|
|
): boolean {
|
|
return typeof reasonCode === "string"
|
|
&& READ_ONLY_NETWORK_UNAVAILABLE_REASON_CODES.has(reasonCode);
|
|
}
|
|
|
|
/**
|
|
* Preserve the stronger BLE observation across refreshes and later scan
|
|
* failures: this saved Bridge needs network setup, not another old-address
|
|
* reconnect attempt.
|
|
*/
|
|
export function savedBridgeRequiresNetworkSetup(
|
|
state: XgridsK1State | null | undefined,
|
|
): boolean {
|
|
if (
|
|
state?.connection_verification?.reason_code
|
|
=== "connection-verify-address-unavailable"
|
|
) return true;
|
|
const operation = state?.last_operation;
|
|
return Boolean(
|
|
operation?.action === "connection.verify"
|
|
&& operation.status === "failed"
|
|
&& operation.error?.code === "connection-verify-address-unavailable",
|
|
);
|
|
}
|
|
|
|
export function requiresReadOnlyPhysicalRecovery(
|
|
state: XgridsK1State | null | undefined,
|
|
): boolean {
|
|
const control = state?.application_control_session;
|
|
const physical = control?.physical_command ?? state?.physical_command;
|
|
return Boolean(
|
|
// The coordinator folds unresolved commands and resolved SCAN_OVER into
|
|
// requires_reconciliation, while resolved active and explicitly reopened
|
|
// rows remain read-only recovery through the separate active flag.
|
|
physical?.requires_reconciliation === true
|
|
|| physical?.resolved_active_recovery_required === true,
|
|
);
|
|
}
|
|
|
|
export interface ReadOnlyPhysicalRecoveryBinding {
|
|
deviceId: string;
|
|
connectionMode: XgridsConnectionMode;
|
|
}
|
|
|
|
export interface TrustedConnectionBinding {
|
|
deviceId: string;
|
|
connectionMode: XgridsConnectionMode;
|
|
}
|
|
|
|
/**
|
|
* CoreBluetooth UUID text is case-insensitive. Keep the original spelling for
|
|
* display and exact CAS payloads, but use this key whenever refs are compared.
|
|
*/
|
|
export function transportRefEquivalenceKey(
|
|
transportRef: string | null | undefined,
|
|
): string {
|
|
return transportRef?.trim().toLowerCase() ?? "";
|
|
}
|
|
|
|
/**
|
|
* Resolve the exact K1 remembered by durable/backend-owned state. This is
|
|
* selection context only: it can authorize bounded observation, but never a
|
|
* device write without the separate connection policy and an explicit click.
|
|
*/
|
|
export function trustedConnectionBinding(
|
|
state: XgridsK1State | null | undefined,
|
|
): TrustedConnectionBinding | null {
|
|
const physical = readOnlyPhysicalRecoveryBinding(state);
|
|
if (physical) return physical;
|
|
|
|
const retiredTransportRefs = retiredPhysicalTransportRefs(state);
|
|
|
|
const semanticRecord = state?.semantic_topology_store?.record;
|
|
const semanticDeviceId = semanticRecord?.transport_ref?.trim();
|
|
const semanticMode = semanticRecord?.connection_mode;
|
|
if (
|
|
semanticDeviceId
|
|
&& !retiredTransportRefs.has(transportRefEquivalenceKey(semanticDeviceId))
|
|
&& isConnectionMode(semanticMode)
|
|
) {
|
|
return { deviceId: semanticDeviceId, connectionMode: semanticMode };
|
|
}
|
|
|
|
const recovery = state?.current_device_recovery;
|
|
const recoveryDeviceId = recovery?.transport_ref?.trim();
|
|
const recoveryMode = recovery?.connection_mode;
|
|
if (
|
|
recoveryDeviceId
|
|
&& !retiredTransportRefs.has(transportRefEquivalenceKey(recoveryDeviceId))
|
|
&& isConnectionMode(recoveryMode)
|
|
) {
|
|
return { deviceId: recoveryDeviceId, connectionMode: recoveryMode };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Transport refs explicitly retired by the operator must never rehydrate. */
|
|
export function retiredPhysicalTransportRefs(
|
|
state: XgridsK1State | null | undefined,
|
|
): ReadonlySet<string> {
|
|
const policyRetiredRefs = state?.connection_policy?.facts.retired_transport_refs;
|
|
if (Array.isArray(policyRetiredRefs)) {
|
|
// This backend projection is the authoritative *active* deny-list. The
|
|
// durable ledger keeps historical retirement audits even after an explicit
|
|
// reconciliation reopen, so unioning every old audit would make a safely
|
|
// reopened UUID impossible to use forever.
|
|
return new Set(
|
|
policyRetiredRefs.map(transportRefEquivalenceKey).filter(Boolean),
|
|
);
|
|
}
|
|
const record = state?.physical_command?.record;
|
|
const retired = new Set<string>();
|
|
if (!record) return retired;
|
|
const retirements = Array.isArray(record.operator_retirements)
|
|
? record.operator_retirements
|
|
: [];
|
|
for (const candidate of retirements) {
|
|
if (!candidate || typeof candidate !== "object") continue;
|
|
const transportRef = "retired_transport_ref" in candidate
|
|
&& typeof candidate.retired_transport_ref === "string"
|
|
? candidate.retired_transport_ref.trim()
|
|
: "";
|
|
if (transportRef) retired.add(transportRefEquivalenceKey(transportRef));
|
|
}
|
|
if (record.resolution === "operator-retired-outcome-unknown") {
|
|
const connection = record.connection;
|
|
if (connection && typeof connection === "object") {
|
|
const transportRef = "transport_ref" in connection
|
|
&& typeof connection.transport_ref === "string"
|
|
? connection.transport_ref.trim()
|
|
: "";
|
|
if (transportRef) retired.add(transportRefEquivalenceKey(transportRef));
|
|
}
|
|
}
|
|
return retired;
|
|
}
|
|
|
|
export interface RetiredPhysicalReopenAuthority {
|
|
expectedRevision: number;
|
|
expectedRetirementId: string;
|
|
expectedTransportRef: string;
|
|
expectedDiscoveryGeneration: number;
|
|
expectedDesiredMode: XgridsConnectionMode;
|
|
expectedDesiredModeRevision: number;
|
|
}
|
|
|
|
/**
|
|
* Admit the local-only reopen affordance only for the exact fresh retired row
|
|
* and exact backend-projected ledger/discovery CAS. This helper grants no GATT
|
|
* or write authority; the separately explicit Verify remains server-fenced.
|
|
*/
|
|
export function retiredPhysicalReopenAuthority(
|
|
state: XgridsK1State | null | undefined,
|
|
candidateTransportRef: string,
|
|
connectionMode: NonNullable<XgridsK1State["connection_mode"]>,
|
|
): RetiredPhysicalReopenAuthority | null {
|
|
const projection = state?.physical_command?.operator_reconciliation_reopen;
|
|
const expectedRevision = projection?.expected_revision;
|
|
const expectedRetirementId = projection?.expected_retirement_id?.trim() ?? "";
|
|
const expectedTransportRef = projection?.expected_transport_ref?.trim() ?? "";
|
|
const expectedDiscoveryGeneration = projection?.expected_discovery_generation;
|
|
const expectedDesiredMode = projection?.expected_desired_mode;
|
|
const expectedDesiredModeRevision =
|
|
projection?.expected_desired_mode_revision;
|
|
const candidateKey = transportRefEquivalenceKey(candidateTransportRef);
|
|
const expectedKey = transportRefEquivalenceKey(expectedTransportRef);
|
|
const currentGeneration = state?.ble_discovery_generation;
|
|
const record = state?.physical_command?.record;
|
|
const recordRevision = record && typeof record.revision === "number"
|
|
? record.revision
|
|
: null;
|
|
const retirements = record && Array.isArray(record.operator_retirements)
|
|
? record.operator_retirements
|
|
: [];
|
|
const exactRetirementRecorded = retirements.some((candidate) => Boolean(
|
|
candidate
|
|
&& typeof candidate === "object"
|
|
&& "retirement_id" in candidate
|
|
&& candidate.retirement_id === expectedRetirementId
|
|
&& "retired_transport_ref" in candidate
|
|
&& typeof candidate.retired_transport_ref === "string"
|
|
&& transportRefEquivalenceKey(candidate.retired_transport_ref) === expectedKey
|
|
));
|
|
const freshCandidates = (state?.devices ?? []).filter(
|
|
(device) => transportRefEquivalenceKey(device.device_id) === candidateKey,
|
|
);
|
|
const freshCandidate = freshCandidates.length === 1
|
|
? freshCandidates[0]
|
|
: null;
|
|
const recordConnection = record && typeof record.connection === "object"
|
|
&& record.connection !== null
|
|
? record.connection
|
|
: null;
|
|
const recoveryConnectionMode = recordConnection
|
|
&& "connection_mode" in recordConnection
|
|
&& typeof recordConnection.connection_mode === "string"
|
|
? recordConnection.connection_mode
|
|
: null;
|
|
const recoveryMatchesMode = Boolean(
|
|
recordConnection
|
|
&& "transport_ref" in recordConnection
|
|
&& typeof recordConnection.transport_ref === "string"
|
|
&& transportRefEquivalenceKey(recordConnection.transport_ref) === expectedKey
|
|
&& recoveryConnectionMode === connectionMode,
|
|
);
|
|
// CoreBluetooth may advertise the exact returned transport while marking
|
|
// the passive scan row non-connectable. The backend's exact reopen CAS is
|
|
// the only exception: the explicit click is still server-fenced, and every
|
|
// unrelated non-connectable row remains blocked by the presentation layer.
|
|
if (
|
|
projection?.allowed !== true
|
|
|| projection.automatic_retry !== false
|
|
|| projection.device_io_performed !== false
|
|
|| projection.reason_codes.length !== 0
|
|
|| !Number.isInteger(expectedRevision)
|
|
|| (expectedRevision ?? 0) < 1
|
|
|| recordRevision !== expectedRevision
|
|
|| !expectedRetirementId
|
|
|| !candidateKey
|
|
|| candidateKey !== expectedKey
|
|
|| !Number.isInteger(expectedDiscoveryGeneration)
|
|
|| expectedDiscoveryGeneration !== currentGeneration
|
|
|| expectedDesiredMode !== connectionMode
|
|
|| state?.desired_connection_mode !== expectedDesiredMode
|
|
|| !Number.isInteger(expectedDesiredModeRevision)
|
|
|| expectedDesiredModeRevision !== state?.desired_connection_mode_revision
|
|
|| !retiredPhysicalTransportRefs(state).has(candidateKey)
|
|
|| !exactRetirementRecorded
|
|
|| !freshCandidate
|
|
|| !recoveryMatchesMode
|
|
) return null;
|
|
return {
|
|
expectedRevision: expectedRevision as number,
|
|
expectedRetirementId,
|
|
expectedTransportRef,
|
|
expectedDiscoveryGeneration: expectedDiscoveryGeneration as number,
|
|
expectedDesiredMode,
|
|
expectedDesiredModeRevision: expectedDesiredModeRevision as number,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Prove that a lost reopen response actually committed the exact local-only
|
|
* ledger transition. Nothing here grants Verify by itself; the caller must
|
|
* also retain the original full connection-action authority.
|
|
*/
|
|
export function reopenedPhysicalReconciliationMatches(
|
|
state: XgridsK1State | null | undefined,
|
|
request: ReopenRetiredPhysicalReconciliationRequest,
|
|
): boolean {
|
|
const record = state?.physical_command?.record;
|
|
const recordRevision = record && typeof record.revision === "number"
|
|
? record.revision
|
|
: null;
|
|
const reopens = record && Array.isArray(record.operator_reconciliation_reopens)
|
|
? record.operator_reconciliation_reopens
|
|
: [];
|
|
const requestKey = transportRefEquivalenceKey(request.expected_transport_ref);
|
|
const exactAudit = reopens.some((candidate) => Boolean(
|
|
candidate
|
|
&& typeof candidate === "object"
|
|
&& "reopening_id" in candidate
|
|
&& candidate.reopening_id === request.reopening_id
|
|
&& "retirement_id" in candidate
|
|
&& candidate.retirement_id === request.expected_retirement_id
|
|
&& "retired_record_revision" in candidate
|
|
&& candidate.retired_record_revision === request.expected_revision
|
|
&& "reopened_transport_ref" in candidate
|
|
&& typeof candidate.reopened_transport_ref === "string"
|
|
&& transportRefEquivalenceKey(candidate.reopened_transport_ref) === requestKey
|
|
&& "discovery_generation" in candidate
|
|
&& candidate.discovery_generation === request.expected_discovery_generation
|
|
&& "reason" in candidate
|
|
&& candidate.reason === request.reason
|
|
));
|
|
const activeRetiredRefs = state?.connection_policy?.facts.retired_transport_refs;
|
|
const activeDenyRemoved = Array.isArray(activeRetiredRefs)
|
|
&& !activeRetiredRefs.some(
|
|
(value) => transportRefEquivalenceKey(value) === requestKey,
|
|
);
|
|
const freshCandidates = (state?.devices ?? []).filter(
|
|
(device) => transportRefEquivalenceKey(device.device_id) === requestKey,
|
|
);
|
|
const freshCandidate = freshCandidates.length === 1
|
|
? freshCandidates[0]
|
|
: null;
|
|
return Boolean(
|
|
request.expected_desired_mode === state?.desired_connection_mode
|
|
&& request.expected_desired_mode_revision
|
|
=== state?.desired_connection_mode_revision
|
|
&& requestKey
|
|
&& recordRevision === request.expected_revision + 1
|
|
&& (record?.stage === "dispatching" || record?.stage === "observing")
|
|
&& record.resolution === null
|
|
&& state?.physical_command?.requires_reconciliation === true
|
|
&& state.ble_discovery_generation === request.expected_discovery_generation
|
|
&& exactAudit
|
|
&& activeDenyRemoved
|
|
&& freshCandidate
|
|
&& freshCandidate.connectable !== false
|
|
);
|
|
}
|
|
|
|
/** The exact durable K1 binding that recovery is allowed to observe. */
|
|
export function readOnlyPhysicalRecoveryBinding(
|
|
state: XgridsK1State | null | undefined,
|
|
): ReadOnlyPhysicalRecoveryBinding | null {
|
|
if (!requiresReadOnlyPhysicalRecovery(state)) return null;
|
|
const actions: ReadOnlyNetworkObservationAction[] = [
|
|
"observe-fresh-device-network",
|
|
"observe-current-device-network",
|
|
"observe-configured-device-network",
|
|
];
|
|
for (const action of actions) {
|
|
const decision = connectionPolicyDecision(state, action);
|
|
const deviceId = decision?.required_transport_ref?.trim();
|
|
const connectionMode = decision?.required_connection_mode ?? null;
|
|
if (deviceId && isConnectionMode(connectionMode)) {
|
|
return { deviceId, connectionMode };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function canSelectConnectionMode(
|
|
state: XgridsK1State | null | undefined,
|
|
): boolean {
|
|
const lifecycle = state?.connection_lifecycle;
|
|
return Boolean(
|
|
lifecycle?.schema_version === "missioncore.xgrids-k1-connection-lifecycle/v1"
|
|
&& lifecycle.mode_selection.allowed === true
|
|
&& lifecycle.mode_selection.automatic_retry === false
|
|
&& lifecycle.allowed_actions.includes("select-connection-mode"),
|
|
);
|
|
}
|
|
|
|
type ReadOnlyNetworkObservationAction = Extract<
|
|
XgridsConnectionPolicyAction,
|
|
| "observe-fresh-device-network"
|
|
| "observe-current-device-network"
|
|
| "observe-configured-device-network"
|
|
>;
|
|
|
|
export type ReadOnlyConnectionObservationAction =
|
|
| ReadOnlyNetworkObservationAction
|
|
| Extract<XgridsConnectionPolicyAction, "verify-control-device-info">;
|
|
|
|
export type ReadOnlyConnectionObservationSource =
|
|
| "fresh-scan"
|
|
| "retained-current-process"
|
|
| "durable-configured-state";
|
|
|
|
export interface ReadOnlyConnectionObservationTarget {
|
|
action: ReadOnlyConnectionObservationAction;
|
|
deviceId: string;
|
|
connectionMode: XgridsConnectionMode;
|
|
source: ReadOnlyConnectionObservationSource;
|
|
serverBound: boolean;
|
|
expectedDiscoveryGeneration: number | null;
|
|
}
|
|
|
|
function isConnectionMode(value: unknown): value is XgridsConnectionMode {
|
|
return value === "bridge"
|
|
|| value === "quick-connect"
|
|
|| value === "direct-connect";
|
|
}
|
|
|
|
function exactPolicyObservationTarget(
|
|
state: XgridsK1State | null | undefined,
|
|
action: ReadOnlyNetworkObservationAction,
|
|
source: ReadOnlyConnectionObservationSource,
|
|
): ReadOnlyConnectionObservationTarget | null {
|
|
if (!connectionPolicyAllows(state, action)) return null;
|
|
const decision = connectionPolicyDecision(state, action);
|
|
const deviceId = decision?.required_transport_ref?.trim();
|
|
const connectionMode = decision?.required_connection_mode ?? null;
|
|
if (
|
|
decision?.target_source !== source
|
|
|| !deviceId
|
|
|| !isConnectionMode(connectionMode)
|
|
) {
|
|
return null;
|
|
}
|
|
if (source === "fresh-scan") {
|
|
const freshDevice = provisioningCandidateById(state?.devices ?? [], deviceId);
|
|
if (
|
|
!freshDevice
|
|
|| freshDevice.connectable === false
|
|
|| !Number.isInteger(state?.ble_discovery_generation)
|
|
|| (state?.ble_discovery_generation ?? -1) < 0
|
|
) return null;
|
|
}
|
|
return {
|
|
action,
|
|
deviceId,
|
|
connectionMode,
|
|
source,
|
|
serverBound: true,
|
|
expectedDiscoveryGeneration: source === "fresh-scan"
|
|
? state?.ble_discovery_generation as number
|
|
: null,
|
|
};
|
|
}
|
|
|
|
const SERVER_BOUND_RECOVERY_OBSERVATION_PRIORITY:
|
|
ReadonlyArray<ReadOnlyNetworkObservationAction> = [
|
|
"observe-current-device-network",
|
|
"observe-configured-device-network",
|
|
"observe-fresh-device-network",
|
|
];
|
|
|
|
/**
|
|
* Resolve a recovery Verify target from the public policy, never from a
|
|
* browser selection. A reconnect must prefer already bound or durable state
|
|
* over a projected fresh scan: a browser projection cannot prove that the
|
|
* native BLEDevice is still retained by the backend process. The backend
|
|
* policy still decides which exact actions are allowed; this helper only
|
|
* chooses the safest allowed source in current -> configured -> fresh order.
|
|
*/
|
|
export function recommendedConnectionRecoveryObservationTarget(
|
|
state: XgridsK1State | null | undefined,
|
|
): ReadOnlyConnectionObservationTarget | null {
|
|
const sources: Record<
|
|
ReadOnlyNetworkObservationAction,
|
|
ReadOnlyConnectionObservationSource
|
|
> = {
|
|
"observe-current-device-network": "retained-current-process",
|
|
"observe-configured-device-network": "durable-configured-state",
|
|
"observe-fresh-device-network": "fresh-scan",
|
|
};
|
|
for (const action of SERVER_BOUND_RECOVERY_OBSERVATION_PRIORITY) {
|
|
const target = exactPolicyObservationTarget(state, action, sources[action]);
|
|
if (target?.serverBound) return target;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* A terminal control session may block the network-observation actions while
|
|
* the supervisor still explicitly allows its narrower DeviceInfo Verify.
|
|
* Reuse that permission only when the physical ledger and durable topology
|
|
* independently pin the same K1 and mode; browser selection is never used.
|
|
*/
|
|
function exactControlVerificationTarget(
|
|
state: XgridsK1State | null | undefined,
|
|
): ReadOnlyConnectionObservationTarget | null {
|
|
if (
|
|
!requiresReadOnlyPhysicalRecovery(state)
|
|
|| !connectionPolicyAllows(state, "verify-control-device-info")
|
|
) return null;
|
|
|
|
const binding = readOnlyPhysicalRecoveryBinding(state);
|
|
const semanticStore = state?.semantic_topology_store;
|
|
const durable = semanticStore?.record;
|
|
if (
|
|
!binding
|
|
|| semanticStore?.status !== "available"
|
|
|| semanticStore.configured_offline_evidence !== true
|
|
|| semanticStore.live_connection_authority !== false
|
|
|| durable?.schema_version !== "missioncore.xgrids-k1-semantic-topology/v1"
|
|
|| transportRefEquivalenceKey(durable.transport_ref)
|
|
!== transportRefEquivalenceKey(binding.deviceId)
|
|
|| durable.connection_mode !== binding.connectionMode
|
|
) return null;
|
|
|
|
return {
|
|
action: "verify-control-device-info",
|
|
deviceId: binding.deviceId,
|
|
connectionMode: binding.connectionMode,
|
|
source: "durable-configured-state",
|
|
serverBound: true,
|
|
expectedDiscoveryGeneration: null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Resolve the one read-only BLE target authorized by the server policy.
|
|
* Unresolved writes never fall back to a browser selection or dropdown mode:
|
|
* their UUID, mode and recovery source must be pinned by the same decision.
|
|
*/
|
|
export function readOnlyConnectionObservationTarget(
|
|
state: XgridsK1State | null | undefined,
|
|
selectedDeviceId = "",
|
|
selectedConnectionMode: XgridsConnectionMode | null = null,
|
|
): ReadOnlyConnectionObservationTarget | null {
|
|
// An unresolved physical command must first use the exact durable
|
|
// DeviceInfo path when the supervisor authorizes it. A projected fresh BLE
|
|
// row can outlive the backend's native BLEDevice and therefore cannot
|
|
// outrank this read-only reconciliation route.
|
|
const exactControl = exactControlVerificationTarget(state);
|
|
if (exactControl) return exactControl;
|
|
|
|
const exactFresh = exactPolicyObservationTarget(
|
|
state,
|
|
"observe-fresh-device-network",
|
|
"fresh-scan",
|
|
);
|
|
if (exactFresh) return exactFresh;
|
|
|
|
// Outside reconciliation, the backend deliberately leaves the fresh target
|
|
// unpinned because the operator may choose among several current adverts.
|
|
// Preserve that reviewed path, but never use it for an unresolved write.
|
|
if (
|
|
!hasUnresolvedNetworkMutation(state)
|
|
&& !requiresReadOnlyPhysicalRecovery(state)
|
|
&& selectedConnectionMode
|
|
&& connectionPolicyAllows(state, "observe-fresh-device-network")
|
|
) {
|
|
const freshDevice = provisioningCandidateById(
|
|
state?.devices ?? [],
|
|
selectedDeviceId,
|
|
);
|
|
if (
|
|
freshDevice
|
|
&& freshDevice.connectable !== false
|
|
&& Number.isInteger(state?.ble_discovery_generation)
|
|
&& (state?.ble_discovery_generation ?? -1) >= 0
|
|
) {
|
|
return {
|
|
action: "observe-fresh-device-network",
|
|
deviceId: freshDevice.device_id,
|
|
connectionMode: selectedConnectionMode,
|
|
source: "fresh-scan",
|
|
serverBound: false,
|
|
expectedDiscoveryGeneration: state?.ble_discovery_generation as number,
|
|
};
|
|
}
|
|
}
|
|
|
|
return exactPolicyObservationTarget(
|
|
state,
|
|
"observe-current-device-network",
|
|
"retained-current-process",
|
|
) ?? exactPolicyObservationTarget(
|
|
state,
|
|
"observe-configured-device-network",
|
|
"durable-configured-state",
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Resolve recovery for an already-applied network only from a backend-pinned
|
|
* current/configured target. A browser-selected advertisement is never an
|
|
* authority for this read-only continuation, even when it happens to carry
|
|
* the same UUID.
|
|
*/
|
|
export function serverBoundAppliedNetworkObservationTarget(
|
|
state: XgridsK1State | null | undefined,
|
|
connectionMode: XgridsConnectionMode,
|
|
): ReadOnlyConnectionObservationTarget | null {
|
|
const target = exactPolicyObservationTarget(
|
|
state,
|
|
"observe-current-device-network",
|
|
"retained-current-process",
|
|
) ?? exactPolicyObservationTarget(
|
|
state,
|
|
"observe-configured-device-network",
|
|
"durable-configured-state",
|
|
);
|
|
return target?.serverBound === true
|
|
&& target.connectionMode === connectionMode
|
|
? target
|
|
: null;
|
|
}
|
|
|
|
export interface BackendConnectionTopology {
|
|
connectionMode: NonNullable<XgridsK1State["connection_mode"]>;
|
|
status: "active" | "configured-unverified" | "configured-offline";
|
|
source: "applied" | "durable" | "last-known";
|
|
endpoint: string | null;
|
|
}
|
|
|
|
function sameTarget(
|
|
left: { ipv4: string; port: number } | null | undefined,
|
|
right: { ipv4: string; port: number } | null | undefined,
|
|
): boolean {
|
|
return Boolean(
|
|
left
|
|
&& right
|
|
&& left.ipv4 === right.ipv4
|
|
&& left.port === right.port,
|
|
);
|
|
}
|
|
|
|
export function currentAppliedConnectionTopology(
|
|
state: XgridsK1State | null | undefined,
|
|
connectionMode?: NonNullable<XgridsK1State["connection_mode"]>,
|
|
): BackendConnectionTopology | null {
|
|
const supervisor = state?.connection_supervisor;
|
|
if (!supervisor || supervisor.closed) return null;
|
|
const { intent, lease, observed } = supervisor;
|
|
const deviceNetwork = observed.device_network;
|
|
const mode = deviceNetwork?.connection_mode;
|
|
const target = deviceNetwork?.target;
|
|
const currentDeviceNetwork = Boolean(
|
|
intent
|
|
&& mode
|
|
&& target
|
|
&& (!connectionMode || mode === connectionMode)
|
|
&& deviceNetwork.state === "applied"
|
|
&& deviceNetwork.intent_id === intent.intent_id
|
|
&& Boolean(deviceNetwork.transport_ref)
|
|
&& mode === intent.requested_mode
|
|
);
|
|
if (!currentDeviceNetwork || !mode || !target) return null;
|
|
const currentEndpoint = Boolean(
|
|
["configured-unverified", "reachable"].includes(lease.state)
|
|
&& lease.intent_id === intent?.intent_id
|
|
&& lease.connection_mode === mode
|
|
&& sameTarget(lease.target, target)
|
|
&& observed.host_path.available === true
|
|
&& observed.host_path.route_class === "direct"
|
|
&& lease.host_path_epoch === observed.host_path.epoch
|
|
&& observed.endpoint.intent_id === intent?.intent_id
|
|
&& observed.endpoint.host_path_epoch === observed.host_path.epoch
|
|
&& observed.endpoint.tcp_state === "reachable"
|
|
&& sameTarget(target, observed.endpoint.target)
|
|
);
|
|
const identity = observed.device_identity;
|
|
const controlPlane = observed.control_plane;
|
|
const lifecycle = state?.connection_lifecycle;
|
|
const activeBinding = lifecycle?.active_binding;
|
|
const identityExact = Boolean(
|
|
identity.state === "verified"
|
|
&& identity.intent_id === intent?.intent_id
|
|
&& identity.connection_mode === mode
|
|
&& identity.host_path_epoch === observed.host_path.epoch
|
|
&& identity.logical_device_id
|
|
&& lease.logical_device_id === identity.logical_device_id
|
|
&& (!intent?.expected_device_id
|
|
|| identity.logical_device_id === intent.expected_device_id),
|
|
);
|
|
const active = Boolean(
|
|
currentEndpoint
|
|
&& lease.state === "reachable"
|
|
&& supervisor.authority.control_allowed === true
|
|
&& identityExact
|
|
&& controlPlane.state === "healthy"
|
|
&& Boolean(controlPlane.session_id)
|
|
&& controlPlane.host_path_epoch === observed.host_path.epoch
|
|
&& lifecycle?.schema_version === "missioncore.xgrids-k1-connection-lifecycle/v1"
|
|
&& lifecycle.connection_ready === true
|
|
&& lifecycle.configured_mode === mode
|
|
&& lifecycle.active_mode === mode
|
|
&& activeBinding?.connection_mode === mode
|
|
&& activeBinding.intent_id === intent?.intent_id
|
|
&& transportRefEquivalenceKey(activeBinding.transport_ref)
|
|
=== transportRefEquivalenceKey(deviceNetwork.transport_ref)
|
|
&& activeBinding.target_ipv4 === target.ipv4
|
|
&& activeBinding.target_port === target.port
|
|
&& activeBinding.host_path_epoch === observed.host_path.epoch
|
|
&& activeBinding.control_session_id === controlPlane.session_id
|
|
);
|
|
return {
|
|
connectionMode: mode,
|
|
status: active
|
|
? "active"
|
|
: currentEndpoint
|
|
? "configured-unverified"
|
|
: "configured-offline",
|
|
source: "applied",
|
|
endpoint: target.ipv4,
|
|
};
|
|
}
|
|
|
|
export function backendConnectionTopology(
|
|
state: XgridsK1State | null | undefined,
|
|
connectionMode?: NonNullable<XgridsK1State["connection_mode"]>,
|
|
): BackendConnectionTopology | null {
|
|
const supervisor = state?.connection_supervisor;
|
|
const applied = currentAppliedConnectionTopology(state);
|
|
// A current BLE-proved device topology supersedes every persisted or
|
|
// historical address, including when the selected UI mode is different.
|
|
if (applied) {
|
|
return !connectionMode || applied.connectionMode === connectionMode
|
|
? applied
|
|
: null;
|
|
}
|
|
const semanticStore = state?.semantic_topology_store;
|
|
const durable = semanticStore?.record;
|
|
if (
|
|
semanticStore?.status === "available"
|
|
&& semanticStore.configured_offline_evidence === true
|
|
&& semanticStore.live_connection_authority === false
|
|
&& durable
|
|
&& durable.schema_version === "missioncore.xgrids-k1-semantic-topology/v1"
|
|
&& (!connectionMode || durable.connection_mode === connectionMode)
|
|
&& Boolean(durable.ipv4.trim())
|
|
) {
|
|
const endpointProbe = state?.configured_endpoint_probe;
|
|
const durableEndpointReachable = Boolean(
|
|
endpointProbe?.status === "reachable"
|
|
&& endpointProbe.target_source === "durable-semantic-topology"
|
|
&& endpointProbe.connection_mode === durable.connection_mode
|
|
&& endpointProbe.endpoint === durable.ipv4
|
|
&& transportRefEquivalenceKey(endpointProbe.transport_ref)
|
|
=== transportRefEquivalenceKey(durable.transport_ref)
|
|
&& endpointProbe.semantic_revision === durable.revision
|
|
&& endpointProbe.host_route_available === true
|
|
&& endpointProbe.host_route_class === "direct"
|
|
&& endpointProbe.tcp_reachable === true
|
|
&& endpointProbe.identity_validation === "not-performed"
|
|
&& endpointProbe.control_authority_granted === false
|
|
&& endpointProbe.ble_operation_performed === false
|
|
&& endpointProbe.network_mutation_performed === false
|
|
&& endpointProbe.automatic_retry === false
|
|
);
|
|
return {
|
|
connectionMode: durable.connection_mode,
|
|
status: durableEndpointReachable
|
|
? "configured-unverified"
|
|
: "configured-offline",
|
|
source: "durable",
|
|
endpoint: durable.ipv4,
|
|
};
|
|
}
|
|
const lastKnown = supervisor?.last_known;
|
|
if (
|
|
!lastKnown
|
|
|| (connectionMode && lastKnown.connection_mode !== connectionMode)
|
|
) return null;
|
|
return {
|
|
connectionMode: lastKnown.connection_mode,
|
|
status: "configured-offline",
|
|
source: "last-known",
|
|
endpoint: lastKnown.target.ipv4,
|
|
};
|
|
}
|
|
|
|
export function hasUnresolvedNetworkMutation(
|
|
state: XgridsK1State | null | undefined,
|
|
): boolean {
|
|
if (state?.network_write_reconciliation) return true;
|
|
const ledger = state?.network_mutation_ledger;
|
|
return Boolean(
|
|
ledger
|
|
&& (
|
|
ledger.status === "unresolved"
|
|
|| ledger.status === "corrupt"
|
|
|| ledger.mutation_allowed !== true
|
|
),
|
|
);
|
|
}
|
|
|
|
export function canSubmitProvisioningMutation({
|
|
devices,
|
|
selectedDeviceId,
|
|
credentialsReady,
|
|
isBusy,
|
|
}: {
|
|
devices: readonly BleDevice[];
|
|
selectedDeviceId: string;
|
|
credentialsReady: boolean;
|
|
isBusy: boolean;
|
|
}): boolean {
|
|
const candidate = provisioningCandidateById(devices, selectedDeviceId);
|
|
return Boolean(
|
|
credentialsReady &&
|
|
!isBusy &&
|
|
candidate &&
|
|
candidate.connectable !== false,
|
|
);
|
|
}
|
|
|
|
export function canAdmitProvisioningConnection({
|
|
policyAllowed,
|
|
targetSource,
|
|
hasSuccessfulLocalConnect,
|
|
localPrerequisitesReady,
|
|
}: {
|
|
policyAllowed: boolean;
|
|
targetSource: "fresh-scan" | null;
|
|
hasSuccessfulLocalConnect: boolean;
|
|
localPrerequisitesReady: boolean;
|
|
}): boolean {
|
|
return Boolean(
|
|
policyAllowed
|
|
&& targetSource === "fresh-scan"
|
|
&& !hasSuccessfulLocalConnect
|
|
&& localPrerequisitesReady,
|
|
);
|
|
}
|
|
|
|
export function isReachableConnectionLease(
|
|
state: XgridsK1State | null | undefined,
|
|
connectionMode: NonNullable<XgridsK1State["connection_mode"]>,
|
|
): boolean {
|
|
const supervisor = state?.connection_supervisor;
|
|
if (!supervisor || supervisor.closed) return false;
|
|
return currentAppliedConnectionTopology(state, connectionMode)?.status === "active";
|
|
}
|
|
|
|
export function isConfiguredConnectionLease(
|
|
state: XgridsK1State | null | undefined,
|
|
connectionMode: NonNullable<XgridsK1State["connection_mode"]>,
|
|
): boolean {
|
|
const topology = backendConnectionTopology(state, connectionMode);
|
|
return Boolean(topology && topology.source !== "last-known");
|
|
}
|
|
|
|
export function hasControlAuthority(
|
|
state: XgridsK1State | null | undefined,
|
|
): boolean {
|
|
const topology = currentAppliedConnectionTopology(state);
|
|
return Boolean(topology?.status === "active");
|
|
}
|
|
|
|
export function hasAuthoritativeData(
|
|
state: XgridsK1State | null | undefined,
|
|
): boolean {
|
|
const supervisor = state?.connection_supervisor;
|
|
const dataPlane = supervisor?.observed.data_plane;
|
|
return Boolean(
|
|
hasControlAuthority(state)
|
|
&& supervisor?.authority.data_ingest_authoritative === true
|
|
&& dataPlane?.state === "healthy"
|
|
&& Boolean(dataPlane.session_id)
|
|
&& dataPlane.host_path_epoch === supervisor.observed.host_path.epoch,
|
|
);
|
|
}
|
|
|
|
export function canonicalDeviceConnectivity(
|
|
state: XgridsK1State | null | undefined,
|
|
): "unknown" | "offline" | "connecting" | "connected" | "degraded" {
|
|
const topology = currentAppliedConnectionTopology(state);
|
|
if (topology?.status === "active") {
|
|
const supervisor = state?.connection_supervisor;
|
|
return supervisor
|
|
&& ["stalled", "lost"].includes(supervisor.observed.data_plane.state)
|
|
? "degraded"
|
|
: "connected";
|
|
}
|
|
if (topology?.status === "configured-unverified") return "connecting";
|
|
if (topology?.status === "configured-offline") return "offline";
|
|
const fallback = backendConnectionTopology(state);
|
|
if (fallback?.source === "last-known") return "degraded";
|
|
if (fallback?.status === "configured-offline") return "offline";
|
|
const supervisor = state?.connection_supervisor;
|
|
if (!supervisor || supervisor.closed) return supervisor?.closed ? "offline" : "unknown";
|
|
if (supervisor.lease.state === "lost" || supervisor.last_known) return "degraded";
|
|
return "offline";
|
|
}
|
|
|
|
export function activeConnectionEndpointLabel(
|
|
state: XgridsK1State | null | undefined,
|
|
): string | null {
|
|
const topology = currentAppliedConnectionTopology(state);
|
|
return topology?.status === "active" ? topology.endpoint : null;
|
|
}
|
|
|
|
export interface ReachableConnectionLeaseIdentity {
|
|
key: string;
|
|
runtimeId: string;
|
|
leaseGeneration: number;
|
|
intentId: string;
|
|
hostPathEpoch: number;
|
|
connectionMode: NonNullable<XgridsK1State["connection_mode"]>;
|
|
}
|
|
|
|
export interface RuntimeErrorCorrelation {
|
|
action: string;
|
|
runtimeId: string | null;
|
|
leaseGeneration: number | null;
|
|
connectionAttemptId: string | null;
|
|
}
|
|
|
|
/**
|
|
* Attach connection-attempt diagnostics only to the exact failed Connect
|
|
* action which produced them. A global Scan/Verify/Refresh failure must never
|
|
* borrow an older durable attempt merely because it remains in the snapshot.
|
|
*/
|
|
export function connectionAttemptForRuntimeError(
|
|
error: RuntimeErrorCorrelation | null | undefined,
|
|
state: XgridsK1State | null | undefined,
|
|
): XgridsConnectionAttempt | null {
|
|
const runtimeId = state?.snapshot_runtime_id?.trim() || null;
|
|
const attempt = state?.connection_attempt;
|
|
return error?.action === "connect"
|
|
&& typeof error.runtimeId === "string"
|
|
&& error.runtimeId === runtimeId
|
|
&& typeof error.connectionAttemptId === "string"
|
|
&& attempt
|
|
&& error.connectionAttemptId === attempt?.attempt_id
|
|
&& !["accepted", "running"].includes(attempt.status)
|
|
? attempt
|
|
: null;
|
|
}
|
|
|
|
export function reachableConnectionLeaseIdentity(
|
|
state: XgridsK1State | null | undefined,
|
|
): ReachableConnectionLeaseIdentity | null {
|
|
const runtimeId = state?.snapshot_runtime_id;
|
|
const supervisor = state?.connection_supervisor;
|
|
const connectionMode = supervisor?.lease.connection_mode;
|
|
const leaseGeneration = supervisor?.lease.generation;
|
|
const intentId = supervisor?.intent?.intent_id;
|
|
const hostPathEpoch = supervisor?.lease.host_path_epoch;
|
|
if (
|
|
typeof runtimeId !== "string"
|
|
|| !runtimeId.trim()
|
|
|| !connectionMode
|
|
|| !intentId
|
|
|| !Number.isInteger(leaseGeneration)
|
|
|| (leaseGeneration ?? -1) < 0
|
|
|| !Number.isInteger(hostPathEpoch)
|
|
|| (hostPathEpoch ?? 0) < 1
|
|
|| !isReachableConnectionLease(state, connectionMode)
|
|
) {
|
|
return null;
|
|
}
|
|
return {
|
|
key: `${runtimeId}:${intentId}:${hostPathEpoch}:${leaseGeneration}`,
|
|
runtimeId,
|
|
leaseGeneration: leaseGeneration as number,
|
|
intentId,
|
|
hostPathEpoch: hostPathEpoch as number,
|
|
connectionMode,
|
|
};
|
|
}
|
|
|
|
export function authoritativeReachableLeaseSupersedesError(
|
|
error: RuntimeErrorCorrelation | null | undefined,
|
|
state: XgridsK1State | null | undefined,
|
|
): boolean {
|
|
if (
|
|
!error
|
|
|| (error.action !== "connect" && error.action !== "verify")
|
|
|| typeof error.runtimeId !== "string"
|
|
|| !Number.isInteger(error.leaseGeneration)
|
|
// An omitted legacy field is not proof that the process-owned write fence
|
|
// was cleared. Only the canonical explicit null may dismiss the banner.
|
|
|| hasUnresolvedNetworkMutation(state)
|
|
) {
|
|
return false;
|
|
}
|
|
const identity = reachableConnectionLeaseIdentity(state);
|
|
return Boolean(
|
|
identity
|
|
&& identity.runtimeId === error.runtimeId
|
|
&& identity.leaseGeneration > (error.leaseGeneration as number),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Clear a transient action banner when a later authoritative state proves
|
|
* that the failed local acquisition has already been sealed and released.
|
|
* Every poll/WebSocket state enters through the same reducer, so recovery
|
|
* needs no refresh button or browser-cache reset.
|
|
*/
|
|
export function authoritativeStateSupersedesRuntimeError(
|
|
error: RuntimeErrorCorrelation | null | undefined,
|
|
state: XgridsK1State | null | undefined,
|
|
): boolean {
|
|
return Boolean(
|
|
authoritativeReachableLeaseSupersedesError(error, state)
|
|
|| (error && !shouldSurfaceRuntimeActionError(error.action, state)),
|
|
);
|
|
}
|
|
|
|
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 newOperationId(): string {
|
|
return `op-${defaultUuid()}`;
|
|
}
|
|
|
|
export function newPhysicalRetirementId(): string {
|
|
return `retirement-${defaultUuid()}`;
|
|
}
|
|
|
|
export function newPhysicalReopeningId(): string {
|
|
return `reopening-${defaultUuid()}`;
|
|
}
|
|
|
|
export function newMutationContext(action: string): {
|
|
operation_id: string;
|
|
idempotency_key: string;
|
|
} {
|
|
const normalizedAction = action.trim();
|
|
if (!normalizedAction) {
|
|
throw new Error("Действие операции не задано; безопасный ключ не создан.");
|
|
}
|
|
const operationId = newOperationId();
|
|
return {
|
|
operation_id: operationId,
|
|
idempotency_key: `${normalizedAction}:${operationId}`,
|
|
};
|
|
}
|
|
|
|
export function provisioningIntentKey(
|
|
current: string | null,
|
|
createUuid: () => string = defaultUuid,
|
|
): string {
|
|
return current ?? `network-provision:${createUuid()}`;
|
|
}
|