src/ws/limits.test.ts
v0.3.0 · 8.2 KB
import { describe, expect, it } from 'vitest';
import { createWsIpBudget, WS_LIMITS } from './limits.js';
const user = (n: number) => '0x' + n.toString(16).padStart(40, '0');
describe('WS_LIMITS', () => {
it('documents the knowledge-base numbers', () => {
expect(WS_LIMITS.pingIntervalMs).toBe(30_000);
expect(WS_LIMITS.silenceTimeoutMs).toBe(65_000);
expect(WS_LIMITS.serverIdleTimeoutMs).toBe(60_000);
expect(WS_LIMITS.maxSubscriptionsPerIp).toBe(1000);
expect(WS_LIMITS.maxConnectionsPerIp).toBe(10);
expect(WS_LIMITS.uniqueUsersDocumented).toBe(10);
expect(WS_LIMITS.uniqueUsersMeasured).toBe(20);
expect(WS_LIMITS.ghostSlotMs).toBe(60_000);
});
});
describe('createWsIpBudget: unique users', () => {
it('defaults to the documented 10 users and refuses the 11th before sending', () => {
const b = createWsIpBudget();
for (let i = 1; i <= 10; i++) {
const check = b.checkSubscribe('c', user(i), 0);
expect(check.ok).toBe(true);
b.addSubscription('c', `k${i}`, user(i), 0);
}
expect(b.checkSubscribe('c', user(11), 0)).toEqual({ ok: false, reason: 'maxUniqueUsers', used: 10, limit: 10 });
});
it('counts users per IP across clients (sharding on one IP adds no capacity)', () => {
const b = createWsIpBudget({ maxUniqueUsers: 2 });
b.addSubscription('a', 'k1', user(1), 0);
b.addSubscription('b', 'k2', user(2), 0);
expect(b.checkSubscribe('c', user(3), 0).ok).toBe(false);
});
it('the same user in several subscriptions or clients takes one slot', () => {
const b = createWsIpBudget({ maxUniqueUsers: 1 });
b.addSubscription('a', 'fills', user(1), 0);
expect(b.checkSubscribe('a', user(1), 0).ok).toBe(true);
expect(b.checkSubscribe('b', user(1), 0).ok).toBe(true);
b.addSubscription('b', 'orders', user(1), 0);
expect(b.stats(0).uniqueUsers).toBe(1);
});
it('market subscriptions do not use user slots', () => {
const b = createWsIpBudget({ maxUniqueUsers: 0 });
expect(b.checkSubscribe('a', undefined, 0).ok).toBe(true);
});
it('warns above the documented 10 when the cap is raised toward the measured ~20', () => {
const b = createWsIpBudget({ maxUniqueUsers: 20 });
for (let i = 1; i <= 10; i++) b.addSubscription('c', `k${i}`, user(i), 0);
const check = b.checkSubscribe('c', user(11), 0);
expect(check).toEqual({ ok: true, warnings: [{ kind: 'userLimitWarning', used: 11, limit: 10 }] });
});
it('unsubscribe on the same connection frees the slot immediately', () => {
const b = createWsIpBudget({ maxUniqueUsers: 1 });
b.tryOpenConnection('a', 0);
b.addSubscription('a', 'k1', user(1), 0);
b.removeSubscription('a', 'k1', user(1), 1);
expect(b.checkSubscribe('a', user(2), 2).ok).toBe(true);
expect(b.checkSubscribe('a', user(1), 2)).toEqual({ ok: true, warnings: [] });
});
it('migrating a user to another connection holds a ghost slot ~60 s and warns', () => {
const b = createWsIpBudget({ maxUniqueUsers: 2 });
b.tryOpenConnection('a', 0);
b.tryOpenConnection('b', 0);
b.addSubscription('a', 'k1', user(1), 0);
b.removeSubscription('a', 'k1', user(1), 1_000);
const check = b.checkSubscribe('b', user(1), 2_000);
expect(check.ok).toBe(true);
expect(check.ok && check.warnings).toEqual([{ kind: 'migrationWarning', user: user(1) }]);
b.addSubscription('b', 'k1', user(1), 2_000);
expect(b.stats(2_000)).toMatchObject({ uniqueUsers: 1, ghostSlots: 1, usedUserSlots: 2 });
expect(b.checkSubscribe('b', user(2), 2_000).ok).toBe(false);
// ghost expires 60 s after the release
expect(b.checkSubscribe('b', user(2), 61_001).ok).toBe(true);
});
it('after a reconnect the old connection keeps its users as ghost slots for 60 s', () => {
const b = createWsIpBudget({ maxUniqueUsers: 3 });
b.tryOpenConnection('a', 0);
b.addSubscription('a', 'k1', user(1), 0);
b.addSubscription('a', 'k2', user(2), 0);
b.connectionClosed('a', 10_000);
expect(b.stats(10_000)).toMatchObject({ uniqueUsers: 2, ghostSlots: 2, usedUserSlots: 4, connections: 0 });
expect(b.checkSubscribe('a', user(3), 10_000).ok).toBe(false);
expect(b.checkSubscribe('a', user(3), 70_001).ok).toBe(true);
});
it('connectionClosed is idempotent', () => {
const b = createWsIpBudget();
b.tryOpenConnection('a', 0);
b.addSubscription('a', 'k1', user(1), 0);
b.connectionClosed('a', 0);
b.connectionClosed('a', 0);
expect(b.stats(0).ghostSlots).toBe(1);
});
it('releaseClient drops subscriptions but keeps ghost slots of a connected client', () => {
const b = createWsIpBudget();
b.tryOpenConnection('a', 0);
b.addSubscription('a', 'k1', user(1), 0);
b.releaseClient('a', 0);
expect(b.stats(0)).toMatchObject({ subscriptions: 0, uniqueUsers: 0, ghostSlots: 1 });
expect(b.stats(60_001).ghostSlots).toBe(0);
b.releaseClient('unknown', 0);
});
});
describe('createWsIpBudget: subscriptions and connections', () => {
it('refuses subscriptions above the per-IP cap', () => {
const b = createWsIpBudget({ maxSubscriptions: 2 });
b.addSubscription('a', 'k1', undefined, 0);
b.addSubscription('b', 'k2', undefined, 0);
expect(b.checkSubscribe('a', undefined, 0)).toEqual({ ok: false, reason: 'maxSubscriptions', used: 2, limit: 2 });
b.removeSubscription('a', 'k1', undefined, 0);
expect(b.checkSubscribe('a', undefined, 0).ok).toBe(true);
});
it('refuses connections above the per-IP connection cap', () => {
const b = createWsIpBudget({ maxConnections: 2 });
expect(b.tryOpenConnection('a', 0).ok).toBe(true);
expect(b.tryOpenConnection('b', 0).ok).toBe(true);
expect(b.tryOpenConnection('c', 0)).toEqual({ ok: false, reason: 'maxConnections', retryInMs: 60_000 });
b.connectionClosed('a', 1);
expect(b.tryOpenConnection('c', 1).ok).toBe(true);
});
it('limits new connections per minute (experimental guard)', () => {
const b = createWsIpBudget({ maxNewConnectionsPerMinute: 2 });
expect(b.tryOpenConnection('a', 0).ok).toBe(true);
b.connectionClosed('a', 0);
expect(b.tryOpenConnection('a', 10_000).ok).toBe(true);
b.connectionClosed('a', 10_000);
expect(b.tryOpenConnection('a', 20_000)).toEqual({ ok: false, reason: 'newConnectionsPerMinute', retryInMs: 40_000 });
expect(b.tryOpenConnection('a', 60_000).ok).toBe(true);
});
it('reserves outgoing messages in a sliding minute window (experimental guard)', () => {
const b = createWsIpBudget({ maxMessagesPerMinute: 2 });
expect(b.reserveMessage(0)).toBe(0);
expect(b.reserveMessage(1)).toBe(0);
expect(b.reserveMessage(2)).toBe(59_998);
expect(b.reserveMessage(60_000)).toBe(0);
});
it('rejects invalid limits', () => {
expect(() => createWsIpBudget({ maxUniqueUsers: -1 })).toThrow(RangeError);
expect(() => createWsIpBudget({ maxConnections: Number.NaN })).toThrow(RangeError);
});
});
describe('edge cases added in review', () => {
it('a socket that never opened holds no ghost slots (heldSlots: false)', () => {
const b = createWsIpBudget({ maxUniqueUsers: 1 });
b.tryOpenConnection('a', 0);
b.addSubscription('a', 'k1', user(1), 0);
b.connectionClosed('a', 1_000, false);
expect(b.stats(1_000)).toMatchObject({ connections: 0, ghostSlots: 0, usedUserSlots: 1 });
// The next attempt opens and then closes for real: exactly one ghost, not one per attempt.
b.tryOpenConnection('a', 2_000);
b.connectionClosed('a', 3_000);
b.tryOpenConnection('a', 4_000);
b.connectionClosed('a', 5_000, false);
expect(b.stats(5_000)).toMatchObject({ ghostSlots: 1, usedUserSlots: 2 });
});
it('ghost expiry is exclusive at exactly ghostSlotMs', () => {
const b = createWsIpBudget({ maxUniqueUsers: 1 });
b.tryOpenConnection('a', 0);
b.addSubscription('a', 'k1', user(1), 0);
b.releaseClient('a', 0);
expect(b.checkSubscribe('b', user(2), 59_999).ok).toBe(false);
expect(b.checkSubscribe('b', user(2), 60_000).ok).toBe(true);
});
it('zero caps refuse everything before sending', () => {
const b = createWsIpBudget({ maxSubscriptions: 0, maxConnections: 0 });
expect(b.checkSubscribe('a', undefined, 0)).toMatchObject({ ok: false, reason: 'maxSubscriptions' });
expect(b.tryOpenConnection('a', 0)).toMatchObject({ ok: false, reason: 'maxConnections' });
});
});