@apibara/indexer 2.0.0-beta.9 → 2.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/index.cjs +271 -39
  2. package/dist/index.d.cts +1 -16
  3. package/dist/index.d.mts +1 -16
  4. package/dist/index.d.ts +1 -16
  5. package/dist/index.mjs +262 -25
  6. package/dist/internal/index.cjs +10 -0
  7. package/dist/internal/index.d.cts +3 -0
  8. package/dist/internal/index.d.mts +3 -0
  9. package/dist/internal/index.d.ts +3 -0
  10. package/dist/internal/index.mjs +8 -0
  11. package/dist/internal/plugins.cjs +38 -0
  12. package/dist/internal/plugins.d.cts +13 -0
  13. package/dist/internal/plugins.d.mts +13 -0
  14. package/dist/internal/plugins.d.ts +13 -0
  15. package/dist/internal/plugins.mjs +34 -0
  16. package/dist/internal/testing.cjs +118 -0
  17. package/dist/internal/testing.d.cts +42 -0
  18. package/dist/internal/testing.d.mts +42 -0
  19. package/dist/internal/testing.d.ts +42 -0
  20. package/dist/internal/testing.mjs +113 -0
  21. package/dist/plugins/index.cjs +39 -3
  22. package/dist/plugins/index.d.cts +16 -2
  23. package/dist/plugins/index.d.mts +16 -2
  24. package/dist/plugins/index.d.ts +16 -2
  25. package/dist/plugins/index.mjs +36 -3
  26. package/dist/shared/indexer.077335f3.cjs +15 -0
  27. package/dist/shared/indexer.2416906c.cjs +29 -0
  28. package/dist/shared/indexer.601ceab0.cjs +7 -0
  29. package/dist/shared/indexer.9b21ddd2.mjs +5 -0
  30. package/dist/shared/indexer.a55ad619.mjs +12 -0
  31. package/dist/shared/indexer.fedcd831.d.cts +100 -0
  32. package/dist/shared/indexer.fedcd831.d.mts +100 -0
  33. package/dist/shared/indexer.fedcd831.d.ts +100 -0
  34. package/dist/shared/indexer.ff25c953.mjs +26 -0
  35. package/dist/testing/index.cjs +52 -50
  36. package/dist/testing/index.d.cts +8 -36
  37. package/dist/testing/index.d.mts +8 -36
  38. package/dist/testing/index.d.ts +8 -36
  39. package/dist/testing/index.mjs +47 -47
  40. package/dist/vcr/index.cjs +84 -17
  41. package/dist/vcr/index.d.cts +16 -7
  42. package/dist/vcr/index.d.mts +16 -7
  43. package/dist/vcr/index.d.ts +16 -7
  44. package/dist/vcr/index.mjs +75 -11
  45. package/package.json +22 -42
  46. package/src/compose.test.ts +76 -0
  47. package/src/compose.ts +71 -0
  48. package/src/context.ts +14 -8
  49. package/src/index.ts +0 -5
  50. package/src/indexer.test.ts +125 -186
  51. package/src/indexer.ts +278 -151
  52. package/src/internal/index.ts +6 -0
  53. package/src/internal/plugins.ts +1 -0
  54. package/src/internal/testing.ts +148 -0
  55. package/src/plugins/config.ts +4 -4
  56. package/src/plugins/context.ts +40 -0
  57. package/src/plugins/index.ts +8 -1
  58. package/src/plugins/logger.ts +30 -0
  59. package/src/plugins/persistence.ts +24 -187
  60. package/src/testing/index.ts +58 -3
  61. package/src/vcr/record.ts +5 -3
  62. package/src/vcr/replay.ts +8 -18
  63. package/dist/plugins/kv.cjs +0 -131
  64. package/dist/plugins/kv.d.cts +0 -32
  65. package/dist/plugins/kv.d.mts +0 -32
  66. package/dist/plugins/kv.d.ts +0 -32
  67. package/dist/plugins/kv.mjs +0 -124
  68. package/dist/plugins/persistence.cjs +0 -182
  69. package/dist/plugins/persistence.d.cts +0 -50
  70. package/dist/plugins/persistence.d.mts +0 -50
  71. package/dist/plugins/persistence.d.ts +0 -50
  72. package/dist/plugins/persistence.mjs +0 -179
  73. package/dist/shared/indexer.2c23c9cd.mjs +0 -35
  74. package/dist/shared/indexer.318d3617.cjs +0 -47
  75. package/dist/shared/indexer.36530330.mjs +0 -249
  76. package/dist/shared/indexer.500fd281.d.cts +0 -23
  77. package/dist/shared/indexer.541d43eb.cjs +0 -266
  78. package/dist/shared/indexer.93d6b2eb.mjs +0 -17
  79. package/dist/shared/indexer.a8b7ab1f.cjs +0 -25
  80. package/dist/shared/indexer.b9c8f0d8.d.cts +0 -19
  81. package/dist/shared/indexer.b9c8f0d8.d.mts +0 -19
  82. package/dist/shared/indexer.b9c8f0d8.d.ts +0 -19
  83. package/dist/shared/indexer.c7ed6b83.d.cts +0 -82
  84. package/dist/shared/indexer.e1856641.d.mts +0 -23
  85. package/dist/shared/indexer.e4f2430f.d.ts +0 -23
  86. package/dist/shared/indexer.e8bd138d.d.mts +0 -82
  87. package/dist/shared/indexer.f761abcd.d.ts +0 -82
  88. package/dist/sinks/csv.cjs +0 -85
  89. package/dist/sinks/csv.d.cts +0 -66
  90. package/dist/sinks/csv.d.mts +0 -66
  91. package/dist/sinks/csv.d.ts +0 -66
  92. package/dist/sinks/csv.mjs +0 -78
  93. package/dist/sinks/drizzle/index.cjs +0 -212
  94. package/dist/sinks/drizzle/index.d.cts +0 -153
  95. package/dist/sinks/drizzle/index.d.mts +0 -153
  96. package/dist/sinks/drizzle/index.d.ts +0 -153
  97. package/dist/sinks/drizzle/index.mjs +0 -198
  98. package/dist/sinks/sqlite.cjs +0 -90
  99. package/dist/sinks/sqlite.d.cts +0 -71
  100. package/dist/sinks/sqlite.d.mts +0 -71
  101. package/dist/sinks/sqlite.d.ts +0 -71
  102. package/dist/sinks/sqlite.mjs +0 -87
  103. package/src/hooks/index.ts +0 -2
  104. package/src/hooks/useKVStore.ts +0 -12
  105. package/src/hooks/useSink.ts +0 -13
  106. package/src/plugins/kv.test.ts +0 -120
  107. package/src/plugins/kv.ts +0 -132
  108. package/src/plugins/persistence.test.ts +0 -151
  109. package/src/sink.ts +0 -36
  110. package/src/sinks/csv.test.ts +0 -65
  111. package/src/sinks/csv.ts +0 -159
  112. package/src/sinks/drizzle/Int8Range.ts +0 -52
  113. package/src/sinks/drizzle/delete.ts +0 -42
  114. package/src/sinks/drizzle/drizzle.test.ts +0 -239
  115. package/src/sinks/drizzle/drizzle.ts +0 -115
  116. package/src/sinks/drizzle/index.ts +0 -6
  117. package/src/sinks/drizzle/insert.ts +0 -42
  118. package/src/sinks/drizzle/select.ts +0 -44
  119. package/src/sinks/drizzle/transaction.ts +0 -49
  120. package/src/sinks/drizzle/update.ts +0 -47
  121. package/src/sinks/drizzle/utils.ts +0 -99
  122. package/src/sinks/sqlite.test.ts +0 -99
  123. package/src/sinks/sqlite.ts +0 -170
  124. package/src/testing/helper.ts +0 -13
  125. package/src/testing/indexer.ts +0 -35
  126. package/src/testing/setup.ts +0 -59
  127. package/src/testing/vcr.ts +0 -54
