fix(k1): harden live handoff and camera recovery
This commit is contained in:
@@ -135,6 +135,23 @@ export class SnapshotRuntimeActionArbiter {
|
||||
}
|
||||
}
|
||||
|
||||
export function runtimeActionResponseAlreadyAccepted(
|
||||
responseState: XgridsK1State,
|
||||
acceptedState: XgridsK1State | null,
|
||||
): boolean {
|
||||
const responseRuntimeId = responseState.snapshot_runtime_id?.trim() ?? "";
|
||||
const acceptedRuntimeId = acceptedState?.snapshot_runtime_id?.trim() ?? "";
|
||||
const responseRevision = responseState.snapshot_revision;
|
||||
const acceptedRevision = acceptedState?.snapshot_revision;
|
||||
return Boolean(
|
||||
responseRuntimeId
|
||||
&& acceptedRuntimeId === responseRuntimeId
|
||||
&& Number.isSafeInteger(responseRevision)
|
||||
&& Number.isSafeInteger(acceptedRevision)
|
||||
&& (acceptedRevision as number) >= (responseRevision as number)
|
||||
);
|
||||
}
|
||||
|
||||
export type AcquisitionPreparationDraft = Omit<
|
||||
PrepareAcquisitionRequest,
|
||||
| "operation_id"
|
||||
@@ -248,6 +265,87 @@ export function connectionActionAuthoritySnapshot(
|
||||
}
|
||||
|
||||
const CONTROL_STATE_READ_INTERVAL_MS = 250;
|
||||
const NETWORK_PROVISION_SETTLEMENT_FALLBACK_MS = 30_000;
|
||||
const NETWORK_PROVISION_SETTLEMENT_MAX_MS = 300_000;
|
||||
const NETWORK_PROVISION_SETTLEMENT_GRACE_MS = 1_000;
|
||||
|
||||
function networkProvisionSettlementDeadline(
|
||||
operation: XgridsOperation,
|
||||
startedAtMs: number,
|
||||
): number {
|
||||
const operationDeadlineMs = operation.deadline_at
|
||||
? Date.parse(operation.deadline_at)
|
||||
: Number.NaN;
|
||||
const requestedDeadlineMs = Number.isFinite(operationDeadlineMs)
|
||||
&& operationDeadlineMs > startedAtMs
|
||||
? operationDeadlineMs + NETWORK_PROVISION_SETTLEMENT_GRACE_MS
|
||||
: startedAtMs + NETWORK_PROVISION_SETTLEMENT_FALLBACK_MS;
|
||||
return Math.min(
|
||||
requestedDeadlineMs,
|
||||
startedAtMs + NETWORK_PROVISION_SETTLEMENT_MAX_MS,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow one already-admitted Apply after a browser response is interrupted by
|
||||
* the host Wi-Fi handoff. This loop only reads the local journal; it never
|
||||
* retries the HTTP mutation, BLE write, CoreWLAN association or control open.
|
||||
*/
|
||||
export async function awaitNetworkProvisionSettlementAfterLostResponse(
|
||||
initialState: XgridsK1State,
|
||||
idempotencyKey: string,
|
||||
readState: () => Promise<XgridsK1State>,
|
||||
acceptState: (state: XgridsK1State) => void,
|
||||
assertOperatorIntentCurrent: () => void,
|
||||
options: {
|
||||
now?: () => number;
|
||||
wait?: (delayMs: number) => Promise<void>;
|
||||
} = {},
|
||||
): Promise<XgridsK1State> {
|
||||
const now = options.now ?? Date.now;
|
||||
const wait = options.wait ?? ((delayMs: number) => new Promise<void>((resolve) => {
|
||||
globalThis.setTimeout(resolve, delayMs);
|
||||
}));
|
||||
let state = initialState;
|
||||
let operation = operationByIdempotencyKey(
|
||||
state,
|
||||
"network.provision",
|
||||
idempotencyKey,
|
||||
);
|
||||
if (!operation || !["accepted", "running"].includes(operation.status)) {
|
||||
return state;
|
||||
}
|
||||
const deadlineMs = networkProvisionSettlementDeadline(operation, now());
|
||||
|
||||
while (["accepted", "running"].includes(operation.status)) {
|
||||
assertOperatorIntentCurrent();
|
||||
const remainingMs = deadlineMs - now();
|
||||
if (remainingMs <= 0) return state;
|
||||
await wait(Math.min(CONTROL_STATE_READ_INTERVAL_MS, remainingMs));
|
||||
assertOperatorIntentCurrent();
|
||||
try {
|
||||
const nextState = await readState();
|
||||
assertOperatorIntentCurrent();
|
||||
acceptState(nextState);
|
||||
state = nextState;
|
||||
} catch (readError) {
|
||||
// A host Wi-Fi transition can briefly abort even a localhost fetch.
|
||||
// Preserve the admitted operation and perform only the next bounded
|
||||
// journal read; the original Apply is never reissued.
|
||||
if (!(readError instanceof ApiError) || !readError.transportUnavailable) {
|
||||
throw readError;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
operation = operationByIdempotencyKey(
|
||||
state,
|
||||
"network.provision",
|
||||
idempotencyKey,
|
||||
);
|
||||
if (!operation) return state;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
const CONTROL_PHASE_WAIT_TIMEOUT_MS = 120_000;
|
||||
|
||||
function controlPhase(state: XgridsK1State): XgridsApplicationControlPhase {
|
||||
@@ -977,7 +1075,10 @@ export function useXgridsK1Runtime(enabled: boolean) {
|
||||
!operatorIntents.current.isRuntimeCurrent(runtimeToken)
|
||||
|| !runtimeActionArbiter.current.isCurrent(actionToken)
|
||||
) return false;
|
||||
if (!acceptState(nextState)) return false;
|
||||
if (
|
||||
!acceptState(nextState)
|
||||
&& !runtimeActionResponseAlreadyAccepted(nextState, latestState.current)
|
||||
) return false;
|
||||
if (!runtimeActionArbiter.current.isCurrent(actionToken)) return false;
|
||||
return true;
|
||||
} catch (operationError) {
|
||||
@@ -1355,6 +1456,13 @@ export function useXgridsK1Runtime(enabled: boolean) {
|
||||
throw connectError;
|
||||
}
|
||||
acceptState(failedState);
|
||||
failedState = await awaitNetworkProvisionSettlementAfterLostResponse(
|
||||
failedState,
|
||||
request.idempotency_key,
|
||||
() => xgridsK1Api.getState(),
|
||||
acceptState,
|
||||
assertOperatorIntentCurrent,
|
||||
);
|
||||
const failedOperation = operationByIdempotencyKey(
|
||||
failedState,
|
||||
"network.provision",
|
||||
|
||||
Reference in New Issue
Block a user