161 lines
4.2 KiB
TypeScript
161 lines
4.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
calculateReconnectDelay,
|
|
defineReconnectPolicy,
|
|
isReconnectAttemptResetEligible,
|
|
parseRetryAfterDelay,
|
|
REALTIME_RECONNECT_CEILINGS,
|
|
reconnectBudgetRemaining,
|
|
type ReconnectPolicy,
|
|
} from "../../../src/adapters/realtime/reconnect-policy.ts";
|
|
|
|
const policy = defineReconnectPolicy({
|
|
baseDelayMs: 1_000,
|
|
maxDelayMs: 60_000,
|
|
maxAttempts: 10,
|
|
maxElapsedMs: 300_000,
|
|
stableOpenMs: 30_000,
|
|
});
|
|
|
|
describe("realtime reconnect policy", () => {
|
|
it("uses full jitter and treats a server hint as a not-before floor", () => {
|
|
expect(
|
|
calculateReconnectDelay({
|
|
policy,
|
|
attemptIndex: 2,
|
|
remainingElapsedMs: 10_000,
|
|
random: () => 0.5,
|
|
}),
|
|
).toBe(2_000);
|
|
expect(
|
|
calculateReconnectDelay({
|
|
policy,
|
|
attemptIndex: 2,
|
|
remainingElapsedMs: 10_000,
|
|
random: () => 0.5,
|
|
serverNotBeforeMs: 3_000,
|
|
}),
|
|
).toBe(3_000);
|
|
});
|
|
|
|
it("stops instead of clamping a hint past a hard or remaining budget", () => {
|
|
expect(
|
|
calculateReconnectDelay({
|
|
policy,
|
|
attemptIndex: 0,
|
|
remainingElapsedMs: 100_000,
|
|
random: () => 0,
|
|
serverNotBeforeMs: 60_001,
|
|
}),
|
|
).toBeNull();
|
|
expect(
|
|
calculateReconnectDelay({
|
|
policy,
|
|
attemptIndex: 0,
|
|
remainingElapsedMs: 3_000,
|
|
random: () => 0,
|
|
serverNotBeforeMs: 3_000,
|
|
}),
|
|
).toBeNull();
|
|
expect(
|
|
calculateReconnectDelay({
|
|
policy,
|
|
attemptIndex: 10,
|
|
remainingElapsedMs: 100_000,
|
|
random: () => 0,
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("parses only bounded-shape Retry-After syntax for caller validation", () => {
|
|
expect(parseRetryAfterDelay("12", 0)).toBe(12_000);
|
|
expect(
|
|
parseRetryAfterDelay(
|
|
"Thu, 01 Jan 1970 00:00:20 GMT",
|
|
5_000,
|
|
),
|
|
).toBe(15_000);
|
|
expect(parseRetryAfterDelay("1.5", 0)).toBeNull();
|
|
expect(parseRetryAfterDelay("-1", 0)).toBeNull();
|
|
});
|
|
|
|
it("resets attempts only after stable open or a valid signal", () => {
|
|
expect(
|
|
isReconnectAttemptResetEligible({
|
|
policy,
|
|
openedAtMs: 10,
|
|
nowMs: 29_000,
|
|
observedValidHeartbeatOrEvent: false,
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
isReconnectAttemptResetEligible({
|
|
policy,
|
|
openedAtMs: 10,
|
|
nowMs: 30_010,
|
|
observedValidHeartbeatOrEvent: false,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isReconnectAttemptResetEligible({
|
|
policy,
|
|
openedAtMs: 10,
|
|
nowMs: 11,
|
|
observedValidHeartbeatOrEvent: true,
|
|
}),
|
|
).toBe(true);
|
|
expect(reconnectBudgetRemaining(policy, 1_000, 2_000)).toBe(
|
|
299_000,
|
|
);
|
|
expect(reconnectBudgetRemaining(policy, 2_000, 1_000)).toBe(0);
|
|
});
|
|
|
|
it("rejects policies above the implementation ceiling", () => {
|
|
expect(() =>
|
|
defineReconnectPolicy({
|
|
...policy,
|
|
maxAttempts: 11,
|
|
}),
|
|
).toThrow(TypeError);
|
|
});
|
|
|
|
it("keeps aborted-task drain below the absolute implementation ceiling", () => {
|
|
expect(REALTIME_RECONNECT_CEILINGS.drainTimeoutMs).toBe(2_000);
|
|
expect(
|
|
REALTIME_RECONNECT_CEILINGS.drainTimeoutMs,
|
|
).toBeLessThanOrEqual(
|
|
REALTIME_RECONNECT_CEILINGS.maxDrainTimeoutMs,
|
|
);
|
|
expect(
|
|
REALTIME_RECONNECT_CEILINGS.maxDrainTimeoutMs,
|
|
).toBe(30_000);
|
|
});
|
|
|
|
it("snapshots only exact own data properties", () => {
|
|
const inherited = Object.create(policy) as ReconnectPolicy;
|
|
const accessor = Object.defineProperties(
|
|
{},
|
|
{
|
|
baseDelayMs: { enumerable: true, value: 1_000 },
|
|
maxDelayMs: {
|
|
enumerable: true,
|
|
get: () => 60_000,
|
|
},
|
|
maxAttempts: { enumerable: true, value: 10 },
|
|
maxElapsedMs: { enumerable: true, value: 300_000 },
|
|
stableOpenMs: { enumerable: true, value: 30_000 },
|
|
},
|
|
) as ReconnectPolicy;
|
|
|
|
expect(() => defineReconnectPolicy(inherited)).toThrow(TypeError);
|
|
expect(() => defineReconnectPolicy(accessor)).toThrow(TypeError);
|
|
expect(() =>
|
|
defineReconnectPolicy({
|
|
...policy,
|
|
extra: true,
|
|
} as ReconnectPolicy),
|
|
).toThrow(TypeError);
|
|
});
|
|
});
|