@@ -0,0 +1,148 @@
1
+ import { type Finalize, type Invalidate, isCursor } from "@apibara/protocol";
2
+ import {
3
+ type MockBlock,
4
+ type MockFilter,
5
+ MockStream,
6
+ type MockStreamResponse,
7
+ } from "@apibara/protocol/testing";
8
+
9
+ import { useIndexerContext } from "../context";
10
+ import { type IndexerConfig, createIndexer, defineIndexer } from "../indexer";
11
+ import { defineIndexerPlugin } from "../plugins";
12
+ import { type InternalContext, internalContext } from "./plugins";
13
+
14
+ export type MockMessagesOptions = {
15
+ invalidate?: {
16
+ invalidateFromIndex: number;
17
+ invalidateTriggerIndex: number;
18
+ };
19
+ finalize?: {
20
+ finalizeToIndex: number;
21
+ finalizeTriggerIndex: number;
22
+ };
23
+ };
24
+
25
+ export function generateMockMessages(
26
+ count = 10,
27
+ options?: MockMessagesOptions,
28
+ ): MockStreamResponse[] {
29
+ const invalidateAt = options?.invalidate;
30
+ const finalizeAt = options?.finalize;
31
+ const messages: MockStreamResponse[] = [];
32
+
33
+ for (let i = 0; i < count; i++) {
34
+ if (invalidateAt && i === invalidateAt.invalidateTriggerIndex) {
35
+ messages.push({
36
+ _tag: "invalidate",
37
+ invalidate: {
38
+ cursor: {
39
+ orderKey: BigInt(5_000_000 + invalidateAt.invalidateFromIndex),
40
+ },
41
+ },
42
+ } as Invalidate);
43
+ } else if (finalizeAt && i === finalizeAt.finalizeTriggerIndex) {
44
+ messages.push({
45
+ _tag: "finalize",
46
+ finalize: {
47
+ cursor: {
48
+ orderKey: BigInt(5_000_000 + finalizeAt.finalizeToIndex),
49
+ },
50
+ },
51
+ } as Finalize);
52
+ } else {
53
+ messages.push({
54
+ _tag: "data",
55
+ data: {
56
+ cursor: { orderKey: BigInt(5_000_000 + i - 1) },
57
+ finality: "accepted",
58
+ data: [{ data: `${5_000_000 + i}` }],
59
+ endCursor: { orderKey: BigInt(5_000_000 + i) },
60
+ production: "backfill",
61
+ },
62
+ });
63
+ }
64
+ }
65
+
66
+ return messages;
67
+ }
68
+
69
+ type MockIndexerParams = {
70
+ internalContext?: InternalContext;
71
+ override?: Partial<IndexerConfig<MockFilter, MockBlock>>;
72
+ };
73
+
74
+ export function getMockIndexer(params?: MockIndexerParams) {
75
+ const { internalContext: contextParams, override } = params ?? {};
76
+ const { plugins, ...rest } = override ?? {};
77
+
78
+ return createIndexer(
79
+ defineIndexer(MockStream)({
80
+ streamUrl: "https://sepolia.ethereum.a5a.ch",
81
+ finality: "accepted",
82
+ filter: {},
83
+ async transform() {},
84
+ plugins: [
85
+ internalContext(
86
+ contextParams ??
87
+ ({
88
+ availableIndexers: ["testing"],
89
+ indexerName: "testing",
90
+ } as InternalContext),
91
+ ),
92
+ ...(plugins ?? []),
93
+ ],
94
+ ...(rest ?? {}),
95
+ }),
96
+ );
97
+ }
98
+
99
+ export type MockRet = {
100
+ data: string;
101
+ };
102
+
103
+ /**
104
+ * A mock sink used for testing. The indexer function can write to the output array.
105
+ * The indexer context is optionally written to the metadata object.
106
+ */
107
+ export function mockSink<TFilter, TBlock>({
108
+ output,
109
+ metadata,
110
+ }: { output: unknown[]; metadata?: Record<string, unknown> }) {
111
+ return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
112
+ indexer.hooks.hook("connect:before", ({ request }) => {
113
+ if (metadata?.lastCursor && isCursor(metadata.lastCursor)) {
114
+ request.startingCursor = metadata.lastCursor;
115
+ }
116
+
117
+ if (metadata?.lastFilter) {
118
+ request.filter[1] = metadata.lastFilter as TFilter;
119
+ }
120
+ });
121
+
122
+ indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
123
+ if (request.filter[1]) {
124
+ if (metadata) {
125
+ metadata.lastCursor = endCursor;
126
+ metadata.lastFilter = request.filter[1];
127
+ }
128
+ }
129
+ });
130
+
131
+ indexer.hooks.hook("handler:middleware", ({ use }) => {
132
+ use(async (context, next) => {
133
+ context.output = output;
134
+ await next();
135
+ context.output = null;
136
+
137
+ if (metadata) {
138
+ metadata.lastCursor = context.endCursor;
139
+ }
140
+ });
141
+ });
142
+ });
143
+ }
144
+
145
+ export function useMockSink(): { output: unknown[] } {
146
+ const context = useIndexerContext();
147
+ return { output: context.output };
148
+ }
@@ -1,11 +1,11 @@
1
1
  import type { Indexer } from "../indexer";
