diff --git a/packages/indexer/src/testing/setup.ts b/packages/indexer/src/testing/setup.ts index 9fc3faf..51039b1 100644 --- a/packages/indexer/src/testing/setup.ts +++ b/packages/indexer/src/testing/setup.ts @@ -2,9 +2,10 @@ import { test as viTest } from "vitest"; import type { Indexer } from "../indexer"; import { type CassetteOptions, - VcrClient, type VcrConfig, type VcrReplayResult, + isCassetteAvailable, + loadCassette, record, replay, } from "../vcr"; @@ -46,13 +47,9 @@ async function withClient( const context: WithClientContext = { async run(indexer) { - const client = new VcrClient( - vcrConfig, - cassetteName, - indexer.streamConfig, - ); + const client = loadCassette(vcrConfig, cassetteName); - if (!client.isCassetteAvailable()) { + if (!isCassetteAvailable(vcrConfig, cassetteName)) { await record(vcrConfig, client, indexer, cassetteOptions); } diff --git a/packages/indexer/src/vcr/client.ts b/packages/indexer/src/vcr/client.ts deleted file mode 100644 index 16e1774..0000000 --- a/packages/indexer/src/vcr/client.ts +++ /dev/null @@ -1,82 +0,0 @@ -import assert from "node:assert"; -import fs from "node:fs"; -import path from "node:path"; -import type { - Client, - ClientCallOptions, - StatusRequest, - StatusResponse, - StreamConfig, - StreamDataOptions, - StreamDataRequest, - StreamDataResponse, -} from "@apibara/protocol"; -import type { VcrConfig } from "./config"; -import { deserialize } from "./helper"; -import type { CassetteDataType } from "./record"; - -export class VcrClient implements Client { - constructor( - private vcrConfig: VcrConfig, - private cassetteName: string, - private streamConfig: StreamConfig, - ) {} - - async status( - request?: StatusRequest, - options?: ClientCallOptions, - ): Promise { - throw new Error("Client.status is not implemented for VcrClient"); - } - - streamData(request: StreamDataRequest, options?: StreamDataOptions) { - if (!this.isCassetteAvailable()) { - throw new Error("Cassette doesn't exists"); - } - - const filePath = path.join( - this.vcrConfig.cassetteDir, - `${this.cassetteName}.json`, - ); - - const data = fs.readFileSync(filePath, "utf8"); - const cassetteData: CassetteDataType = deserialize(data); - - const { filter, messages } = cassetteData; - assert.deepStrictEqual( - filter, - request.filter[0], - Error("Request and Cassette filter mismatch"), - ); - - return new StreamDataIterable(messages); - } - - isCassetteAvailable(): boolean { - const filePath = path.join( - this.vcrConfig.cassetteDir, - `${this.cassetteName}.json`, - ); - return fs.existsSync(filePath); - } -} - -export class StreamDataIterable { - constructor(private messages: StreamDataResponse[]) {} - - [Symbol.asyncIterator](): AsyncIterator> { - let index = 0; - const messages = this.messages; - - return { - async next() { - if (index >= messages.length) { - return { done: true, value: undefined }; - } - - const message = messages[index++]; - return { done: false, value: message }; - }, - }; - } -} diff --git a/packages/indexer/src/vcr/helper.ts b/packages/indexer/src/vcr/helper.ts index d6a53a4..b1bbf37 100644 --- a/packages/indexer/src/vcr/helper.ts +++ b/packages/indexer/src/vcr/helper.ts @@ -1,3 +1,7 @@ +import fs from "node:fs"; +import path from "node:path"; +import type { VcrConfig } from "./config"; + export function deserialize(str: string) { return JSON.parse(str, (_, value) => typeof value === "string" && value.match(/^\d+n$/) @@ -13,3 +17,11 @@ export function serialize(obj: Record): string { "\t", ); } + +export function isCassetteAvailable( + vcrConfig: VcrConfig, + cassetteName: string, +): boolean { + const filePath = path.join(vcrConfig.cassetteDir, `${cassetteName}.json`); + return fs.existsSync(filePath); +} diff --git a/packages/indexer/src/vcr/index.ts b/packages/indexer/src/vcr/index.ts index 4e1857c..a375e41 100644 --- a/packages/indexer/src/vcr/index.ts +++ b/packages/indexer/src/vcr/index.ts @@ -1,4 +1,3 @@ -export * from "./client"; export * from "./config"; export * from "./helper"; export * from "./record"; diff --git a/packages/indexer/src/vcr/replay.ts b/packages/indexer/src/vcr/replay.ts index a04b6e0..a5a569b 100644 --- a/packages/indexer/src/vcr/replay.ts +++ b/packages/indexer/src/vcr/replay.ts @@ -1,7 +1,10 @@ -import { type Indexer, run } from "@apibara/indexer"; -import type { Cursor } from "@apibara/protocol"; +import fs from "node:fs"; +import path from "node:path"; +import type { Client, Cursor } from "@apibara/protocol"; +import { MockClient } from "@apibara/protocol/testing"; +import { type Indexer, run } from "../indexer"; import { vcr } from "../testing/vcr"; -import { VcrClient } from "./client"; +import { type CassetteDataType, deserialize } from "../vcr"; import type { VcrConfig } from "./config"; export async function replay( @@ -9,7 +12,7 @@ export async function replay( indexer: Indexer, cassetteName: string, ): Promise> { - const client = new VcrClient(vcrConfig, cassetteName, indexer.streamConfig); + const client = loadCassette(vcrConfig, cassetteName); const sink = vcr(); @@ -23,3 +26,17 @@ export async function replay( export type VcrReplayResult = { outputs: Array<{ endCursor?: Cursor; data: TRet[] }>; }; + +export function loadCassette( + vcrConfig: VcrConfig, + cassetteName: string, +): Client { + const filePath = path.join(vcrConfig.cassetteDir, `${cassetteName}.json`); + + const data = fs.readFileSync(filePath, "utf8"); + const cassetteData: CassetteDataType = deserialize(data); + + const { filter, messages } = cassetteData; + + return new MockClient(messages, [filter]); +} diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 1d8330e..a69bdef 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -4,7 +4,10 @@ "type": "module", "source": "./src/index.ts", "main": "./src/index.ts", - "exports": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./testing": "./src/testing/index.ts" + }, "publishConfig": { "files": ["dist", "src", "README.md"], "main": "./dist/index.mjs", @@ -14,6 +17,11 @@ "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "default": "./dist/index.mjs" + }, + "./testing": { + "types": "./dist/testing/index.d.ts", + "import": "./dist/testing/index.mjs", + "default": "./dist/testing/index.mjs" } } }, diff --git a/packages/protocol/src/testing/client.ts b/packages/protocol/src/testing/client.ts new file mode 100644 index 0000000..a92188b --- /dev/null +++ b/packages/protocol/src/testing/client.ts @@ -0,0 +1,49 @@ +import assert from "node:assert"; + +import type { Client, ClientCallOptions, StreamDataOptions } from "../client"; +import type { StatusRequest, StatusResponse } from "../status"; +import type { StreamDataRequest, StreamDataResponse } from "../stream"; + +export class MockClient implements Client { + constructor( + private messages: StreamDataResponse[], + private filter: TFilter[], + ) {} + + async status( + request?: StatusRequest, + options?: ClientCallOptions, + ): Promise { + throw new Error("Client.status is not implemented for VcrClient"); + } + + streamData(request: StreamDataRequest, options?: StreamDataOptions) { + assert.deepStrictEqual( + this.filter, + request.filter, + "Request and Cassette filter mismatch", + ); + + return new StreamDataIterable(this.messages); + } +} + +export class StreamDataIterable { + constructor(private messages: StreamDataResponse[]) {} + + [Symbol.asyncIterator](): AsyncIterator> { + let index = 0; + const messages = this.messages; + + return { + async next() { + if (index >= messages.length) { + return { done: true, value: undefined }; + } + + const message = messages[index++]; + return { done: false, value: message }; + }, + }; + } +} diff --git a/packages/protocol/src/testing/index.ts b/packages/protocol/src/testing/index.ts new file mode 100644 index 0000000..5ec7692 --- /dev/null +++ b/packages/protocol/src/testing/index.ts @@ -0,0 +1 @@ +export * from "./client";