Skip to content
markpaper

src/ws/limits.test.ts

v0.3.0 · 8.2 KB

Download file
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' });
  });
});
All files