Skip to content
markpaper

src/ws/dedupe.test.ts

v0.3.0 · 5.2 KB

Download file
import { describe, expect, it } from 'vitest';
import { createTidDeduper, isSnapshotFrame, splitUserFills } from './dedupe.js';
import type { WsFill, WsUserFills } from './types.js';

const USER = '0x' + '0'.repeat(40);

const fill = (tid: number, time: number): WsFill => ({
  coin: 'BTC', px: '100', sz: '1', side: 'B', time, startPosition: '0', dir: 'Open Long', closedPnl: '0',
  hash: '0x0', oid: 1, crossed: true, fee: '0', tid, feeToken: 'USDC',
});

describe('createTidDeduper', () => {
  it('reports repeats', () => {
    const d = createTidDeduper();
    expect(d.seen(1)).toBe(false);
    expect(d.seen(1)).toBe(true);
    expect(d.seen('1')).toBe(true);
    expect(d.has(2)).toBe(false);
  });

  it('evicts the oldest ids beyond maxSize', () => {
    const d = createTidDeduper(2);
    d.seen(1);
    d.seen(2);
    d.seen(3);
    expect(d.size).toBe(2);
    expect(d.has(1)).toBe(false);
    expect(d.has(3)).toBe(true);
  });

  it('rejects a non-positive size', () => {
    expect(() => createTidDeduper(0)).toThrow(RangeError);
  });
});

describe('isSnapshotFrame', () => {
  it('is true only for isSnapshot === true', () => {
    expect(isSnapshotFrame({ isSnapshot: true })).toBe(true);
    expect(isSnapshotFrame({ isSnapshot: 'true' })).toBe(false);
    expect(isSnapshotFrame({})).toBe(false);
    expect(isSnapshotFrame(null)).toBe(false);
  });
});

describe('splitUserFills', () => {
  it('a snapshot is history: only session fills not seen before go to the ledger, reconcile from REST', () => {
    const d = createTidDeduper();
    const snapshot: WsUserFills = { user: USER, isSnapshot: true, fills: [fill(1, 500), fill(2, 1_500), fill(3, 2_000)] };
    const r = splitUserFills(snapshot, { sessionStartTime: 1_000, deduper: d });
    expect(r.isSnapshot).toBe(true);
    expect(r.reconcileFromRest).toBe(true);
    expect(r.fresh.map((f) => f.tid)).toEqual([2, 3]);
    expect(r.skipped.map((f) => f.tid)).toEqual([1]);
  });

  it('a snapshot re-sent after reconnect does not double fills', () => {
    const d = createTidDeduper();
    const live: WsUserFills = { user: USER, fills: [fill(5, 3_000)] };
    expect(splitUserFills(live, { sessionStartTime: 1_000, deduper: d }).fresh).toHaveLength(1);
    const again: WsUserFills = { user: USER, isSnapshot: true, fills: [fill(5, 3_000), fill(6, 3_500)] };
    const r = splitUserFills(again, { sessionStartTime: 1_000, deduper: d });
    expect(r.fresh.map((f) => f.tid)).toEqual([6]);
  });

  it('live frames ignore sessionStartTime but still dedupe', () => {
    const d = createTidDeduper();
    const r = splitUserFills({ user: USER, fills: [fill(1, 0), fill(1, 0)] }, { sessionStartTime: 1_000, deduper: d });
    expect(r.fresh.map((f) => f.tid)).toEqual([1]);
    expect(r.skipped).toHaveLength(1);
    expect(r.reconcileFromRest).toBe(false);
  });

  it('drops frames of another user (lowercase compare)', () => {
    const d = createTidDeduper();
    const r = splitUserFills({ user: '0x' + 'A'.repeat(40), fills: [fill(1, 0)] }, {
      sessionStartTime: 0, deduper: d, user: '0x' + 'b'.repeat(40),
    });
    expect(r.fresh).toEqual([]);
    const same = splitUserFills({ user: '0x' + 'A'.repeat(40), fills: [fill(1, 0)] }, {
      sessionStartTime: 0, deduper: d, user: '0x' + 'a'.repeat(40),
    });
    expect(same.fresh).toHaveLength(1);
  });

  it('tolerates a frame without fills', () => {
    const r = splitUserFills({ user: USER } as WsUserFills, { sessionStartTime: 0, deduper: createTidDeduper() });
    expect(r).toEqual({ isSnapshot: false, fresh: [], skipped: [], reconcileFromRest: false });
  });
});

describe('splitUserFills edge cases (review)', () => {
  it('includes a snapshot fill exactly at sessionStartTime and skips one 1 ms earlier', () => {
    const d = createTidDeduper();
    const out = splitUserFills({ user: USER, isSnapshot: true, fills: [fill(1, 999), fill(2, 1_000)] }, { sessionStartTime: 1_000, deduper: d });
    expect(out.fresh.map((f) => f.tid)).toEqual([2]);
    expect(out.skipped.map((f) => f.tid)).toEqual([1]);
    // A pre-session fill is not marked as seen: it is history, not a duplicate.
    expect(d.has(1)).toBe(false);
  });

  it('an empty snapshot still asks for a REST reconcile', () => {
    const out = splitUserFills({ user: USER, isSnapshot: true, fills: [] }, { sessionStartTime: 0, deduper: createTidDeduper() });
    expect(out).toEqual({ isSnapshot: true, fresh: [], skipped: [], reconcileFromRest: true });
  });

  it('dedupes large tids (> 2^49) and number/string forms of the same id', () => {
    const d = createTidDeduper();
    expect(d.seen(900000000000001)).toBe(false);
    expect(d.seen('900000000000001')).toBe(true);
    expect(d.seen(900000000000002)).toBe(false);
  });

  it('isSnapshot: false or a string "true" is a live frame', () => {
    expect(isSnapshotFrame({ isSnapshot: false })).toBe(false);
    expect(isSnapshotFrame({ isSnapshot: 'true' })).toBe(false);
    expect(isSnapshotFrame(null)).toBe(false);
  });

  it('a snapshot fill without a numeric time is treated as history', () => {
    const bad = { ...fill(9, 0), time: undefined as unknown as number };
    const out = splitUserFills({ user: USER, isSnapshot: true, fills: [bad] }, { sessionStartTime: 0, deduper: createTidDeduper() });
    expect(out.fresh).toEqual([]);
  });
});
All files