SPB Git

Repository Public

venue-helpers.ts 3.5 KB 54 lines typescript

1// Fixture replay for the venue adapters: recorded HTTP exchanges served through an injected fetch.2import { readFileSync } from 'node:fs';34export interface HttpFixture { recordedAt: string; url: string; status: number; text: string; trimmed?: string }5export interface WsFixture { recordedAt: string; url: string; sent: unknown[]; messages: Record<string, unknown>[] }6export const fx = (venue: string, name: string): HttpFixture => JSON.parse(readFileSync(new URL(`./fixtures/${venue}/${name}.json`, import.meta.url), 'utf8'));7export const wsFx = (venue: string, name: string): WsFixture => JSON.parse(readFileSync(new URL(`./fixtures/${venue}/${name}.json`, import.meta.url), 'utf8'));89export type Route = [match: string | RegExp, answer: HttpFixture | { status: number; text: string } | (() => Response | Promise<Response>)];10/**11 * fetch replacement: the first route whose pattern matches the URL answers. A string pattern matches when the URL contains it.12 * `patch` rewrites the parsed JSON body of a matched fixture (to test malformed answers).13 */14export function replay(routes: Route[], patch?: (url: string, body: any) => unknown) {15  const calls: { url: string; method: string; body: string | null }[] = [];16  const fetchImpl = (async (url: string, init?: RequestInit) => {17    calls.push({ url: String(url), method: init?.method ?? 'GET', body: typeof init?.body === 'string' ? init.body : null });18    for (const [match, answer] of routes) {19      if (typeof match === 'string' ? !String(url).includes(match) : !match.test(String(url))) continue;20      if (typeof answer === 'function') return answer();21      let text = answer.text;22      if (patch && answer.status === 200) { const p = patch(String(url), JSON.parse(text)); if (p !== undefined) text = typeof p === 'string' ? p : JSON.stringify(p); }23      return new Response(text, { status: answer.status, headers: { 'content-type': 'application/json' } });24    }25    throw new Error(`no fixture route for ${url}`);26  }) as unknown as typeof fetch;27  return { fetch: fetchImpl, calls };28}2930/** Scripted WebSocket: the test drives `open`, `serve` (a server message), `drop` (connection lost). */31export class FakeSocket {32  static instances: FakeSocket[] = [];33  static reset(): void { FakeSocket.instances = []; }34  static get last(): FakeSocket { return FakeSocket.instances[FakeSocket.instances.length - 1]!; }35  readonly url: string;36  readonly sent: any[] = [];37  closed = false;38  onopen: ((ev: unknown) => void) | null = null;39  onmessage: ((ev: { data: unknown }) => void) | null = null;40  onclose: ((ev: unknown) => void) | null = null;41  onerror: ((ev: unknown) => void) | null = null;42  constructor(url: string) { this.url = url; FakeSocket.instances.push(this); }43  send(data: string): void { this.sent.push(JSON.parse(data)); }44  close(): void { this.closed = true; }45  open(): void { this.onopen?.({}); }46  serve(message: unknown): void { this.onmessage?.({ data: typeof message === 'string' ? message : JSON.stringify(message) }); }47  drop(): void { this.onclose?.({}); }48  subscriptions(): string[] { return this.sent.filter((m) => m.type === 'subscribe').map((m) => `${m.channel}|${m.id ?? ''}`); }49}5051// the shared models name their schemas XSchema; the venue tests use short names52export { AccountStateSchema as accountState, BookSchema as book, CandleSchema as candle, FillSchema as fill, FundingPaymentSchema as fundingPayment, InstrumentSchema as instrument,53  OrderSchema as order, PositionSchema as position, PublicTradeSchema as publicTrade, TickerSchema as ticker } from '../src/index.ts';54
Files main
Menu

Theme