@apibara/indexer 2.0.0-beta.3 → 2.0.0-beta.31

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 (74) hide show
  1. package/dist/index.cjs +270 -0
  2. package/dist/index.d.cts +3 -0
  3. package/dist/index.d.mts +3 -0
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.mjs +259 -0
  6. package/dist/internal/testing.cjs +109 -0
  7. package/dist/internal/testing.d.cts +40 -0
  8. package/dist/internal/testing.d.mts +40 -0
  9. package/dist/internal/testing.d.ts +40 -0
  10. package/dist/internal/testing.mjs +104 -0
  11. package/dist/plugins/index.cjs +43 -0
  12. package/dist/plugins/index.d.cts +18 -0
  13. package/dist/plugins/index.d.mts +18 -0
  14. package/dist/plugins/index.d.ts +18 -0
  15. package/dist/plugins/index.mjs +38 -0
  16. package/dist/shared/indexer.077335f3.cjs +15 -0
  17. package/dist/shared/indexer.2416906c.cjs +29 -0
  18. package/dist/shared/indexer.601ceab0.cjs +7 -0
  19. package/dist/shared/indexer.8939ecc8.d.cts +91 -0
  20. package/dist/shared/indexer.8939ecc8.d.mts +91 -0
  21. package/dist/shared/indexer.8939ecc8.d.ts +91 -0
  22. package/dist/shared/indexer.9b21ddd2.mjs +5 -0
  23. package/dist/shared/indexer.a55ad619.mjs +12 -0
  24. package/dist/shared/indexer.ff25c953.mjs +26 -0
  25. package/dist/testing/index.cjs +58 -0
  26. package/dist/testing/index.d.cts +12 -0
  27. package/dist/testing/index.d.mts +12 -0
  28. package/dist/testing/index.d.ts +12 -0
  29. package/dist/testing/index.mjs +52 -0
  30. package/dist/vcr/index.cjs +92 -0
  31. package/dist/vcr/index.d.cts +27 -0
  32. package/dist/vcr/index.d.mts +27 -0
  33. package/dist/vcr/index.d.ts +27 -0
  34. package/dist/vcr/index.mjs +78 -0
  35. package/package.json +31 -41
  36. package/src/compose.test.ts +76 -0
  37. package/src/compose.ts +71 -0
  38. package/src/context.ts +14 -8
  39. package/src/index.ts +0 -5
  40. package/src/indexer.test.ts +109 -186
  41. package/src/indexer.ts +244 -144
  42. package/src/internal/testing.ts +135 -0
  43. package/src/plugins/config.ts +4 -4
  44. package/src/plugins/index.ts +8 -1
  45. package/src/plugins/logger.ts +30 -0
  46. package/src/plugins/persistence.ts +24 -187
  47. package/src/testing/index.ts +50 -3
  48. package/src/vcr/record.ts +6 -4
  49. package/src/vcr/replay.ts +8 -18
  50. package/src/hooks/index.ts +0 -2
  51. package/src/hooks/useKVStore.ts +0 -12
  52. package/src/hooks/useSink.ts +0 -13
  53. package/src/plugins/kv.test.ts +0 -120
  54. package/src/plugins/kv.ts +0 -132
  55. package/src/plugins/persistence.test.ts +0 -151
  56. package/src/sink.ts +0 -36
  57. package/src/sinks/csv.test.ts +0 -65
  58. package/src/sinks/csv.ts +0 -159
  59. package/src/sinks/drizzle/Int8Range.ts +0 -52
  60. package/src/sinks/drizzle/delete.ts +0 -42
  61. package/src/sinks/drizzle/drizzle.test.ts +0 -239
  62. package/src/sinks/drizzle/drizzle.ts +0 -115
  63. package/src/sinks/drizzle/index.ts +0 -6
  64. package/src/sinks/drizzle/insert.ts +0 -39
  65. package/src/sinks/drizzle/select.ts +0 -44
  66. package/src/sinks/drizzle/transaction.ts +0 -49
  67. package/src/sinks/drizzle/update.ts +0 -47
  68. package/src/sinks/drizzle/utils.ts +0 -36
  69. package/src/sinks/sqlite.test.ts +0 -99
  70. package/src/sinks/sqlite.ts +0 -170
  71. package/src/testing/helper.ts +0 -13
  72. package/src/testing/indexer.ts +0 -35
  73. package/src/testing/setup.ts +0 -59
  74. package/src/testing/vcr.ts +0 -54
