1797 lines
48 KiB
TypeScript
1797 lines
48 KiB
TypeScript
import type { ClockPort } from "../../application/ports/clock-port.ts";
|
|
import type {
|
|
RealtimeTransportEventOutcome,
|
|
} from "../../application/ports/realtime/event-authority.ts";
|
|
import {
|
|
type RealtimeFailureKind,
|
|
type RealtimeResult,
|
|
} from "../../application/ports/realtime/shared.ts";
|
|
import { systemClock } from "../platform/system-clock.ts";
|
|
import {
|
|
calculateReconnectDelay,
|
|
defineReconnectPolicy,
|
|
isReconnectAttemptResetEligible,
|
|
REALTIME_RECONNECT_CEILINGS,
|
|
reconnectBudgetRemaining,
|
|
type ReconnectPolicy,
|
|
} from "./reconnect-policy.ts";
|
|
import {
|
|
isRealtimeResult,
|
|
isRealtimeTransportEventOutcome,
|
|
realtimeFailure,
|
|
} from "./result.ts";
|
|
|
|
type FailureResult = Extract<RealtimeResult<never>, { ok: false }>;
|
|
type SuccessResult<Value> = Extract<
|
|
RealtimeResult<Value>,
|
|
{ ok: true }
|
|
>;
|
|
|
|
export type RealtimeCommittedRecovery = Extract<
|
|
RealtimeTransportEventOutcome,
|
|
{ kind: "RECOVERY_COMMITTED" }
|
|
>;
|
|
|
|
/**
|
|
* The hint is coordinator-private metadata. A terminal RealtimeResult can
|
|
* therefore be returned without rewriting its failure.
|
|
*/
|
|
export type RealtimeReconnectOutcome<Value> =
|
|
| Readonly<{
|
|
result: SuccessResult<Value>;
|
|
}>
|
|
| Readonly<{
|
|
result: FailureResult;
|
|
serverNotBeforeMs?: number | null;
|
|
}>;
|
|
|
|
export type RealtimeReconnectAttemptContext = Readonly<{
|
|
signal: AbortSignal;
|
|
pendingRecovery: RealtimeCommittedRecovery | null;
|
|
isCurrent(): boolean;
|
|
/**
|
|
* Call only after a protocol-valid heartbeat or event.
|
|
*/
|
|
markValidHeartbeatOrEvent(): void;
|
|
/**
|
|
* A transport may expose readiness and then wait here before admitting
|
|
* inbound events. With no pending recovery the call fails closed.
|
|
*/
|
|
waitForRecoveryBarrierConfirmation(): Promise<
|
|
RealtimeResult<void>
|
|
>;
|
|
}>;
|
|
|
|
export type RealtimeReconnectAttemptSuccess<ClosedReceipt> =
|
|
Readonly<{
|
|
session: RealtimeReconnectSession<ClosedReceipt>;
|
|
establishedRecoveryBarrier?: RealtimeCommittedRecovery;
|
|
}>;
|
|
|
|
export type RealtimeRecoveryReconnectDirective = Readonly<{
|
|
kind: "RECOVERY_RECONNECT";
|
|
recovery: RealtimeCommittedRecovery;
|
|
terminalResult: FailureResult;
|
|
serverNotBeforeMs?: number | null;
|
|
}>;
|
|
|
|
export type RealtimeReconnectCloseClassification =
|
|
| RealtimeReconnectOutcome<void>
|
|
| RealtimeRecoveryReconnectDirective;
|
|
|
|
/**
|
|
* The receipt stays strongly typed until `classifyClosed`. `close` must be
|
|
* terminal and idempotent.
|
|
*/
|
|
export type RealtimeReconnectSession<
|
|
ClosedReceipt = RealtimeReconnectOutcome<void>,
|
|
> = Readonly<{
|
|
waitClosed():
|
|
| ClosedReceipt
|
|
| Promise<ClosedReceipt>;
|
|
classifyClosed(
|
|
receipt: ClosedReceipt,
|
|
): RealtimeReconnectCloseClassification;
|
|
close(): void;
|
|
}>;
|
|
|
|
export type RealtimeReconnectEnvironment = Readonly<{
|
|
online(): boolean;
|
|
subscribeOnline(listener: (online: boolean) => void): () => void;
|
|
}>;
|
|
|
|
export type RealtimeReconnectRunInput = Readonly<{
|
|
signal?: AbortSignal;
|
|
initialRecovery?: RealtimeRecoveryReconnectDirective;
|
|
}>;
|
|
|
|
export type RealtimeReconnectCoordinator = Readonly<{
|
|
run(input?: RealtimeReconnectRunInput): Promise<
|
|
RealtimeResult<void>
|
|
>;
|
|
getState(): "CLOSED" | "DRAINING" | "IDLE" | "RUNNING";
|
|
close(): void;
|
|
}>;
|
|
|
|
export type RealtimeReconnectCoordinatorDependencies<
|
|
ClosedReceipt = RealtimeReconnectOutcome<void>,
|
|
> = Readonly<{
|
|
policy: ReconnectPolicy;
|
|
environment: RealtimeReconnectEnvironment;
|
|
attempt(
|
|
context: RealtimeReconnectAttemptContext,
|
|
):
|
|
| RealtimeReconnectOutcome<
|
|
RealtimeReconnectAttemptSuccess<ClosedReceipt>
|
|
>
|
|
| Promise<
|
|
RealtimeReconnectOutcome<
|
|
RealtimeReconnectAttemptSuccess<ClosedReceipt>
|
|
>
|
|
>;
|
|
confirmTransportBarrier(
|
|
recovery: RealtimeCommittedRecovery,
|
|
): RealtimeResult<void>;
|
|
clock?: ClockPort;
|
|
random?: () => number;
|
|
isCurrent?: () => boolean;
|
|
}>;
|
|
|
|
const RETRYABLE_KINDS = Object.freeze([
|
|
"CONNECT_TIMEOUT",
|
|
"IDLE_TIMEOUT",
|
|
"OFFLINE",
|
|
"PROVIDER_UNAVAILABLE",
|
|
"RATE_LIMITED",
|
|
] as const satisfies readonly RealtimeFailureKind[]);
|
|
|
|
type Retry = Readonly<{
|
|
result: FailureResult;
|
|
terminalResult: FailureResult;
|
|
at: number;
|
|
notBeforeMs: number | null;
|
|
}>;
|
|
|
|
type Settled<Value> =
|
|
| Readonly<{ kind: "VALUE"; value: Value }>
|
|
| Readonly<{ kind: "THREW" }>;
|
|
|
|
type AwaitedTask<Value> =
|
|
| Settled<Value>
|
|
| Readonly<{ kind: "INTERRUPTED" }>;
|
|
|
|
type DrainedTask<Value> =
|
|
| AwaitedTask<Value>
|
|
| Readonly<{ kind: "TIMED_OUT" }>;
|
|
|
|
type Phase = Readonly<{
|
|
controller: AbortController;
|
|
offlineVersion: number;
|
|
dispose(): void;
|
|
}>;
|
|
|
|
type BarrierGate = Readonly<{
|
|
promise: Promise<RealtimeResult<void>>;
|
|
settle(result: RealtimeResult<void>): void;
|
|
}>;
|
|
|
|
type FailureSnapshot = Readonly<{
|
|
result: FailureResult;
|
|
kind: RealtimeFailureKind;
|
|
retryable: boolean;
|
|
}>;
|
|
|
|
type ParsedResult<Value> =
|
|
| Readonly<{
|
|
ok: true;
|
|
result: SuccessResult<unknown>;
|
|
value: Value;
|
|
}>
|
|
| Readonly<{
|
|
ok: false;
|
|
failure: FailureSnapshot;
|
|
}>;
|
|
|
|
type ParsedOutcome<Value> =
|
|
| Readonly<{
|
|
ok: true;
|
|
result: SuccessResult<unknown>;
|
|
value: Value;
|
|
}>
|
|
| Readonly<{
|
|
ok: false;
|
|
failure: FailureSnapshot;
|
|
serverNotBeforeMs: number | null;
|
|
}>;
|
|
|
|
type ParsedAttemptSuccess<ClosedReceipt> = Readonly<{
|
|
session: RealtimeReconnectSession<ClosedReceipt>;
|
|
hasRecoveryBarrier: boolean;
|
|
establishedRecoveryBarrier: unknown;
|
|
}>;
|
|
|
|
type ParsedRecoveryDirective = Readonly<{
|
|
recovery: RealtimeCommittedRecovery;
|
|
terminalFailure: FailureSnapshot;
|
|
serverNotBeforeMs: number | null;
|
|
}>;
|
|
|
|
type ParsedCloseClassification =
|
|
| Readonly<{
|
|
kind: "OUTCOME";
|
|
outcome: ParsedOutcome<void>;
|
|
}>
|
|
| Readonly<{
|
|
kind: "RECOVERY_RECONNECT";
|
|
directive: ParsedRecoveryDirective;
|
|
}>;
|
|
|
|
type DataSnapshot = Readonly<{
|
|
source: object;
|
|
keys: readonly string[];
|
|
values: Readonly<Record<string, unknown>>;
|
|
}>;
|
|
|
|
type ParsedRunInput = Readonly<{
|
|
signal: AbortSignal | undefined;
|
|
initialRecovery: ParsedRecoveryDirective | null;
|
|
}>;
|
|
|
|
type ParsedValue<Value> = Readonly<{ value: Value }>;
|
|
|
|
export function createRealtimeReconnectCoordinator<ClosedReceipt>(
|
|
dependencies: RealtimeReconnectCoordinatorDependencies<ClosedReceipt>,
|
|
): RealtimeReconnectCoordinator {
|
|
const attempt = dependencies?.attempt;
|
|
const confirmTransportBarrier =
|
|
dependencies?.confirmTransportBarrier;
|
|
const environment = dependencies?.environment;
|
|
if (
|
|
typeof attempt !== "function" ||
|
|
typeof confirmTransportBarrier !== "function" ||
|
|
typeof environment?.online !== "function" ||
|
|
typeof environment.subscribeOnline !== "function"
|
|
) {
|
|
throw new TypeError("Invalid realtime reconnect dependencies.");
|
|
}
|
|
|
|
const policy = defineReconnectPolicy(dependencies.policy);
|
|
const clock = dependencies.clock ?? systemClock;
|
|
const random = dependencies.random ?? Math.random;
|
|
const scopeCurrent = dependencies.isCurrent ?? (() => true);
|
|
let state: "CLOSED" | "DRAINING" | "IDLE" | "RUNNING" = "IDLE";
|
|
let generation = 0;
|
|
let activeTask: Promise<Settled<unknown>> | null = null;
|
|
let runController: AbortController | null = null;
|
|
let phaseController: AbortController | null = null;
|
|
let session: RealtimeReconnectSession<ClosedReceipt> | null = null;
|
|
let activeRecoveryGate: BarrierGate | null = null;
|
|
|
|
function retainTracked<Value>(
|
|
task: Promise<Settled<Value>>,
|
|
onSettled?: (outcome: Settled<Value>) => void,
|
|
): Promise<Settled<Value>> {
|
|
const retained = onSettled
|
|
? task.then((outcome) => {
|
|
onSettled(outcome);
|
|
return outcome;
|
|
})
|
|
: task;
|
|
const tracked = retained as Promise<Settled<unknown>>;
|
|
activeTask = tracked;
|
|
void retained.then(() => {
|
|
if (activeTask === tracked) {
|
|
activeTask = null;
|
|
if (state === "DRAINING") state = "IDLE";
|
|
}
|
|
});
|
|
return retained;
|
|
}
|
|
|
|
function track<Value>(
|
|
task: () => Value | Promise<Value>,
|
|
): Promise<Settled<Value>> {
|
|
let execution: Promise<Value>;
|
|
try {
|
|
execution = Promise.resolve(task());
|
|
} catch {
|
|
execution = Promise.reject();
|
|
}
|
|
const settled = execution.then<Settled<Value>, Settled<Value>>(
|
|
(value) => Object.freeze({ kind: "VALUE", value }),
|
|
() => Object.freeze({ kind: "THREW" }),
|
|
);
|
|
return retainTracked(settled);
|
|
}
|
|
|
|
async function run(
|
|
input: RealtimeReconnectRunInput = {},
|
|
): Promise<RealtimeResult<void>> {
|
|
if (state === "CLOSED") return failure("CLOSED");
|
|
if (state !== "IDLE") return failure("PROTOCOL_MISMATCH");
|
|
|
|
const parsedInput = parseRunInput(input);
|
|
if (!parsedInput) return failure("PROTOCOL_MISMATCH");
|
|
const inputSignal = parsedInput.signal;
|
|
const initiallyAborted = safeSignalAborted(inputSignal);
|
|
if (initiallyAborted === null) {
|
|
return failure("PROTOCOL_MISMATCH");
|
|
}
|
|
if (initiallyAborted) return failure("ABORTED");
|
|
if (!safeCurrent(scopeCurrent)) return failure("SCOPE_FENCED");
|
|
|
|
const initialNow = safeNow(clock);
|
|
if (initialNow === null) return failure("PROVIDER_UNAVAILABLE");
|
|
|
|
state = "RUNNING";
|
|
const runGeneration = ++generation;
|
|
const controller = new AbortController();
|
|
runController = controller;
|
|
let requested: RealtimeFailureKind | null = null;
|
|
let online = false;
|
|
let onlineVersion = 0;
|
|
let offlineVersion = 0;
|
|
let requiredOnlineVersion: number | null = null;
|
|
let wakeOnline: (() => void) | null = null;
|
|
let unsubscribe: (() => void) | undefined;
|
|
let pendingRecovery = parsedInput.initialRecovery;
|
|
let retry =
|
|
pendingRecovery === null
|
|
? null
|
|
: captureRetry(
|
|
pendingRecovery.terminalFailure.result,
|
|
pendingRecovery.terminalFailure.result,
|
|
initialNow,
|
|
pendingRecovery.serverNotBeforeMs,
|
|
);
|
|
let retriesUsed = 0;
|
|
let budgetStartedAt = initialNow;
|
|
|
|
const currentFailure = (): RealtimeFailureKind | null => {
|
|
if (state === "CLOSED" || generation !== runGeneration) {
|
|
return "CLOSED";
|
|
}
|
|
if (requested) return requested;
|
|
return safeCurrent(scopeCurrent) ? null : "SCOPE_FENCED";
|
|
};
|
|
|
|
const settleActiveGate = (result: RealtimeResult<void>) => {
|
|
activeRecoveryGate?.settle(result);
|
|
};
|
|
|
|
const stop = (kind: RealtimeFailureKind) => {
|
|
requested ??= kind;
|
|
settleActiveGate(recoveryFailure(kind));
|
|
controller.abort();
|
|
phaseController?.abort();
|
|
safelyClose(session);
|
|
wakeOnline?.();
|
|
};
|
|
|
|
const requireOnline = () => {
|
|
requiredOnlineVersion = Math.max(
|
|
requiredOnlineVersion ?? 0,
|
|
onlineVersion + 1,
|
|
);
|
|
};
|
|
|
|
const observeOnline = (value: boolean) => {
|
|
if (typeof value !== "boolean") {
|
|
stop("PROVIDER_UNAVAILABLE");
|
|
return;
|
|
}
|
|
online = value;
|
|
if (value) {
|
|
onlineVersion += 1;
|
|
if (
|
|
requiredOnlineVersion !== null &&
|
|
onlineVersion >= requiredOnlineVersion
|
|
) {
|
|
requiredOnlineVersion = null;
|
|
}
|
|
if (requiredOnlineVersion === null) wakeOnline?.();
|
|
} else {
|
|
offlineVersion += 1;
|
|
requireOnline();
|
|
settleActiveGate(recoveryFailure("OFFLINE"));
|
|
phaseController?.abort();
|
|
safelyClose(session);
|
|
}
|
|
};
|
|
|
|
const waitOnline = async (): Promise<boolean> => {
|
|
if (online && requiredOnlineVersion === null) return true;
|
|
await new Promise<void>((resolve) => {
|
|
const wake = () => {
|
|
if (wakeOnline === wake) wakeOnline = null;
|
|
controller.signal.removeEventListener("abort", wake);
|
|
resolve();
|
|
};
|
|
wakeOnline = wake;
|
|
controller.signal.addEventListener("abort", wake, {
|
|
once: true,
|
|
});
|
|
if (
|
|
controller.signal.aborted ||
|
|
(online && requiredOnlineVersion === null)
|
|
) {
|
|
wake();
|
|
}
|
|
});
|
|
return (
|
|
currentFailure() === null &&
|
|
online &&
|
|
requiredOnlineVersion === null
|
|
);
|
|
};
|
|
|
|
const beginPhase = (): Phase => {
|
|
const phase = new AbortController();
|
|
const abort = () => phase.abort();
|
|
controller.signal.addEventListener("abort", abort, {
|
|
once: true,
|
|
});
|
|
if (controller.signal.aborted || !online) phase.abort();
|
|
phaseController = phase;
|
|
const capturedOfflineVersion = offlineVersion;
|
|
return Object.freeze({
|
|
controller: phase,
|
|
offlineVersion: capturedOfflineVersion,
|
|
dispose() {
|
|
controller.signal.removeEventListener("abort", abort);
|
|
if (phaseController === phase) phaseController = null;
|
|
},
|
|
});
|
|
};
|
|
|
|
const wentOffline = (phase: Phase) =>
|
|
phase.offlineVersion !== offlineVersion || !online;
|
|
|
|
const onAbort = () => stop("ABORTED");
|
|
if (!safelyAddAbortListener(inputSignal, onAbort)) {
|
|
stop("PROVIDER_UNAVAILABLE");
|
|
}
|
|
const abortedAfterRegistration = safeSignalAborted(inputSignal);
|
|
if (abortedAfterRegistration === null) {
|
|
stop("PROVIDER_UNAVAILABLE");
|
|
} else if (abortedAfterRegistration) {
|
|
onAbort();
|
|
}
|
|
|
|
try {
|
|
try {
|
|
unsubscribe = environment.subscribeOnline(observeOnline);
|
|
if (typeof unsubscribe !== "function") {
|
|
stop("PROVIDER_UNAVAILABLE");
|
|
}
|
|
} catch {
|
|
stop("PROVIDER_UNAVAILABLE");
|
|
}
|
|
|
|
const initiallyOnline = safeOnline(environment);
|
|
if (initiallyOnline === null) {
|
|
stop("PROVIDER_UNAVAILABLE");
|
|
} else {
|
|
online = initiallyOnline;
|
|
if (!online) requireOnline();
|
|
}
|
|
|
|
while (true) {
|
|
const stopped = currentFailure();
|
|
if (stopped) return failure(stopped);
|
|
|
|
if (retry) {
|
|
let now = safeNow(clock);
|
|
if (
|
|
now === null ||
|
|
!canRetry(
|
|
retry,
|
|
retriesUsed,
|
|
budgetStartedAt,
|
|
now,
|
|
policy,
|
|
)
|
|
) {
|
|
return retry.terminalResult;
|
|
}
|
|
if (
|
|
(!online || requiredOnlineVersion !== null) &&
|
|
!(await waitOnline())
|
|
) {
|
|
return failure(currentFailure() ?? "ABORTED");
|
|
}
|
|
|
|
now = safeNow(clock);
|
|
if (now === null) return retry.terminalResult;
|
|
const delay = nextDelay(
|
|
retry,
|
|
retriesUsed,
|
|
budgetStartedAt,
|
|
now,
|
|
policy,
|
|
random,
|
|
);
|
|
if (delay === null) return retry.terminalResult;
|
|
|
|
const phase = beginPhase();
|
|
const sleeping = track(
|
|
() => clock.sleep(delay, phase.controller.signal),
|
|
);
|
|
const slept = await race(sleeping, phase.controller.signal);
|
|
const offline = wentOffline(phase);
|
|
if (slept.kind === "INTERRUPTED") {
|
|
if (offline) {
|
|
const drained = await drainWithinCeiling(
|
|
sleeping,
|
|
controller.signal,
|
|
);
|
|
phase.dispose();
|
|
const afterDrain = currentFailure();
|
|
if (drained.kind === "INTERRUPTED" || afterDrain) {
|
|
return failure(afterDrain ?? "ABORTED");
|
|
}
|
|
if (drained.kind === "TIMED_OUT") {
|
|
return failure("PROVIDER_UNAVAILABLE");
|
|
}
|
|
continue;
|
|
}
|
|
phase.dispose();
|
|
return failure(currentFailure() ?? "ABORTED");
|
|
}
|
|
phase.dispose();
|
|
if (offline) continue;
|
|
if (slept.kind === "THREW") return retry.terminalResult;
|
|
now = safeNow(clock);
|
|
if (
|
|
now === null ||
|
|
reconnectBudgetRemaining(
|
|
policy,
|
|
budgetStartedAt,
|
|
now,
|
|
) <= 0
|
|
) {
|
|
return retry.terminalResult;
|
|
}
|
|
retriesUsed += 1;
|
|
} else if (!online || requiredOnlineVersion !== null) {
|
|
retry = offlineRetry(initialNow, "CONNECT");
|
|
continue;
|
|
}
|
|
|
|
const beforeAttempt = safeNow(clock);
|
|
if (
|
|
beforeAttempt === null ||
|
|
reconnectBudgetRemaining(
|
|
policy,
|
|
budgetStartedAt,
|
|
beforeAttempt,
|
|
) <= 0
|
|
) {
|
|
return (
|
|
retry?.terminalResult ??
|
|
failure("PROVIDER_UNAVAILABLE")
|
|
);
|
|
}
|
|
|
|
const phase = beginPhase();
|
|
let validSignalObserved = false;
|
|
const attemptRecovery = pendingRecovery?.recovery ?? null;
|
|
const attemptGate =
|
|
attemptRecovery === null ? null : createBarrierGate();
|
|
if (attemptGate) activeRecoveryGate = attemptGate;
|
|
const noPendingRecovery = recoveryFailure(
|
|
"PROTOCOL_MISMATCH",
|
|
);
|
|
const settleAttemptGate = (
|
|
result: RealtimeResult<void>,
|
|
) => {
|
|
attemptGate?.settle(result);
|
|
if (activeRecoveryGate === attemptGate) {
|
|
activeRecoveryGate = null;
|
|
}
|
|
};
|
|
const isAttemptCurrent = () =>
|
|
state === "RUNNING" &&
|
|
generation === runGeneration &&
|
|
phaseController === phase.controller &&
|
|
!phase.controller.signal.aborted &&
|
|
online &&
|
|
safeCurrent(scopeCurrent);
|
|
const context =
|
|
Object.freeze<RealtimeReconnectAttemptContext>({
|
|
signal: phase.controller.signal,
|
|
pendingRecovery: attemptRecovery,
|
|
isCurrent: isAttemptCurrent,
|
|
markValidHeartbeatOrEvent() {
|
|
if (isAttemptCurrent()) validSignalObserved = true;
|
|
},
|
|
waitForRecoveryBarrierConfirmation() {
|
|
return (
|
|
attemptGate?.promise ??
|
|
Promise.resolve(noPendingRecovery)
|
|
);
|
|
},
|
|
});
|
|
const attempting = track(
|
|
() => attempt(context),
|
|
);
|
|
const attempted = await race(
|
|
attempting,
|
|
phase.controller.signal,
|
|
);
|
|
const attemptOffline = wentOffline(phase);
|
|
|
|
if (attempted.kind === "INTERRUPTED") {
|
|
const retainedAttempt = retainTracked(
|
|
attempting,
|
|
closeLateSession,
|
|
);
|
|
if (attemptOffline) {
|
|
const offlineFailure = recoveryFailure("OFFLINE");
|
|
settleAttemptGate(offlineFailure);
|
|
const drained = await drainWithinCeiling(
|
|
retainedAttempt,
|
|
controller.signal,
|
|
);
|
|
phase.dispose();
|
|
const afterDrain = currentFailure();
|
|
if (drained.kind === "INTERRUPTED" || afterDrain) {
|
|
return failure(afterDrain ?? "ABORTED");
|
|
}
|
|
if (drained.kind === "TIMED_OUT") {
|
|
return failure("PROVIDER_UNAVAILABLE");
|
|
}
|
|
const drainedAt = safeNow(clock);
|
|
if (drainedAt === null) {
|
|
return failure("PROVIDER_UNAVAILABLE");
|
|
}
|
|
retry = offlineRetry(
|
|
drainedAt,
|
|
"CONNECT",
|
|
pendingRecovery?.terminalFailure.result,
|
|
);
|
|
continue;
|
|
}
|
|
const interrupted = currentFailure() ?? "ABORTED";
|
|
settleAttemptGate(recoveryFailure(interrupted));
|
|
phase.dispose();
|
|
return failure(interrupted);
|
|
}
|
|
|
|
const afterAttempt = currentFailure();
|
|
if (afterAttempt) {
|
|
closeLateSession(attempted);
|
|
settleAttemptGate(recoveryFailure(afterAttempt));
|
|
phase.controller.abort();
|
|
phase.dispose();
|
|
return failure(afterAttempt);
|
|
}
|
|
if (attemptOffline) {
|
|
closeLateSession(attempted);
|
|
settleAttemptGate(recoveryFailure("OFFLINE"));
|
|
phase.controller.abort();
|
|
phase.dispose();
|
|
retry = offlineRetry(
|
|
safeNow(clock) ?? beforeAttempt,
|
|
"CONNECT",
|
|
pendingRecovery?.terminalFailure.result,
|
|
);
|
|
continue;
|
|
}
|
|
if (attempted.kind === "THREW") {
|
|
const nextRetry = providerRetry(
|
|
safeNow(clock) ?? beforeAttempt,
|
|
"CONNECT",
|
|
pendingRecovery?.terminalFailure.result,
|
|
);
|
|
settleAttemptGate(nextRetry.result);
|
|
phase.controller.abort();
|
|
phase.dispose();
|
|
retry = nextRetry;
|
|
continue;
|
|
}
|
|
|
|
const opened = parseAttemptOutcome<ClosedReceipt>(
|
|
attempted.value,
|
|
);
|
|
if (!opened) {
|
|
const mismatch = failure("PROTOCOL_MISMATCH");
|
|
settleAttemptGate(mismatch);
|
|
phase.controller.abort();
|
|
phase.dispose();
|
|
return mismatch;
|
|
}
|
|
|
|
const afterAttemptValidation = currentFailure();
|
|
if (afterAttemptValidation) {
|
|
if (opened.ok) safelyClose(opened.value.session);
|
|
settleAttemptGate(
|
|
recoveryFailure(afterAttemptValidation),
|
|
);
|
|
phase.controller.abort();
|
|
phase.dispose();
|
|
return failure(afterAttemptValidation);
|
|
}
|
|
|
|
if (!opened.ok) {
|
|
settleAttemptGate(opened.failure.result);
|
|
phase.controller.abort();
|
|
phase.dispose();
|
|
if (
|
|
!reconnectable(
|
|
opened.failure,
|
|
opened.serverNotBeforeMs,
|
|
)
|
|
) {
|
|
return opened.failure.result;
|
|
}
|
|
const failedAt = safeNow(clock);
|
|
if (failedAt === null) return opened.failure.result;
|
|
retry = captureRetry(
|
|
opened.failure.result,
|
|
pendingRecovery?.terminalFailure.result ??
|
|
opened.failure.result,
|
|
failedAt,
|
|
opened.serverNotBeforeMs,
|
|
);
|
|
if (opened.failure.kind === "OFFLINE") requireOnline();
|
|
continue;
|
|
}
|
|
|
|
const active = opened.value.session;
|
|
if (pendingRecovery) {
|
|
if (
|
|
!opened.value.hasRecoveryBarrier ||
|
|
opened.value.establishedRecoveryBarrier !==
|
|
pendingRecovery.recovery
|
|
) {
|
|
const mismatch = recoveryFailure(
|
|
"PROTOCOL_MISMATCH",
|
|
);
|
|
settleAttemptGate(mismatch);
|
|
safelyClose(active);
|
|
phase.controller.abort();
|
|
phase.dispose();
|
|
return mismatch;
|
|
}
|
|
|
|
const beforeConfirmation = currentFailure();
|
|
if (beforeConfirmation) {
|
|
settleAttemptGate(
|
|
recoveryFailure(beforeConfirmation),
|
|
);
|
|
safelyClose(active);
|
|
phase.controller.abort();
|
|
phase.dispose();
|
|
return failure(beforeConfirmation);
|
|
}
|
|
|
|
let rawConfirmation: unknown;
|
|
try {
|
|
rawConfirmation =
|
|
confirmTransportBarrier(pendingRecovery.recovery);
|
|
} catch {
|
|
rawConfirmation = null;
|
|
}
|
|
const confirmation = parseVoidResult(rawConfirmation);
|
|
if (!confirmation) {
|
|
const mismatch = recoveryFailure(
|
|
"PROTOCOL_MISMATCH",
|
|
);
|
|
settleAttemptGate(mismatch);
|
|
safelyClose(active);
|
|
phase.controller.abort();
|
|
phase.dispose();
|
|
return mismatch;
|
|
}
|
|
|
|
const afterConfirmation = currentFailure();
|
|
if (afterConfirmation) {
|
|
settleAttemptGate(
|
|
recoveryFailure(afterConfirmation),
|
|
);
|
|
safelyClose(active);
|
|
phase.controller.abort();
|
|
phase.dispose();
|
|
return failure(afterConfirmation);
|
|
}
|
|
if (!confirmation.ok) {
|
|
settleAttemptGate(confirmation.failure.result);
|
|
safelyClose(active);
|
|
phase.controller.abort();
|
|
phase.dispose();
|
|
return confirmation.failure.result;
|
|
}
|
|
|
|
settleAttemptGate(
|
|
confirmation.result as RealtimeResult<void>,
|
|
);
|
|
pendingRecovery = null;
|
|
} else {
|
|
settleAttemptGate(noPendingRecovery);
|
|
if (opened.value.hasRecoveryBarrier) {
|
|
safelyClose(active);
|
|
phase.controller.abort();
|
|
phase.dispose();
|
|
return noPendingRecovery;
|
|
}
|
|
}
|
|
|
|
const openedAt = safeNow(clock);
|
|
if (
|
|
openedAt === null ||
|
|
reconnectBudgetRemaining(
|
|
policy,
|
|
budgetStartedAt,
|
|
openedAt,
|
|
) <= 0
|
|
) {
|
|
safelyClose(active);
|
|
phase.controller.abort();
|
|
phase.dispose();
|
|
return (
|
|
retry?.terminalResult ??
|
|
failure("PROVIDER_UNAVAILABLE")
|
|
);
|
|
}
|
|
retry = null;
|
|
|
|
session = active;
|
|
const waiting = track(() => active.waitClosed());
|
|
const closed = await race(
|
|
waiting,
|
|
phase.controller.signal,
|
|
);
|
|
const closeOffline = wentOffline(phase);
|
|
|
|
const afterWaitClosed = currentFailure();
|
|
if (afterWaitClosed) {
|
|
safelyClose(active);
|
|
session = null;
|
|
phase.controller.abort();
|
|
phase.dispose();
|
|
return failure(afterWaitClosed);
|
|
}
|
|
if (closed.kind === "INTERRUPTED") {
|
|
safelyClose(active);
|
|
if (closeOffline) {
|
|
const drained = await drainWithinCeiling(
|
|
waiting,
|
|
controller.signal,
|
|
);
|
|
session = null;
|
|
phase.dispose();
|
|
const afterDrain = currentFailure();
|
|
if (drained.kind === "INTERRUPTED" || afterDrain) {
|
|
return failure(afterDrain ?? "ABORTED");
|
|
}
|
|
if (drained.kind === "TIMED_OUT") {
|
|
return failure("PROVIDER_UNAVAILABLE");
|
|
}
|
|
const drainedAt = safeNow(clock);
|
|
if (drainedAt === null) {
|
|
return failure("PROVIDER_UNAVAILABLE");
|
|
}
|
|
retry = offlineRetry(
|
|
drainedAt,
|
|
"RECEIVE",
|
|
);
|
|
continue;
|
|
}
|
|
session = null;
|
|
phase.dispose();
|
|
return failure(currentFailure() ?? "ABORTED");
|
|
}
|
|
|
|
if (closeOffline) {
|
|
safelyClose(active);
|
|
session = null;
|
|
phase.controller.abort();
|
|
phase.dispose();
|
|
retry = offlineRetry(
|
|
safeNow(clock) ?? openedAt,
|
|
"RECEIVE",
|
|
);
|
|
continue;
|
|
}
|
|
if (closed.kind === "THREW") {
|
|
safelyClose(active);
|
|
session = null;
|
|
phase.controller.abort();
|
|
phase.dispose();
|
|
retry = providerRetry(
|
|
safeNow(clock) ?? openedAt,
|
|
"RECEIVE",
|
|
);
|
|
continue;
|
|
}
|
|
|
|
let rawClassification: unknown;
|
|
try {
|
|
rawClassification = active.classifyClosed(closed.value);
|
|
} catch {
|
|
rawClassification = null;
|
|
}
|
|
const afterClassification = currentFailure();
|
|
safelyClose(active);
|
|
session = null;
|
|
phase.controller.abort();
|
|
phase.dispose();
|
|
if (afterClassification) {
|
|
return failure(afterClassification);
|
|
}
|
|
|
|
const classified = parseCloseClassification(
|
|
rawClassification,
|
|
);
|
|
if (!classified) return failure("PROTOCOL_MISMATCH");
|
|
const afterClassificationValidation = currentFailure();
|
|
if (afterClassificationValidation) {
|
|
return failure(afterClassificationValidation);
|
|
}
|
|
|
|
if (classified.kind === "OUTCOME") {
|
|
if (classified.outcome.ok) {
|
|
return classified.outcome.result as RealtimeResult<void>;
|
|
}
|
|
if (
|
|
!reconnectable(
|
|
classified.outcome.failure,
|
|
classified.outcome.serverNotBeforeMs,
|
|
)
|
|
) {
|
|
return classified.outcome.failure.result;
|
|
}
|
|
|
|
const closedAt = safeNow(clock);
|
|
if (closedAt === null) {
|
|
return classified.outcome.failure.result;
|
|
}
|
|
if (
|
|
isReconnectAttemptResetEligible({
|
|
policy,
|
|
openedAtMs: openedAt,
|
|
nowMs: closedAt,
|
|
observedValidHeartbeatOrEvent:
|
|
validSignalObserved,
|
|
})
|
|
) {
|
|
retriesUsed = 0;
|
|
budgetStartedAt = closedAt;
|
|
}
|
|
retry = captureRetry(
|
|
classified.outcome.failure.result,
|
|
classified.outcome.failure.result,
|
|
closedAt,
|
|
classified.outcome.serverNotBeforeMs,
|
|
);
|
|
if (
|
|
classified.outcome.failure.kind === "OFFLINE"
|
|
) {
|
|
requireOnline();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const closedAt = safeNow(clock);
|
|
if (closedAt === null) {
|
|
return classified.directive.terminalFailure.result;
|
|
}
|
|
if (
|
|
isReconnectAttemptResetEligible({
|
|
policy,
|
|
openedAtMs: openedAt,
|
|
nowMs: closedAt,
|
|
observedValidHeartbeatOrEvent: validSignalObserved,
|
|
})
|
|
) {
|
|
retriesUsed = 0;
|
|
budgetStartedAt = closedAt;
|
|
}
|
|
pendingRecovery = classified.directive;
|
|
retry = captureRetry(
|
|
classified.directive.terminalFailure.result,
|
|
classified.directive.terminalFailure.result,
|
|
closedAt,
|
|
classified.directive.serverNotBeforeMs,
|
|
);
|
|
}
|
|
} finally {
|
|
safelyRemoveAbortListener(inputSignal, onAbort);
|
|
safelyUnsubscribe(unsubscribe);
|
|
controller.abort();
|
|
phaseController?.abort();
|
|
safelyClose(session);
|
|
session = null;
|
|
activeRecoveryGate?.settle(
|
|
recoveryFailure(currentFailure() ?? "CLOSED"),
|
|
);
|
|
activeRecoveryGate = null;
|
|
safelyWake(wakeOnline);
|
|
if (runController === controller) runController = null;
|
|
if (generation === runGeneration) {
|
|
state = activeTask === null ? "IDLE" : "DRAINING";
|
|
}
|
|
}
|
|
}
|
|
|
|
function close(): void {
|
|
if (state === "CLOSED") return;
|
|
state = "CLOSED";
|
|
generation += 1;
|
|
activeRecoveryGate?.settle(recoveryFailure("CLOSED"));
|
|
activeRecoveryGate = null;
|
|
runController?.abort();
|
|
phaseController?.abort();
|
|
safelyClose(session);
|
|
}
|
|
|
|
return Object.freeze({
|
|
run,
|
|
getState: () => state,
|
|
close,
|
|
});
|
|
}
|
|
|
|
async function race<Value>(
|
|
task: Promise<Settled<Value>>,
|
|
signal: AbortSignal,
|
|
): Promise<AwaitedTask<Value>> {
|
|
if (signal.aborted) return Object.freeze({ kind: "INTERRUPTED" });
|
|
let cleanup: () => void = () => undefined;
|
|
const interrupted = new Promise<
|
|
Readonly<{ kind: "INTERRUPTED" }>
|
|
>((resolve) => {
|
|
const abort = () =>
|
|
resolve(Object.freeze({ kind: "INTERRUPTED" }));
|
|
signal.addEventListener("abort", abort, { once: true });
|
|
cleanup = () => signal.removeEventListener("abort", abort);
|
|
if (signal.aborted) abort();
|
|
});
|
|
try {
|
|
return await Promise.race([task, interrupted]);
|
|
} finally {
|
|
cleanup();
|
|
}
|
|
}
|
|
|
|
async function drainWithinCeiling<Value>(
|
|
task: Promise<Settled<Value>>,
|
|
signal: AbortSignal,
|
|
): Promise<DrainedTask<Value>> {
|
|
if (signal.aborted) return Object.freeze({ kind: "INTERRUPTED" });
|
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
let cleanup: () => void = () => undefined;
|
|
const boundary = new Promise<
|
|
| Readonly<{ kind: "INTERRUPTED" }>
|
|
| Readonly<{ kind: "TIMED_OUT" }>
|
|
>((resolve) => {
|
|
const abort = () =>
|
|
resolve(Object.freeze({ kind: "INTERRUPTED" }));
|
|
cleanup = () => safelyRemoveAbortListener(signal, abort);
|
|
try {
|
|
signal.addEventListener("abort", abort, { once: true });
|
|
timer = setTimeout(
|
|
() => resolve(Object.freeze({ kind: "TIMED_OUT" })),
|
|
REALTIME_RECONNECT_CEILINGS.drainTimeoutMs,
|
|
);
|
|
} catch {
|
|
resolve(Object.freeze({ kind: "TIMED_OUT" }));
|
|
}
|
|
if (signal.aborted) abort();
|
|
});
|
|
try {
|
|
return await Promise.race([task, boundary]);
|
|
} finally {
|
|
cleanup();
|
|
if (timer !== undefined) {
|
|
try {
|
|
clearTimeout(timer);
|
|
} catch {
|
|
// The generation remains fenced if host timer cleanup fails.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function createBarrierGate(): BarrierGate {
|
|
let settled = false;
|
|
let resolveGate:
|
|
| ((result: RealtimeResult<void>) => void)
|
|
| undefined;
|
|
const promise = new Promise<RealtimeResult<void>>((resolve) => {
|
|
resolveGate = resolve;
|
|
});
|
|
return Object.freeze({
|
|
promise,
|
|
settle(result) {
|
|
if (settled) return;
|
|
settled = true;
|
|
resolveGate?.(result);
|
|
resolveGate = undefined;
|
|
},
|
|
});
|
|
}
|
|
|
|
function parseRunInput(value: unknown): ParsedRunInput | null {
|
|
const snapshot = captureDataSnapshot(value);
|
|
if (
|
|
!snapshot ||
|
|
!hasOneExactKeySet(snapshot, [
|
|
[],
|
|
["signal"],
|
|
["initialRecovery"],
|
|
["initialRecovery", "signal"],
|
|
])
|
|
) {
|
|
return null;
|
|
}
|
|
const signalValue = snapshot.values.signal;
|
|
if (
|
|
signalValue !== undefined &&
|
|
(signalValue === null ||
|
|
(typeof signalValue !== "object" &&
|
|
typeof signalValue !== "function"))
|
|
) {
|
|
return null;
|
|
}
|
|
const initialValue = snapshot.values.initialRecovery;
|
|
const initialRecovery =
|
|
initialValue === undefined
|
|
? null
|
|
: parseRecoveryDirectiveSnapshot(
|
|
captureDataSnapshot(initialValue),
|
|
);
|
|
if (initialValue !== undefined && !initialRecovery) return null;
|
|
return Object.freeze({
|
|
signal: signalValue as AbortSignal | undefined,
|
|
initialRecovery,
|
|
});
|
|
}
|
|
|
|
function parseAttemptOutcome<ClosedReceipt>(
|
|
value: unknown,
|
|
): ParsedOutcome<ParsedAttemptSuccess<ClosedReceipt>> | null {
|
|
return parseOutcomeSnapshot(
|
|
captureDataSnapshot(value),
|
|
(candidate) =>
|
|
parseAttemptSuccess<ClosedReceipt>(
|
|
captureDataSnapshot(candidate),
|
|
),
|
|
);
|
|
}
|
|
|
|
function parseAttemptSuccess<ClosedReceipt>(
|
|
snapshot: DataSnapshot | null,
|
|
): ParsedValue<ParsedAttemptSuccess<ClosedReceipt>> | null {
|
|
if (
|
|
!snapshot ||
|
|
!hasOneExactKeySet(snapshot, [
|
|
["session"],
|
|
["establishedRecoveryBarrier", "session"],
|
|
])
|
|
) {
|
|
return null;
|
|
}
|
|
const session = parseSession<ClosedReceipt>(
|
|
captureDataSnapshot(snapshot.values.session),
|
|
);
|
|
if (!session) return null;
|
|
const hasRecoveryBarrier = snapshot.keys.includes(
|
|
"establishedRecoveryBarrier",
|
|
);
|
|
return Object.freeze({
|
|
value: Object.freeze({
|
|
session,
|
|
hasRecoveryBarrier,
|
|
establishedRecoveryBarrier:
|
|
snapshot.values.establishedRecoveryBarrier,
|
|
}),
|
|
});
|
|
}
|
|
|
|
function parseSession<ClosedReceipt>(
|
|
snapshot: DataSnapshot | null,
|
|
): RealtimeReconnectSession<ClosedReceipt> | null {
|
|
if (
|
|
!snapshot ||
|
|
!hasExactKeys(snapshot, [
|
|
"classifyClosed",
|
|
"close",
|
|
"waitClosed",
|
|
])
|
|
) {
|
|
return null;
|
|
}
|
|
const waitClosed = snapshot.values.waitClosed;
|
|
const classifyClosed = snapshot.values.classifyClosed;
|
|
const close = snapshot.values.close;
|
|
if (
|
|
typeof waitClosed !== "function" ||
|
|
typeof classifyClosed !== "function" ||
|
|
typeof close !== "function"
|
|
) {
|
|
return null;
|
|
}
|
|
return Object.freeze({
|
|
waitClosed: () =>
|
|
Reflect.apply(waitClosed, snapshot.source, []) as
|
|
| ClosedReceipt
|
|
| Promise<ClosedReceipt>,
|
|
classifyClosed: (receipt: ClosedReceipt) =>
|
|
Reflect.apply(classifyClosed, snapshot.source, [
|
|
receipt,
|
|
]) as RealtimeReconnectCloseClassification,
|
|
close: () => {
|
|
Reflect.apply(close, snapshot.source, []);
|
|
},
|
|
});
|
|
}
|
|
|
|
function parseCloseClassification(
|
|
value: unknown,
|
|
): ParsedCloseClassification | null {
|
|
const snapshot = captureDataSnapshot(value);
|
|
if (!snapshot) return null;
|
|
if (snapshot.values.kind === "RECOVERY_RECONNECT") {
|
|
const directive = parseRecoveryDirectiveSnapshot(snapshot);
|
|
return directive
|
|
? Object.freeze({
|
|
kind: "RECOVERY_RECONNECT",
|
|
directive,
|
|
})
|
|
: null;
|
|
}
|
|
const outcome = parseOutcomeSnapshot(
|
|
snapshot,
|
|
(candidate) =>
|
|
candidate === undefined
|
|
? Object.freeze({ value: undefined })
|
|
: null,
|
|
);
|
|
return outcome
|
|
? Object.freeze({ kind: "OUTCOME", outcome })
|
|
: null;
|
|
}
|
|
|
|
function parseRecoveryDirectiveSnapshot(
|
|
snapshot: DataSnapshot | null,
|
|
): ParsedRecoveryDirective | null {
|
|
if (
|
|
!snapshot ||
|
|
!hasOneExactKeySet(snapshot, [
|
|
["kind", "recovery", "terminalResult"],
|
|
[
|
|
"kind",
|
|
"recovery",
|
|
"serverNotBeforeMs",
|
|
"terminalResult",
|
|
],
|
|
]) ||
|
|
snapshot.values.kind !== "RECOVERY_RECONNECT"
|
|
) {
|
|
return null;
|
|
}
|
|
const recovery = parseCommittedRecovery(
|
|
captureDataSnapshot(snapshot.values.recovery),
|
|
);
|
|
const terminalFailure = parseFailureResult(
|
|
snapshot.values.terminalResult,
|
|
);
|
|
const serverNotBeforeMs = parseNotBefore(
|
|
snapshot.values.serverNotBeforeMs,
|
|
);
|
|
if (
|
|
!recovery ||
|
|
!terminalFailure ||
|
|
serverNotBeforeMs === undefined
|
|
) {
|
|
return null;
|
|
}
|
|
return Object.freeze({
|
|
recovery,
|
|
terminalFailure,
|
|
serverNotBeforeMs,
|
|
});
|
|
}
|
|
|
|
function parseCommittedRecovery(
|
|
snapshot: DataSnapshot | null,
|
|
): RealtimeCommittedRecovery | null {
|
|
if (
|
|
!snapshot ||
|
|
!hasExactKeys(snapshot, [
|
|
"checkpoint",
|
|
"kind",
|
|
"streamId",
|
|
]) ||
|
|
snapshot.values.kind !== "RECOVERY_COMMITTED" ||
|
|
!safeFrozen(snapshot.source)
|
|
) {
|
|
return null;
|
|
}
|
|
const checkpoint = captureDataSnapshot(
|
|
snapshot.values.checkpoint,
|
|
);
|
|
if (
|
|
!checkpoint ||
|
|
!hasExactKeys(checkpoint, [
|
|
"lastAppliedSequence",
|
|
"recoveryMode",
|
|
"resumeCursor",
|
|
"streamEpoch",
|
|
]) ||
|
|
!safeFrozen(checkpoint.source)
|
|
) {
|
|
return null;
|
|
}
|
|
const canonicalCheckpoint = Object.freeze({
|
|
lastAppliedSequence:
|
|
checkpoint.values.lastAppliedSequence,
|
|
recoveryMode: checkpoint.values.recoveryMode,
|
|
resumeCursor: checkpoint.values.resumeCursor,
|
|
streamEpoch: checkpoint.values.streamEpoch,
|
|
});
|
|
const canonicalRecovery = Object.freeze({
|
|
checkpoint: canonicalCheckpoint,
|
|
kind: snapshot.values.kind,
|
|
streamId: snapshot.values.streamId,
|
|
});
|
|
if (
|
|
!isRealtimeTransportEventOutcome(canonicalRecovery) ||
|
|
canonicalRecovery.kind !== "RECOVERY_COMMITTED"
|
|
) {
|
|
return null;
|
|
}
|
|
return snapshot.source as RealtimeCommittedRecovery;
|
|
}
|
|
|
|
function parseVoidResult(
|
|
value: unknown,
|
|
): ParsedResult<void> | null {
|
|
return parseResult(value, (candidate) =>
|
|
candidate === undefined
|
|
? Object.freeze({ value: undefined })
|
|
: null,
|
|
);
|
|
}
|
|
|
|
function parseOutcomeSnapshot<Value>(
|
|
snapshot: DataSnapshot | null,
|
|
parseValue: (candidate: unknown) => ParsedValue<Value> | null,
|
|
): ParsedOutcome<Value> | null {
|
|
if (
|
|
!snapshot ||
|
|
!hasOneExactKeySet(snapshot, [
|
|
["result"],
|
|
["result", "serverNotBeforeMs"],
|
|
])
|
|
) {
|
|
return null;
|
|
}
|
|
const result = parseResult(snapshot.values.result, parseValue);
|
|
if (!result) return null;
|
|
if (result.ok) {
|
|
return hasExactKeys(snapshot, ["result"])
|
|
? Object.freeze({
|
|
ok: true,
|
|
result: result.result,
|
|
value: result.value,
|
|
})
|
|
: null;
|
|
}
|
|
const serverNotBeforeMs = parseNotBefore(
|
|
snapshot.values.serverNotBeforeMs,
|
|
);
|
|
if (serverNotBeforeMs === undefined) return null;
|
|
return Object.freeze({
|
|
ok: false,
|
|
failure: result.failure,
|
|
serverNotBeforeMs,
|
|
});
|
|
}
|
|
|
|
function parseResult<Value>(
|
|
value: unknown,
|
|
parseValue: (candidate: unknown) => ParsedValue<Value> | null,
|
|
): ParsedResult<Value> | null {
|
|
const snapshot = captureDataSnapshot(value);
|
|
if (!snapshot || !safeFrozen(snapshot.source)) return null;
|
|
|
|
if (snapshot.values.ok === true) {
|
|
if (!hasExactKeys(snapshot, ["ok", "value"])) return null;
|
|
const parsedValue = parseValue(snapshot.values.value);
|
|
const canonical = Object.freeze({
|
|
ok: true,
|
|
value: snapshot.values.value,
|
|
});
|
|
if (
|
|
!parsedValue ||
|
|
!isRealtimeResult(
|
|
canonical,
|
|
(candidate: unknown): candidate is unknown => true,
|
|
)
|
|
) {
|
|
return null;
|
|
}
|
|
return Object.freeze({
|
|
ok: true,
|
|
result: snapshot.source as SuccessResult<unknown>,
|
|
value: parsedValue.value,
|
|
});
|
|
}
|
|
|
|
const failure = parseFailureResultSnapshot(snapshot);
|
|
return failure
|
|
? Object.freeze({ ok: false, failure })
|
|
: null;
|
|
}
|
|
|
|
function parseFailureResult(
|
|
value: unknown,
|
|
): FailureSnapshot | null {
|
|
return parseFailureResultSnapshot(captureDataSnapshot(value));
|
|
}
|
|
|
|
function parseFailureResultSnapshot(
|
|
snapshot: DataSnapshot | null,
|
|
): FailureSnapshot | null {
|
|
if (
|
|
!snapshot ||
|
|
!hasExactKeys(snapshot, ["error", "ok"]) ||
|
|
snapshot.values.ok !== false ||
|
|
!safeFrozen(snapshot.source)
|
|
) {
|
|
return null;
|
|
}
|
|
const error = captureDataSnapshot(snapshot.values.error);
|
|
if (
|
|
!error ||
|
|
!hasExactKeys(error, [
|
|
"kind",
|
|
"operation",
|
|
"retryable",
|
|
]) ||
|
|
!safeFrozen(error.source)
|
|
) {
|
|
return null;
|
|
}
|
|
const canonicalError = Object.freeze({
|
|
kind: error.values.kind,
|
|
operation: error.values.operation,
|
|
retryable: error.values.retryable,
|
|
});
|
|
const canonical = Object.freeze({
|
|
error: canonicalError,
|
|
ok: false,
|
|
});
|
|
if (
|
|
!isRealtimeResult(
|
|
canonical,
|
|
(candidate: unknown): candidate is never => false,
|
|
)
|
|
) {
|
|
return null;
|
|
}
|
|
const validated =
|
|
canonical as Extract<RealtimeResult<never>, { ok: false }>;
|
|
return Object.freeze({
|
|
result: snapshot.source as FailureResult,
|
|
kind: validated.error.kind,
|
|
retryable: validated.error.retryable,
|
|
});
|
|
}
|
|
|
|
function captureDataSnapshot(value: unknown): DataSnapshot | null {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return null;
|
|
}
|
|
try {
|
|
const prototype = Object.getPrototypeOf(value);
|
|
if (
|
|
prototype !== Object.prototype &&
|
|
prototype !== null
|
|
) {
|
|
return null;
|
|
}
|
|
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
const ownKeys = Reflect.ownKeys(descriptors);
|
|
if (ownKeys.some((key) => typeof key !== "string")) {
|
|
return null;
|
|
}
|
|
const keys = (ownKeys as string[]).sort();
|
|
const values = Object.create(null) as Record<string, unknown>;
|
|
for (const key of keys) {
|
|
const descriptor = descriptors[key];
|
|
if (
|
|
!descriptor ||
|
|
!("value" in descriptor) ||
|
|
descriptor.enumerable !== true
|
|
) {
|
|
return null;
|
|
}
|
|
Object.defineProperty(values, key, {
|
|
configurable: false,
|
|
enumerable: true,
|
|
value: descriptor.value,
|
|
writable: false,
|
|
});
|
|
}
|
|
return Object.freeze({
|
|
source: value,
|
|
keys: Object.freeze(keys),
|
|
values: Object.freeze(values),
|
|
});
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function hasOneExactKeySet(
|
|
snapshot: DataSnapshot,
|
|
expected: readonly (readonly string[])[],
|
|
): boolean {
|
|
return expected.some((keys) => hasExactKeys(snapshot, keys));
|
|
}
|
|
|
|
function hasExactKeys(
|
|
snapshot: DataSnapshot,
|
|
expected: readonly string[],
|
|
): boolean {
|
|
const sorted = [...expected].sort();
|
|
return (
|
|
snapshot.keys.length === sorted.length &&
|
|
snapshot.keys.every((key, index) => key === sorted[index])
|
|
);
|
|
}
|
|
|
|
function parseNotBefore(
|
|
value: unknown,
|
|
): number | null | undefined {
|
|
if (value === undefined || value === null) return null;
|
|
return typeof value === "number" &&
|
|
Number.isSafeInteger(value) &&
|
|
value >= 0
|
|
? value
|
|
: undefined;
|
|
}
|
|
|
|
function captureRetry(
|
|
result: FailureResult,
|
|
terminalResult: FailureResult,
|
|
at: number,
|
|
serverNotBeforeMs: number | null = null,
|
|
): Retry {
|
|
return Object.freeze({
|
|
result,
|
|
terminalResult,
|
|
at,
|
|
notBeforeMs: serverNotBeforeMs,
|
|
});
|
|
}
|
|
|
|
function canRetry(
|
|
retry: Retry,
|
|
attempts: number,
|
|
startedAt: number,
|
|
now: number,
|
|
policy: ReconnectPolicy,
|
|
): boolean {
|
|
if (attempts >= policy.maxAttempts) return false;
|
|
const remaining = reconnectBudgetRemaining(
|
|
policy,
|
|
startedAt,
|
|
now,
|
|
);
|
|
const serverDelay = remainingServerDelay(retry, now);
|
|
return (
|
|
remaining > 0 &&
|
|
serverDelay !== null &&
|
|
serverDelay <= policy.maxDelayMs &&
|
|
serverDelay < remaining
|
|
);
|
|
}
|
|
|
|
function nextDelay(
|
|
retry: Retry,
|
|
attempts: number,
|
|
startedAt: number,
|
|
now: number,
|
|
policy: ReconnectPolicy,
|
|
random: () => number,
|
|
): number | null {
|
|
const serverNotBeforeMs = remainingServerDelay(retry, now);
|
|
if (serverNotBeforeMs === null) return null;
|
|
return calculateReconnectDelay({
|
|
policy,
|
|
attemptIndex: attempts,
|
|
remainingElapsedMs: reconnectBudgetRemaining(
|
|
policy,
|
|
startedAt,
|
|
now,
|
|
),
|
|
random,
|
|
serverNotBeforeMs,
|
|
});
|
|
}
|
|
|
|
function remainingServerDelay(
|
|
retry: Retry,
|
|
now: number,
|
|
): number | null {
|
|
if (!Number.isFinite(now) || now < retry.at) return null;
|
|
return retry.notBeforeMs === null
|
|
? 0
|
|
: Math.max(
|
|
0,
|
|
Math.ceil(retry.notBeforeMs - (now - retry.at)),
|
|
);
|
|
}
|
|
|
|
function offlineRetry(
|
|
at: number,
|
|
operation: "CONNECT" | "RECEIVE",
|
|
terminalResult?: FailureResult,
|
|
): Retry {
|
|
const result = realtimeFailure("OFFLINE", operation, true);
|
|
return captureRetry(
|
|
result,
|
|
terminalResult ?? result,
|
|
at,
|
|
);
|
|
}
|
|
|
|
function providerRetry(
|
|
at: number,
|
|
operation: "CONNECT" | "RECEIVE",
|
|
terminalResult?: FailureResult,
|
|
): Retry {
|
|
const result = realtimeFailure(
|
|
"PROVIDER_UNAVAILABLE",
|
|
operation,
|
|
true,
|
|
);
|
|
return captureRetry(
|
|
result,
|
|
terminalResult ?? result,
|
|
at,
|
|
);
|
|
}
|
|
|
|
function reconnectable(
|
|
failureSnapshot: FailureSnapshot,
|
|
serverNotBeforeMs: number | null,
|
|
): boolean {
|
|
if (
|
|
serverNotBeforeMs === null &&
|
|
(failureSnapshot.kind === "RATE_LIMITED" ||
|
|
failureSnapshot.kind === "PROVIDER_UNAVAILABLE")
|
|
) {
|
|
return false;
|
|
}
|
|
return (
|
|
failureSnapshot.retryable &&
|
|
RETRYABLE_KINDS.includes(
|
|
failureSnapshot.kind as (typeof RETRYABLE_KINDS)[number],
|
|
)
|
|
);
|
|
}
|
|
|
|
function closeLateSession(
|
|
outcome: Settled<unknown>,
|
|
): void {
|
|
if (outcome.kind !== "VALUE") return;
|
|
const parsed = parseAttemptOutcome<unknown>(outcome.value);
|
|
if (parsed?.ok) safelyClose(parsed.value.session);
|
|
}
|
|
|
|
function safeNow(clock: ClockPort): number | null {
|
|
try {
|
|
const value = clock.now();
|
|
return Number.isFinite(value) ? value : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function safeOnline(
|
|
environment: RealtimeReconnectEnvironment,
|
|
): boolean | null {
|
|
try {
|
|
const value = environment.online();
|
|
return typeof value === "boolean" ? value : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function safeCurrent(check: () => boolean): boolean {
|
|
try {
|
|
return check() === true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function safeSignalAborted(
|
|
signal: AbortSignal | undefined,
|
|
): boolean | null {
|
|
if (!signal) return false;
|
|
try {
|
|
const aborted = signal.aborted;
|
|
return typeof aborted === "boolean" ? aborted : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function safeFrozen(value: object): boolean {
|
|
try {
|
|
return Object.isFrozen(value);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function safelyAddAbortListener(
|
|
signal: AbortSignal | undefined,
|
|
listener: () => void,
|
|
): boolean {
|
|
if (!signal) return true;
|
|
try {
|
|
signal.addEventListener("abort", listener, { once: true });
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function safelyRemoveAbortListener(
|
|
signal: AbortSignal | undefined,
|
|
listener: () => void,
|
|
): void {
|
|
try {
|
|
signal?.removeEventListener("abort", listener);
|
|
} catch {
|
|
// Cleanup is best effort for a hostile signal implementation.
|
|
}
|
|
}
|
|
|
|
function safelyClose<ClosedReceipt>(
|
|
active: RealtimeReconnectSession<ClosedReceipt> | null,
|
|
): void {
|
|
try {
|
|
active?.close();
|
|
} catch {
|
|
// The generation is fenced even if host cleanup throws.
|
|
}
|
|
}
|
|
|
|
function safelyUnsubscribe(
|
|
unsubscribe: (() => void) | undefined,
|
|
): void {
|
|
try {
|
|
unsubscribe?.();
|
|
} catch {
|
|
// Cleanup cannot reopen a terminal run.
|
|
}
|
|
}
|
|
|
|
function safelyWake(wake: (() => void) | null): void {
|
|
try {
|
|
wake?.();
|
|
} catch {
|
|
// Wake-up remains best effort during terminal cleanup.
|
|
}
|
|
}
|
|
|
|
function recoveryFailure(
|
|
kind: RealtimeFailureKind,
|
|
): FailureResult {
|
|
return realtimeFailure(kind, "RECOVER");
|
|
}
|
|
|
|
function failure(kind: RealtimeFailureKind): FailureResult {
|
|
return realtimeFailure(kind, "CONNECT", false);
|
|
}
|