Repository Public
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