2
2
 
3
- export type IndexerPlugin<TFilter, TBlock, TTxnParams> = (
4
- indexer: Indexer<TFilter, TBlock, TTxnParams>,
3
+ export type IndexerPlugin<TFilter, TBlock> = (
4
+ indexer: Indexer<TFilter, TBlock>,
5
5
  ) => void;
6
6
 
7
- export function defineIndexerPlugin<TFilter, TBlock, TTxnParams>(
8
- def: IndexerPlugin<TFilter, TBlock, TTxnParams>,
7
+ export function defineIndexerPlugin<TFilter, TBlock>(
8
+ def: IndexerPlugin<TFilter, TBlock>,
9
9
  ) {
10
10
  return def;
11
11
  }
@@ -0,0 +1,40 @@
1
+ import { useIndexerContext } from "../context";
2
+ import { defineIndexerPlugin } from "./config";
3
+
4
+ export const INTERNAL_CONTEXT_PROPERTY = "_internal";
5
+
6
+ export function internalContext<TFilter, TBlock, TTxnParams>(
7
+ values: Record<string, unknown>,
8
+ ) {
9
+ return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
10
+ indexer.hooks.hook("run:before", () => {
11
+ try {
12
+ const ctx = useIndexerContext();
13
+ ctx[INTERNAL_CONTEXT_PROPERTY] = {
14
+ ...(ctx[INTERNAL_CONTEXT_PROPERTY] || {}),
15
+ ...values,
16
+ };
17
+ } catch (error) {
18
+ throw new Error("Failed to set internal context", {
19
+ cause: error,
20
+ });
21
+ }
22
+ });
23
+ });
24
+ }
25
+
26
+ export type InternalContext = {
27
+ indexerName: string;
28
+ availableIndexers: string[];
29
+ };
30
+
31
+ export function useInternalContext(): InternalContext {
32
+ const ctx = useIndexerContext();
33
+
34
+ if (ctx[INTERNAL_CONTEXT_PROPERTY] === undefined) {
35
+ throw new Error(
36
+ "Internal context is not available, possibly 'internalContext' plugin is missing!",
37
+ );
38
+ }
39
+ return ctx[INTERNAL_CONTEXT_PROPERTY];
40
+ }
@@ -1 +1,8 @@
1
- export * from "./config";
1
+ export { defineIndexerPlugin, type IndexerPlugin } from "./config";
2
+ export {
3
+ type ConsolaInstance,
4
+ type ConsolaReporter,
5
+ logger,
6
+ useLogger,
7
+ } from "./logger";
8
+ export { inMemoryPersistence } from "./persistence";
@@ -0,0 +1,30 @@
1
+ import { type ConsolaInstance, type ConsolaReporter, consola } from "consola";
2
+ import { useIndexerContext } from "../context";
3
+ import { defineIndexerPlugin } from "./config";
4
+
5
+ export type { ConsolaReporter, ConsolaInstance } from "consola";
6
+
7
+ export function logger<TFilter, TBlock, TTxnParams>({
8
+ logger,
9
+ }: { logger?: ConsolaReporter } = {}) {
10
+ return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
11
+ indexer.hooks.hook("run:before", () => {
12
+ const ctx = useIndexerContext();
13
+
14
+ if (logger) {
15
+ ctx.logger = consola.create({ reporters: [logger] });
16
+ } else {
17
+ ctx.logger = consola.create({});
18
+ }
19
+ });
20
+ });
21
+ }
22
+
23
+ export function useLogger(): ConsolaInstance {
24
+ const ctx = useIndexerContext();
25
+
26
+ if (!ctx?.logger)
27
+ throw new Error("Logger plugin is not available in context");
28
+
29
+ return ctx.logger;
30
+ }
@@ -1,202 +1,39 @@
1
- import type { Cursor } from "@apibara/protocol";
2
- import type { Database as SqliteDatabase, Statement } from "better-sqlite3";
3
- import { deserialize, serialize } from "../vcr";
4
- import { defineIndexerPlugin } from "./config";
1
+ import { type Cursor, isCursor } from "@apibara/protocol";
5
2
 
