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); }); });