package/src/indexer.ts CHANGED
@@ -1,11 +1,17 @@
1
- import type {
2
- Client,
3
- Cursor,
4
- DataFinality,
5
- StreamConfig,
6
- StreamDataOptions,
7
- StreamDataRequest,
8
- StreamDataResponse,
1
+ import {
2
+ type Client,
3
+ ClientError,
4
+ type Cursor,
5
+ type DataFinality,
6
+ type Finalize,
7
+ type Heartbeat,
8
+ type Invalidate,
9
+ Status,
10
+ type StreamConfig,
11
+ type StreamDataOptions,
12
+ type StreamDataRequest,
13
+ type StreamDataResponse,
14
+ type SystemMessage,
9
15
  } from "@apibara/protocol";
10
16
  import consola from "consola";
11
17
  import {
@@ -16,6 +22,7 @@ import {
16
22
  } from "hookable";
17
23
 
18
24
  import assert from "node:assert";
25
+ import { type MiddlewareFunction, type NextFunction, compose } from "./compose";
19
26
  import {
20
27
  type IndexerContext,
21
28
  indexerAsyncContext,
@@ -23,7 +30,10 @@ import {
23
30
  } from "./context";
24
31
  import { tracer } from "./otel";
25
32
  import type { IndexerPlugin } from "./plugins";
26
- import { type Sink, defaultSink } from "./sink";
33
+
34
+ export type UseMiddlewareFunction = (
35
+ fn: MiddlewareFunction<IndexerContext>,
36
+ ) => void;
27
37
 
28
38
  export interface IndexerHooks<TFilter, TBlock> {
29
39
  "run:before": () => void;
@@ -35,7 +45,9 @@ export interface IndexerHooks<TFilter, TBlock> {
35
45
  request: StreamDataRequest<TFilter>;
36
46
  options: StreamDataOptions;
37
47
  }) => void;
38
- "connect:after": () => void;
48
+ "connect:after": ({
49
+ request,
50
+ }: { request: StreamDataRequest<TFilter> }) => void;
39
51
  "connect:factory": ({
40
52
  request,
41
53
  endCursor,
@@ -43,45 +55,23 @@ export interface IndexerHooks<TFilter, TBlock> {
43
55
  request: StreamDataRequest<TFilter>;
44
56
  endCursor?: Cursor;
45
57
  }) => void;
46
- "handler:before": ({
47
- block,
48
- finality,
49
- endCursor,
50
- }: {
51
- block: TBlock;
52
- finality: DataFinality;
53
- endCursor?: Cursor;
54
- }) => void;
55
- "handler:after": ({
56
- block,
57
- finality,
58
- endCursor,
59
- }: {
60
- block: TBlock;
61
- finality: DataFinality;
62
- endCursor?: Cursor;
63
- }) => void;
64
- "transaction:commit": ({
65
- finality,
66
- endCursor,
67
- }: {
68
- finality: DataFinality;
69
- endCursor?: Cursor;
70
- }) => void;
71
- "handler:exception": ({ error }: { error: Error }) => void;
58
+ "handler:middleware": ({ use }: { use: UseMiddlewareFunction }) => void;
72
59
  message: ({ message }: { message: StreamDataResponse<TBlock> }) => void;
60
+ "message:invalidate": ({ message }: { message: Invalidate }) => void;
61
+ "message:finalize": ({ message }: { message: Finalize }) => void;
62
+ "message:heartbeat": ({ message }: { message: Heartbeat }) => void;
63
+ "message:systemMessage": ({ message }: { message: SystemMessage }) => void;
73
64
  }
74
65
 
75
- export interface IndexerConfig<TFilter, TBlock, TTxnParams> {
66
+ export interface IndexerConfig<TFilter, TBlock> {
76
67
  streamUrl: string;
77
68
  filter: TFilter;
78
69
  finality?: DataFinality;
79
70
  startingCursor?: Cursor;
80
- sink?: Sink<TTxnParams>;
81
71
  factory?: ({
82
72
  block,
83
73
  context,
84
- }: { block: TBlock; context: IndexerContext<TTxnParams> }) => Promise<{
74
+ }: { block: TBlock; context: IndexerContext }) => Promise<{
85
75
  filter?: TFilter;
86
76
  }>;
87
77
  transform: (args: {
@@ -89,40 +79,40 @@ export interface IndexerConfig<TFilter, TBlock, TTxnParams> {
89
79
  cursor?: Cursor | undefined;
90
80
  endCursor?: Cursor | undefined;
91
81
  finality: DataFinality;
92
- context: IndexerContext<TTxnParams>;
82
+ context: IndexerContext;
93
83
  }) => Promise<void>;
94
84
  hooks?: NestedHooks<IndexerHooks<TFilter, TBlock>>;
95
- plugins?: ReadonlyArray<IndexerPlugin<TFilter, TBlock, TTxnParams>>;
85
+ plugins?: ReadonlyArray<IndexerPlugin<TFilter, TBlock>>;
96
86
  debug?: boolean;
97
87
  }
98
88
 
99
- export interface IndexerWithStreamConfig<TFilter, TBlock, TTxnParams>
100
- extends IndexerConfig<TFilter, TBlock, TTxnParams> {
89
+ export interface IndexerWithStreamConfig<TFilter, TBlock>
90
+ extends IndexerConfig<TFilter, TBlock> {
101
91
  streamConfig: StreamConfig<TFilter, TBlock>;
102
92
  }
103
93
 
104
94
  export function defineIndexer<TFilter, TBlock>(
105
95
  streamConfig: StreamConfig<TFilter, TBlock>,
106
96
  ) {
107
- return <TTxnParams>(
108
- config: IndexerConfig<TFilter, TBlock, TTxnParams>,
109
- ): IndexerWithStreamConfig<TFilter, TBlock, TTxnParams> => ({
97
+ return (
98
+ config: IndexerConfig<TFilter, TBlock>,
99
+ ): IndexerWithStreamConfig<TFilter, TBlock> => ({
110
100
  streamConfig,
111
101
  ...config,
112
102
  });
113
103
  }
114
104
 
115
- export interface Indexer<TFilter, TBlock, TTxnParams> {
105
+ export interface Indexer<TFilter, TBlock> {
116
106
  streamConfig: StreamConfig<TFilter, TBlock>;
117
- options: IndexerConfig<TFilter, TBlock, TTxnParams>;
107
+ options: IndexerConfig<TFilter, TBlock>;
118
108
  hooks: Hookable<IndexerHooks<TFilter, TBlock>>;
119
109
  }
120
110
 
121
- export function createIndexer<TFilter, TBlock, TTxnParams>({
111
+ export function createIndexer<TFilter, TBlock>({
122
112
  streamConfig,
123
113
  ...options
124
- }: IndexerWithStreamConfig<TFilter, TBlock, TTxnParams>) {
125
- const indexer: Indexer<TFilter, TBlock, TTxnParams> = {
114
+ }: IndexerWithStreamConfig<TFilter, TBlock>) {
115
+ const indexer: Indexer<TFilter, TBlock> = {
126
116
  options,
127
117
  streamConfig,
128
118
  hooks: createHooks<IndexerHooks<TFilter, TBlock>>(),
@@ -141,16 +131,75 @@ export function createIndexer<TFilter, TBlock, TTxnParams>({
141
131
  return indexer;
142
132
  }
143
133
 
144
- export async function run<TFilter, TBlock, TTxnParams>(
134
+ export interface ReconnectOptions {
135
+ maxRetries?: number;
136
+ retryDelay?: number;
137
+ maxWait?: number;
138
+ }
139
+
140
+ export async function runWithReconnect<TFilter, TBlock>(
145
141
  client: Client<TFilter, TBlock>,
146
- indexer: Indexer<TFilter, TBlock, TTxnParams>,
142
+ indexer: Indexer<TFilter, TBlock>,
143
+ options: ReconnectOptions = {},
147
144
  ) {
148
- await indexerAsyncContext.callAsync({}, async () => {
149
- const context = useIndexerContext<TTxnParams>();
145
+ let retryCount = 0;
146
+
147
+ const maxRetries = options.maxRetries ?? 10;
148
+ const retryDelay = options.retryDelay ?? 1_000;
149
+ const maxWait = options.maxWait ?? 30_000;
150
150
 
151
- const sink = indexer.options.sink ?? defaultSink();
151
+ const runOptions: RunOptions = {
152
+ onConnect() {
153
+ retryCount = 0;
154
+ },
155
+ };
152
156
 
153
- context.sink = sink as Sink<TTxnParams>;
157
+ while (true) {
158
+ try {
159
+ await run(client, indexer, runOptions);
160
+ return;
161
+ } catch (error) {
162
+ // Only reconnect on internal/server errors.
163
+ // All other errors should be rethrown.
164
+
165
+ retryCount++;
166
+
167
+ if (error instanceof ClientError) {
168
+ if (error.code === Status.INTERNAL) {
169
+ if (retryCount < maxRetries) {
170
+ consola.error(
171
+ "Internal server error, reconnecting...",
172
+ error.message,
173
+ );
174
+
175
+ // Add jitter to the retry delay to avoid all clients retrying at the same time.
176
+ const delay = Math.random() * (retryDelay * 0.2) + retryDelay;
177
+ await new Promise((resolve) =>
178
+ setTimeout(resolve, Math.min(retryCount * delay, maxWait)),
179
+ );
180
+
181
+ continue;
182
+ }
183
+ }
184
+ }
185
+
186
+ throw error;
187
+ }
188
+ }
189
+ }
190
+
191
+ export interface RunOptions {
192
+ onConnect?: () => void | Promise<void>;
193
+ }
194
+
195
+ export async function run<TFilter, TBlock>(
196
+ client: Client<TFilter, TBlock>,
197
+ indexer: Indexer<TFilter, TBlock>,
198
+ runOptions: RunOptions = {},
199
+ ) {
200
+ await indexerAsyncContext.callAsync({}, async () => {
201
+ const context = useIndexerContext();
202
+ const middleware = await registerMiddleware(indexer);
154
203
 
155
204
  await indexer.hooks.callHook("run:before");
156
205
 
@@ -182,7 +231,9 @@ export async function run<TFilter, TBlock, TTxnParams>(
182
231
  StreamDataResponse<TBlock>
183
232
  > = client.streamData(request, options)[Symbol.asyncIterator]();
184
233
 
185
- await indexer.hooks.callHook("connect:after");
234
+ await indexer.hooks.callHook("connect:after", { request });
235
+
236
+ let onConnectCalled = false;
186
237
 
187
238
  while (true) {
188
239
  const { value: message, done } = await stream.next();
@@ -191,6 +242,13 @@ export async function run<TFilter, TBlock, TTxnParams>(
191
242
  break;
192
243
  }
193
244
 
245
+ if (!onConnectCalled) {
246
+ onConnectCalled = true;
247
+ if (runOptions.onConnect) {
248
+ await runOptions.onConnect();
249
+ }
250
+ }
251
+
194
252
  await indexer.hooks.callHook("message", { message });
195
253
 
196
254
  switch (message._tag) {
@@ -199,117 +257,138 @@ export async function run<TFilter, TBlock, TTxnParams>(
199
257
  const blocks = message.data.data;
200
258
  const { cursor, endCursor, finality } = message.data;
201
259
 
202
- await sink.transaction(
203
- { cursor, endCursor, finality },
204
- async (txn) => {
205
- // attach transaction to context
206
- context.sinkTransaction = txn as TTxnParams;
260
+ context.cursor = cursor;
261
+ context.endCursor = endCursor;
262
+ context.finality = finality;
207
263
 
208
- let block: TBlock | null;
264
+ await middleware(context, async () => {
265
+ let block: TBlock | null;
209
266
 
210
- // when factory mode
211
- if (isFactoryMode) {
212
- assert(indexer.options.factory !== undefined);
267
+ // when factory mode
268
+ if (isFactoryMode) {
269
+ assert(indexer.options.factory !== undefined);
213
270
 
214
- const [factoryBlock, mainBlock] = blocks;
271
+ const [factoryBlock, mainBlock] = blocks;
215
272
 
216
- block = mainBlock;
273
+ block = mainBlock;
217
274
 
218
- if (factoryBlock !== null) {
219
- const { filter } = await indexer.options.factory({
220
- block: factoryBlock,
221
- context,
222
- });
223
-
224
- // write returned data from factory function if filter is not defined
225
- if (filter) {
226
- // when filter is defined
227
- // merge old and new filters
228
- mainFilter = indexer.streamConfig.mergeFilter(
229
- mainFilter,
230
- filter,
231
- );
275
+ if (factoryBlock !== null) {
276
+ const { filter } = await indexer.options.factory({
277
+ block: factoryBlock,
278
+ context,
279
+ });
232
280
 
233
- // create request with new filters
234
- const request = indexer.streamConfig.Request.make({
235
- filter: [indexer.options.filter, mainFilter],
236
- finality: indexer.options.finality,
237
- startingCursor: cursor,
238
- });
281
+ // write returned data from factory function if filter is not defined
282
+ if (filter) {
283
+ // when filter is defined
284
+ // merge old and new filters
285
+ mainFilter = indexer.streamConfig.mergeFilter(
286
+ mainFilter,
287
+ filter,
288
+ );
289
+
290
+ // create request with new filters
291
+ const request = indexer.streamConfig.Request.make({
292
+ filter: [indexer.options.filter, mainFilter],
293
+ finality: indexer.options.finality,
294
+ startingCursor: cursor,
295
+ });
239
296
 
240
- await indexer.hooks.callHook("connect:factory", {
241
- request,
242
- endCursor,
243
- });
297
+ await indexer.hooks.callHook("connect:factory", {
298
+ request,
299
+ endCursor,
300
+ });
244
301
 
245
- // create new stream with new request
246
- stream = client
247
- .streamData(request, options)
248
- [Symbol.asyncIterator]();
302
+ // create new stream with new request
303
+ stream = client
304
+ .streamData(request, options)
305
+ [Symbol.asyncIterator]();
249
306
 
250
- const { value: message } = await stream.next();
307
+ const { value: message } = await stream.next();
251
308
 
252
- assert(message._tag === "data");
309
+ assert(message._tag === "data");
253
310
 
254
- const [_factoryBlock, _block] = message.data.data;
311
+ const [_factoryBlock, _block] = message.data.data;
255
312
 
256
- block = _block;
257
- }
313
+ block = _block;
258
314
  }
259
- } else {
260
- // when not in factory mode
261
- block = blocks[0];
262
315
  }
263
-
264
- // if block is not null
265
- if (block) {
266
- await tracer.startActiveSpan("handler", async (span) => {
267
- await indexer.hooks.callHook("handler:before", {
268
- block,
269
- endCursor,
270
- finality,
271
- });
272
-
273
- try {
274
- await indexer.options.transform({
275
- block,
276
- cursor,
277
- endCursor,
278
- finality,
279
- context,
280
- });
281
- await indexer.hooks.callHook("handler:after", {
282
- block,
283
- finality,
284
- endCursor,
285
- });
286
- } catch (error) {
287
- assert(error instanceof Error);
288
- await indexer.hooks.callHook("handler:exception", {
289
- error,
290
- });
291
- throw error;
292
- }
293
-
294
- span.end();
316
+ } else {
317
+ // when not in factory mode
318
+ block = blocks[0];
319
+ }
320
+
321
+ // if block is not null
322
+ if (block) {
323
+ await tracer.startActiveSpan("handler", async (span) => {
324
+ await indexer.options.transform({
325
+ block,
326
+ cursor,
327
+ endCursor,
328
+ finality,
329
+ context,
295
330
  });
296
- }
297
- },
298
- );
299
- await indexer.hooks.callHook("transaction:commit", {
300
- finality,
301
- endCursor,
331
+
332
+ span.end();
333
+ });
334
+ }
302
335
  });
336
+
303
337
  span.end();
304
338
  });
339
+
340
+ context.cursor = undefined;
341
+ context.endCursor = undefined;
342
+ context.finality = undefined;
343
+
305
344
  break;
306
345
  }
307
346
  case "invalidate": {
308
347
  await tracer.startActiveSpan("message invalidate", async (span) => {
309
- await sink.invalidate(message.invalidate.cursor);
348
+ await indexer.hooks.callHook("message:invalidate", { message });
349
+ span.end();
350
+ });
351
+ break;
352
+ }
353
+ case "finalize": {
354
+ await tracer.startActiveSpan("message finalize", async (span) => {
355
+ await indexer.hooks.callHook("message:finalize", { message });
356
+ span.end();
310
357
  });
311
358
  break;
312
359
  }
360
+ case "heartbeat": {
361
+ await tracer.startActiveSpan("message heartbeat", async (span) => {
362
+ await indexer.hooks.callHook("message:heartbeat", { message });
363
+ span.end();
364
+ });
365
+ break;
366
+ }
367
+ case "systemMessage": {
368
+ await tracer.startActiveSpan(
369
+ "message systemMessage",
370
+ async (span) => {
371
+ switch (message.systemMessage.output?._tag) {
372
+ case "stderr": {
373
+ consola.warn(message.systemMessage.output.stderr);
374
+ break;
375
+ }
376
+ case "stdout": {
377
+ consola.info(message.systemMessage.output.stdout);
378
+ break;
379
+ }
380
+ default: {
381
+ }
382
+ }
383
+
384
+ await indexer.hooks.callHook("message:systemMessage", {
385
+ message,
386
+ });
387
+ span.end();
388
+ },
389
+ );
390
+ break;
391
+ }
313
392
  default: {
314
393
  consola.warn("unexpected message", message);
315
394
  throw new Error("not implemented");
@@ -320,3 +399,24 @@ export async function run<TFilter, TBlock, TTxnParams>(
320
399
  }
321
400
  });
322
401
  }
402
+
403
+ async function registerMiddleware<TFilter, TBlock>(
404
+ indexer: Indexer<TFilter, TBlock>,
405
+ ): Promise<MiddlewareFunction<IndexerContext>> {
406
+ const middleware: MiddlewareFunction<IndexerContext>[] = [];
407
+ const use = (fn: MiddlewareFunction<IndexerContext>) => {
408
+ middleware.push(fn);
409
+ };
410
+
411
+ await indexer.hooks.callHook("handler:middleware", { use });
412
+
413
+ const composed = compose(middleware);
414
+
415
+ // Return a named function to help debugging
416
+ return async function _composedIndexerMiddleware(
417
+ context: IndexerContext,
418
+ next?: NextFunction,
419
+ ) {
420
+ await composed(context, next);
421
+ };
422
+ }
@@ -0,0 +1,135 @@
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 { type IndexerPlugin, defineIndexerPlugin } from "../plugins";
12
+
13
+ export type MockMessagesOptions = {
14
+ invalidate?: {
15
+ invalidateFromIndex: number;
16
+ invalidateTriggerIndex: number;
17
+ };
18
+ finalize?: {
19
+ finalizeToIndex: number;
20
+ finalizeTriggerIndex: number;
21
+ };
22
+ };
23
+
24
+ export function generateMockMessages(
25
+ count = 10,
26
+ options?: MockMessagesOptions,
27
+ ): MockStreamResponse[] {
28
+ const invalidateAt = options?.invalidate;
29
+ const finalizeAt = options?.finalize;
30
+ const messages: MockStreamResponse[] = [];
31
+
32
+ for (let i = 0; i < count; i++) {
33
+ if (invalidateAt && i === invalidateAt.invalidateTriggerIndex) {
34
+ messages.push({
35
+ _tag: "invalidate",
36
+ invalidate: {
37
+ cursor: {
38
+ orderKey: BigInt(5_000_000 + invalidateAt.invalidateFromIndex),
39
+ },
40
+ },
41
+ } as Invalidate);
42
+ } else if (finalizeAt && i === finalizeAt.finalizeTriggerIndex) {
43
+ messages.push({
44
+ _tag: "finalize",
45
+ finalize: {
46
+ cursor: {
47
+ orderKey: BigInt(5_000_000 + finalizeAt.finalizeToIndex),
48
+ },
49
+ },
50
+ } as Finalize);
51
+ } else {
52
+ messages.push({
53
+ _tag: "data",
54
+ data: {
55
+ cursor: { orderKey: BigInt(5_000_000 + i - 1) },
56
+ finality: "accepted",
57
+ data: [{ data: `${5_000_000 + i}` }],
58
+ endCursor: { orderKey: BigInt(5_000_000 + i) },
59
+ },
60
+ });
61
+ }
62
+ }
63
+
64
+ return messages;
65
+ }
66
+
67
+ export function getMockIndexer({
68
+ plugins,
69
+ override,
70
+ }: {
71
+ plugins?: ReadonlyArray<IndexerPlugin<MockFilter, MockBlock>>;
72
+ override?: Partial<IndexerConfig<MockFilter, MockBlock>>;
73
+ } = {}) {
74
+ return createIndexer(
75
+ defineIndexer(MockStream)({
76
+ streamUrl: "https://sepolia.ethereum.a5a.ch",
77
+ finality: "accepted",
78
+ filter: {},
79
+ async transform({ block: { data }, context }) {},
80
+ plugins,
81
+ ...override,
82
+ }),
83
+ );
84
+ }
85
+
86
+ export type MockRet = {
87
+ data: string;
88
+ };
89
+
90
+ /**
91
+ * A mock sink used for testing. The indexer function can write to the output array.
92
+ * The indexer context is optionally written to the metadata object.
93
+ */
94
+ export function mockSink<TFilter, TBlock>({
95
+ output,
96
+ metadata,
97
+ }: { output: unknown[]; metadata?: Record<string, unknown> }) {
98
+ return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
99
+ indexer.hooks.hook("connect:before", ({ request }) => {
100
+ if (metadata?.lastCursor && isCursor(metadata.lastCursor)) {
101
+ request.startingCursor = metadata.lastCursor;
102
+ }
103
+
104
+ if (metadata?.lastFilter) {
105
+ request.filter[1] = metadata.lastFilter as TFilter;
106
+ }
107
+ });
108
+
109
+ indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
110
+ if (request.filter[1]) {
111
+ if (metadata) {
112
+ metadata.lastCursor = endCursor;
113
+ metadata.lastFilter = request.filter[1];
114
+ }
115
+ }
116
+ });
117
+
118
+ indexer.hooks.hook("handler:middleware", ({ use }) => {
119
+ use(async (context, next) => {
120
+ context.output = output;
121
+ await next();
122
+ context.output = null;
123
+
124
+ if (metadata) {
125
+ metadata.lastCursor = context.endCursor;
126
+ }
127
+ });
128
+ });
129
+ });
130
+ }
131
+
132
+ export function useMockSink(): { output: unknown[] } {
133
+ const context = useIndexerContext();
134
+ return { output: context.output };
135
+ }
@@ -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
  }
@@ -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";