6
- export function sqlitePersistence<TFilter, TBlock, TTxnParams>({
7
- database,
8
- }: { database: SqliteDatabase }) {
9
- return defineIndexerPlugin<TFilter, TBlock, TTxnParams>((indexer) => {
10
- let store: SqlitePersistence<TFilter>;
3
+ import { defineIndexerPlugin } from "./config";
11
4
 
12
- indexer.hooks.hook("run:before", () => {
13
- SqlitePersistence.initialize(database);
14
-
15
- store = new SqlitePersistence(database);
16
- });
5
+ /**
6
+ * A plugin that persists the last cursor and filter to memory.
7
+ */
8
+ export function inMemoryPersistence<TFilter, TBlock>() {
9
+ return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
10
+ let lastCursor: Cursor | undefined;
11
+ let lastFilter: TFilter | undefined;
17
12
 
18
13
  indexer.hooks.hook("connect:before", ({ request }) => {
19
- const { cursor, filter } = store.get();
20
-
21
- if (cursor) {
22
- request.startingCursor = cursor;
14
+ if (lastCursor) {
15
+ request.startingCursor = lastCursor;
23
16
  }
24
17
 
25
- if (filter) {
26
- request.filter[1] = filter;
27
- }
28
- });
29
-
30
- indexer.hooks.hook("transaction:commit", ({ endCursor }) => {
31
- if (endCursor) {
32
- store.put({ cursor: endCursor });
18
+ if (lastFilter) {
19
+ request.filter[1] = lastFilter;
33
20
  }
34
21
  });
35
22
 
36
23
  indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
37
24
  if (request.filter[1]) {
38
- store.put({ cursor: endCursor, filter: request.filter[1] });
25
+ lastCursor = endCursor;
26
+ lastFilter = request.filter[1];
39
27
  }
40
28
  });
41
- });
42
- }
43
-
44
- export class SqlitePersistence<TFilter> {
45
- /** Sqlite Queries Prepare Statements */
46
- private _getCheckpointQuery: Statement<string, CheckpointRow>;
47
- private _putCheckpointQuery: Statement<
48
- [string, number, `0x${string}` | undefined]
49
- >;
50
- private _delCheckpointQuery: Statement<string>;
51
- private _getFilterQuery: Statement<string, FilterRow>;
52
- private _updateFilterToBlockQuery: Statement<[number, string]>;
53
- private _insertFilterQuery: Statement<[string, string, number]>;
54
- private _delFilterQuery: Statement<string>;
55
-
56
- constructor(private _db: SqliteDatabase) {
57
- this._getCheckpointQuery = this._db.prepare(statements.getCheckpoint);
58
- this._putCheckpointQuery = this._db.prepare(statements.putCheckpoint);
59
- this._delCheckpointQuery = this._db.prepare(statements.delCheckpoint);
60
- this._getFilterQuery = this._db.prepare(statements.getFilter);
61
- this._updateFilterToBlockQuery = this._db.prepare(
62
- statements.updateFilterToBlock,
63
- );
64
- this._insertFilterQuery = this._db.prepare(statements.insertFilter);
65
- this._delFilterQuery = this._db.prepare(statements.delFilter);
66
- }
67
-
68
- static initialize(db: SqliteDatabase) {
69
- db.prepare(statements.createCheckpointsTable).run();
70
- db.prepare(statements.createFiltersTable).run();
71
- }
72
-
73
- public get(): { cursor?: Cursor; filter?: TFilter } {
74
- const cursor = this._getCheckpoint();
75
- const filter = this._getFilter();
76
-
77
- return { cursor, filter };
78
- }
79
-
80
- public put({ cursor, filter }: { cursor?: Cursor; filter?: TFilter }) {
81
- if (cursor) {
82
- this._putCheckpoint(cursor);
83
-
84
- if (filter) {
85
- this._putFilter(filter, cursor);
86
- }
87
- }
88
- }
89
29
 
90
- public del() {
91
- this._delCheckpoint();
92
- this._delFilter();
93
- }
94
-
95
- // --- CHECKPOINTS TABLE METHODS ---
96
-
97
- private _getCheckpoint(): Cursor | undefined {
98
- const row = this._getCheckpointQuery.get("default");
99
-
100
- if (!row) return undefined;
101
-
102
- return { orderKey: BigInt(row.order_key), uniqueKey: row.unique_key };
103
- }
104
-
105
- private _putCheckpoint(cursor: Cursor) {
106
- this._putCheckpointQuery.run(
107
- "default",
108
- Number(cursor.orderKey),
109
- cursor.uniqueKey,
110
- );
111
- }
112
-
113
- private _delCheckpoint() {
114
- this._delCheckpointQuery.run("default");
115
- }
116
-
117
- // --- FILTERS TABLE METHODS ---
118
-
119
- private _getFilter(): TFilter | undefined {
120
- const row = this._getFilterQuery.get("default");
121
-
122
- if (!row) return undefined;
123
-
124
- return deserialize(row.filter) as TFilter;
125
- }
126
-
127
- private _putFilter(filter: TFilter, endCursor: Cursor) {
128
- this._updateFilterToBlockQuery.run(Number(endCursor.orderKey), "default");
129
- this._insertFilterQuery.run(
130
- "default",
131
- serialize(filter as Record<string, unknown>),
132
- Number(endCursor.orderKey),
133
- );
134
- }
135
-
136
- private _delFilter() {
137
- this._delFilterQuery.run("default");
138
- }
30
+ indexer.hooks.hook("handler:middleware", ({ use }) => {
31
+ use(async (context, next) => {
32
+ await next();
33
+ if (context.endCursor && isCursor(context.endCursor)) {
34
+ lastCursor = context.endCursor;
35
+ }
36
+ });
37
+ });
38
+ });
139
39
  }
