Files
core2026/app/game-api/test/realtimeConnectionAdmission.test.ts
T

152 lines
6.3 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { PassThrough } from 'node:stream';
import type { ServerResponse } from 'node:http';
import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken';
import { RealtimeConnectionAdmission } from '../src/realtime/connectionAdmission.js';
import { AuthenticatedRealtimeConnection } from '../src/realtime/authenticatedConnection.js';
afterEach(() => vi.useRealTimers());
describe('realtime connection admission', () => {
it('reserves the process total before authentication and counts live streams too', () => {
const budget = new RealtimeConnectionAdmission({ connections: 2, perUser: 2, opening: 2 });
const first = budget.begin(vi.fn())!;
const second = budget.begin(vi.fn())!;
expect(first.reserveUser('actor')).toBe(true);
expect(first.startStreaming()).toBe(true);
expect(budget.begin(vi.fn())).toBeNull();
second.release();
const replacement = budget.begin(vi.fn())!;
expect(replacement).not.toBeNull();
replacement.release();
first.release();
});
it('shares the authenticated actor limit while allowing another actor', () => {
const budget = new RealtimeConnectionAdmission({ connections: 5, perUser: 1, opening: 5 });
const first = budget.begin(vi.fn())!;
expect(first.reserveUser('one')).toBe(true);
const rejected = budget.begin(vi.fn())!;
expect(rejected.reserveUser('one')).toBe(false);
expect(rejected.startStreaming()).toBe(false);
rejected.release();
const other = budget.begin(vi.fn())!;
expect(other.reserveUser('two')).toBe(true);
first.release();
first.release();
const replacement = budget.begin(vi.fn())!;
expect(replacement.reserveUser('one')).toBe(true);
other.release();
replacement.release();
});
it('keeps timed-out and disconnected backend work admitted until it actually settles', async () => {
vi.useFakeTimers();
const budget = new RealtimeConnectionAdmission({ connections: 5, perUser: 5, opening: 2 });
const timeout = vi.fn();
const stalled = budget.begin(timeout)!;
const disconnected = budget.begin(vi.fn())!;
expect(stalled.reserveUser('stalled')).toBe(true);
disconnected.cancelOpening();
await vi.advanceTimersByTimeAsync(5000);
expect(timeout).toHaveBeenCalledTimes(1);
expect(stalled.canContinue()).toBe(false);
expect(stalled.startStreaming()).toBe(false);
expect(disconnected.canContinue()).toBe(false);
expect(budget.begin(vi.fn())).toBeNull();
stalled.release();
const recovered = budget.begin(vi.fn())!;
expect(recovered.reserveUser('stalled')).toBe(true);
expect(recovered.startStreaming()).toBe(true);
await vi.advanceTimersByTimeAsync(5000);
expect(timeout).toHaveBeenCalledTimes(1);
disconnected.release();
recovered.release();
});
it('stops pending setup on shutdown and permanently refuses new admissions', () => {
const budget = new RealtimeConnectionAdmission();
const timeout = vi.fn();
const pending = budget.begin(timeout)!;
budget.close();
budget.close();
expect(timeout).toHaveBeenCalledTimes(1);
expect(pending.canContinue()).toBe(false);
expect(budget.begin(vi.fn())).toBeNull();
pending.release();
});
it('keeps a closed stream counted while its in-flight authentication is still pending', async () => {
const budget = new RealtimeConnectionAdmission({ connections: 1, perUser: 1, opening: 1 });
const slot = budget.begin(vi.fn())!;
expect(slot.reserveUser('actor')).toBe(true);
expect(slot.startStreaming()).toBe(true);
let finish!: () => void;
let started!: () => void;
const inFlight = new Promise<void>((resolve) => {
finish = resolve;
});
const running = new Promise<void>((resolve) => {
started = resolve;
});
const auth: GameSessionTokenPayload = {
version: 1,
sessionId: 'session',
profile: 'che:default',
issuedAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 60_000).toISOString(),
user: {
id: 'actor',
username: 'actor',
displayName: 'actor',
roles: ['user'],
createdAt: new Date().toISOString(),
},
sanctions: {},
};
const response = new PassThrough();
const connection = new AuthenticatedRealtimeConnection(
auth,
response as unknown as ServerResponse,
async () => {
started();
await inFlight;
return auth;
}
);
connection.onClose(() => {
void connection.whenIdle().then(slot.release, slot.release);
});
connection.enqueue(async () => 'event: ready\ndata: {}\n\n');
await running;
connection.close();
expect(budget.begin(vi.fn())).toBeNull();
finish();
await connection.whenIdle();
const replacement = budget.begin(vi.fn())!;
expect(replacement.reserveUser('actor')).toBe(true);
replacement.release();
});
it('enforces the production 512 total and 32 pending openings without accumulating a queue', () => {
const budget = new RealtimeConnectionAdmission();
const pending = Array.from({ length: 32 }, () => budget.begin(vi.fn())!);
expect(pending.every(Boolean)).toBe(true);
expect(budget.begin(vi.fn())).toBeNull();
for (const slot of pending) slot.release();
const live = Array.from({ length: 512 }, (_, index) => {
const slot = budget.begin(vi.fn())!;
expect(slot.reserveUser(`actor-${index}`)).toBe(true);
expect(slot.startStreaming()).toBe(true);
return slot;
});
expect(budget.begin(vi.fn())).toBeNull();
live[0]!.release();
const replacement = budget.begin(vi.fn())!;
expect(replacement.reserveUser('replacement')).toBe(true);
expect(replacement.startStreaming()).toBe(true);
for (const slot of live) slot.release();
replacement.release();
});
});