Skip to content
markpaper

src/writeBudget/window.test.ts

v0.2.1 · 4.1 KB

Download file
import { describe, expect, it } from 'vitest';
import {
  createWriteWindow,
  EXCHANGE_WRITE_LIMIT,
  seedWriteWindow,
} from './window.js';

describe('constants', () => {
  it('exposes only the exchange limit', () => {
    expect(EXCHANGE_WRITE_LIMIT).toBe(40);
  });
});

describe('createWriteWindow', () => {
  it('gives ordinary writes limit - reserve and critical writes the whole limit', () => {
    const w = createWriteWindow({ limit: 5, reserve: 2, now: () => 0 });
    let ordinary = 0;
    while (w.tryTake(false).ok) ordinary++;
    expect(ordinary).toBe(3);
    expect(w.budget()).toMatchObject({ used: 3, freeOrdinary: 0, freeCritical: 2 });
    let critical = 0;
    while (w.tryTake(true).ok) critical++;
    expect(critical).toBe(2);
    expect(w.budget()).toMatchObject({ used: 5, freeOrdinary: 0, freeCritical: 0 });
    const refused = w.tryTake(true);
    expect(refused.ok).toBe(false);
    if (!refused.ok) expect(refused.rateLimited).toBe(true);
  });

  it('is a sliding window by timestamps, not a per-tick counter', () => {
    const w = createWriteWindow({ limit: 4, reserve: 0, windowMs: 60_000 });
    expect(w.tryTake(false, 0).ok).toBe(true);
    expect(w.tryTake(false, 15_000).ok).toBe(true);
    expect(w.tryTake(false, 30_000).ok).toBe(true);
    expect(w.tryTake(false, 45_000).ok).toBe(true);
    expect(w.tryTake(false, 59_999).ok).toBe(false); // four ticks, one window
    expect(w.tryTake(false, 60_000).ok).toBe(true); // the write at t=0 left the window
    expect(w.budget(60_000).used).toBe(4);
  });

  it('takes the slot before the request and does not give it back', () => {
    const w = createWriteWindow({ limit: 1, reserve: 0, now: () => 0 });
    expect(w.tryTake(false).ok).toBe(true);
    // a failed request cannot "return" the slot: there is no API for it
    expect(w.tryTake(false).ok).toBe(false);
  });

  it('explains a refusal with numbers', () => {
    const w = createWriteWindow({ limit: 3, reserve: 1, now: () => 0 });
    w.tryTake(false);
    w.tryTake(false);
    const r = w.tryTake(false);
    expect(r.ok).toBe(false);
    if (!r.ok)
      expect(r.error).toBe('write budget 2/3 per 60s exhausted (1 reserved for cancels and reduceOnly orders)');
  });

  it('seedWriteWindow spreads a full budget evenly over the past window', () => {
    const stamps = seedWriteWindow(5, 60_000);
    expect(stamps).toHaveLength(5);
    expect(stamps[0]).toBeGreaterThan(0);
    expect(stamps[4]).toBeLessThan(60_000);
    expect(stamps.every((t, i, a) => i === 0 || t > (a[i - 1] as number))).toBe(true);
    expect(seedWriteWindow(0, 1)).toEqual([]);
  });

  it('seedAsExhausted blocks at start and frees gradually', () => {
    const w = createWriteWindow({ limit: 5, reserve: 2, windowMs: 60_000 });
    w.seedAsExhausted(100_000);
    expect(w.budget(100_000).used).toBe(5);
    expect(w.tryTake(true, 100_000).ok).toBe(false);
    expect(w.tryTake(true, 111_000).ok).toBe(true);
    expect(w.tryTake(false, 121_000).ok).toBe(false);
    expect(w.budget(160_001).used).toBeLessThanOrEqual(1);
  });

  it('seedFromRecentWrites replays a journal, ignoring stale and future stamps', () => {
    const w = createWriteWindow({ limit: 5, reserve: 0, windowMs: 60_000 });
    w.seedFromRecentWrites([10_000, 50_000, 70_000, 200_000, Number.NaN], 100_000);
    expect(w.stamps(100_000)).toEqual([50_000, 70_000]);
    expect(w.budget(100_000).used).toBe(2);
  });

  it('validates options', () => {
    expect(() => (createWriteWindow as unknown as () => unknown)()).toThrow(
      'createWriteWindow requires explicit limit and reserve',
    );
    expect(() => createWriteWindow({ limit: 0, reserve: 0 })).toThrow(RangeError);
    expect(() => createWriteWindow({ limit: 41, reserve: 0 })).toThrow(RangeError);
    expect(() => createWriteWindow({ limit: 5, reserve: 6 })).toThrow(RangeError);
    expect(() => createWriteWindow({ limit: 5, reserve: 0, windowMs: 0 })).toThrow(RangeError);
  });

  it('reset clears the window', () => {
    const w = createWriteWindow({ limit: 1, reserve: 0, now: () => 0 });
    w.tryTake(false);
    w.reset();
    expect(w.tryTake(false).ok).toBe(true);
  });
});
All files