140
-
141
- const statements = {
142
- beginTxn: "BEGIN TRANSACTION",
143
- commitTxn: "COMMIT TRANSACTION",
144
- rollbackTxn: "ROLLBACK TRANSACTION",
145
- createCheckpointsTable: `
146
- CREATE TABLE IF NOT EXISTS checkpoints (
147
- id TEXT NOT NULL PRIMARY KEY,
148
- order_key INTEGER NOT NULL,
149
- unique_key TEXT
150
- );`,
151
- createFiltersTable: `
152
- CREATE TABLE IF NOT EXISTS filters (
153
- id TEXT NOT NULL,
154
- filter BLOB NOT NULL,
155
- from_block INTEGER NOT NULL,
156
- to_block INTEGER,
157
- PRIMARY KEY (id, from_block)
158
- );`,
159
- getCheckpoint: `
160
- SELECT *
161
- FROM checkpoints
162
- WHERE id = ?`,
163
- putCheckpoint: `
164
- INSERT INTO checkpoints (id, order_key, unique_key)
165
- VALUES (?, ?, ?)
166
- ON CONFLICT(id) DO UPDATE SET
167
- order_key = excluded.order_key,
168
- unique_key = excluded.unique_key`,
169
- delCheckpoint: `
170
- DELETE FROM checkpoints
171
- WHERE id = ?`,
172
- getFilter: `
173
- SELECT *
174
- FROM filters
175
- WHERE id = ? AND to_block IS NULL`,
176
- updateFilterToBlock: `
177
- UPDATE filters
178
- SET to_block = ?
179
- WHERE id = ? AND to_block IS NULL`,
180
- insertFilter: `
181
- INSERT INTO filters (id, filter, from_block)
182
- VALUES (?, ?, ?)
183
- ON CONFLICT(id, from_block) DO UPDATE SET
184
- filter = excluded.filter,
185
- from_block = excluded.from_block`,
186
- delFilter: `
187
- DELETE FROM filters
188
- WHERE id = ?`,
189
- };
190
-
191
- export type CheckpointRow = {
192
- id: string;
193
- order_key: number;
194
- unique_key?: `0x${string}`;
195
- };
196
-
197
- export type FilterRow = {
198
- id: string;
199
- filter: string;
200
- from_block: number;
201
- to_block?: number;
202
- };
@@ -1,3 +1,58 @@
1
- export * from "./setup";
2
- export * from "./vcr";
3
- export * from "./helper";
1
+ import { createClient } from "@apibara/protocol";
2
+ import ci from "ci-info";
3
+ import { type IndexerWithStreamConfig, createIndexer } from "../indexer";
4
+ import { type InternalContext, internalContext } from "../plugins/context";
5
+ import { logger } from "../plugins/logger";
6
+ import type { CassetteOptions, VcrConfig } from "../vcr/config";
7
+ import { isCassetteAvailable } from "../vcr/helper";
8
+ import { record } from "../vcr/record";
9
+ import { replay } from "../vcr/replay";
10
+
11
+ export function createVcr() {
12
+ return {
13
+ async run<TFilter, TBlock>(
14
+ cassetteName: string,
15
+ indexerConfig: IndexerWithStreamConfig<TFilter, TBlock>,
16
+ range: { fromBlock: bigint; toBlock: bigint },
17
+ ) {
18
+ const vcrConfig: VcrConfig = {
19
+ cassetteDir: "cassettes",
20
+ };
21
+
22
+ const cassetteOptions: CassetteOptions = {
23
+ name: cassetteName,
24
+ startingCursor: {
25
+ orderKey: range.fromBlock,
26
+ },
27
+ endingCursor: {
28
+ orderKey: range.toBlock,
29
+ },
30
+ };
31
+
32
+ indexerConfig.plugins = [
33
+ internalContext({
34
+ indexerName: cassetteName,
35
+ availableIndexers: [cassetteName],
36
+ } as InternalContext),
37
+ logger(),
38
+ ...(indexerConfig.plugins ?? []),
39
+ ];
40
+
41
+ const indexer = createIndexer(indexerConfig);
42
+
43
+ if (!isCassetteAvailable(vcrConfig, cassetteName)) {
44
+ if (ci.isCI) {
45
+ throw new Error("Cannot record cassette in CI");
46
+ }
47
+
48
+ const client = createClient(
49
+ indexer.streamConfig,
50
+ indexer.options.streamUrl,
51
+ );
52
+ await record(vcrConfig, client, indexer, cassetteOptions);
53
+ } else {
54
+ await replay(vcrConfig, indexer, cassetteName);
55
+ }
56
+ },
57
+ };
58
+ }
package/src/vcr/record.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import type { Client, StreamDataResponse } from "@apibara/protocol";
4
- import { klona } from "klona/full";
5
4
  import { type Indexer, run } from "../indexer";
6
5
  import type { CassetteOptions, VcrConfig } from "./config";
7
6
  import { serialize } from "./helper";
@@ -14,10 +13,9 @@ export type CassetteDataType<TFilter, TBlock> = {
14
13
  export async function record<TFilter, TBlock, TTxnParams>(
15
14
  vcrConfig: VcrConfig,
16
15
  client: Client<TFilter, TBlock>,
17
- indexerArg: Indexer<TFilter, TBlock, TTxnParams>,
16
+ indexer: Indexer<TFilter, TBlock>,
18
17
  cassetteOptions: CassetteOptions,
19
18
  ) {
20
- const indexer = klona(indexerArg);
21
19
  const messages: StreamDataResponse<TBlock>[] = [];
22
20
 
23
21
  indexer.hooks.addHooks({
@@ -33,10 +31,14 @@ export async function record<TFilter, TBlock, TTxnParams>(
33
31
  filter: indexer.options.filter,
34
32
  messages: messages,
35
33
  };
34
+
35
+ await fs.mkdir(vcrConfig.cassetteDir, { recursive: true });
36
+
36
37
  const filePath = path.join(
37
38
  vcrConfig.cassetteDir,
38
39
  `${cassetteOptions.name}.json`,
39
40
  );
41
+
40
42
  await fs.writeFile(filePath, serialize(output), { flag: "w" });
41
43
  },
42
44
  });
package/src/vcr/replay.ts CHANGED
@@ -1,34 +1,21 @@
1
1
  import assert from "node:assert";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import type { Client, Cursor } from "@apibara/protocol";
4
+ import type { Client } from "@apibara/protocol";
5
5
  import { MockClient } from "@apibara/protocol/testing";
6
6
  import { type Indexer, run } from "../indexer";
7
- import type { SinkData } from "../sink";
8
- import { vcr } from "../testing/vcr";
9
7
  import { type CassetteDataType, deserialize } from "../vcr";
10
8
  import type { VcrConfig } from "./config";
11
9
 
12
10
  export async function replay<TFilter, TBlock, TTxnParams>(
13
11
  vcrConfig: VcrConfig,
14
- indexer: Indexer<TFilter, TBlock, TTxnParams>,
12
+ indexer: Indexer<TFilter, TBlock>,
15
13
  cassetteName: string,
16
- ): Promise<VcrReplayResult> {
14
+ ) {
17
15
  const client = loadCassette<TFilter, TBlock>(vcrConfig, cassetteName);
18
-
19
- const sink = vcr();
20
-
21
16
  await run(client, indexer);
22
-
23
- return {
24
- outputs: sink.result,
25
- };
26
17
  }
27
18
 
28
- export type VcrReplayResult = {
29
- outputs: Array<{ endCursor?: Cursor; data: SinkData[] }>;
30
- };
31
-
32
19
  export function loadCassette<TFilter, TBlock>(
33
20
  vcrConfig: VcrConfig,
34
21
  cassetteName: string,
@@ -41,11 +28,14 @@ export function loadCassette<TFilter, TBlock>(
41
28
  const { filter, messages } = cassetteData;
42
29
 
43
30
  return new MockClient<TFilter, TBlock>((request, options) => {
31
+ // Notice that the request filter is an array of filters,
32
+ // so we need to wrap the indexer filter in an array.
44
33
  assert.deepStrictEqual(
45
34
  request.filter,
46
- filter,
47
- "Request and Cassette filter mismatch",
35
+ [filter],
36
+ "Indexer and cassette filter mismatch. Hint: delete the cassette and run again.",
48
37
  );
38
+
49
39
  return messages;
50
40
  });
51
41
  }