@apibara/indexer 2.1.0-beta.4 → 2.1.0-beta.40
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.
- package/dist/index.cjs +81 -27
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +71 -17
- package/dist/index.mjs.map +1 -0
- package/dist/internal/index.cjs +1 -0
- package/dist/internal/index.cjs.map +1 -0
- package/dist/internal/index.mjs +1 -0
- package/dist/internal/index.mjs.map +1 -0
- package/dist/internal/plugins.cjs +5 -5
- package/dist/internal/plugins.cjs.map +1 -0
- package/dist/internal/plugins.d.cts +1 -1
- package/dist/internal/plugins.d.mts +1 -1
- package/dist/internal/plugins.d.ts +1 -1
- package/dist/internal/plugins.mjs +3 -3
- package/dist/internal/plugins.mjs.map +1 -0
- package/dist/internal/testing.cjs +25 -10
- package/dist/internal/testing.cjs.map +1 -0
- package/dist/internal/testing.d.cts +5 -3
- package/dist/internal/testing.d.mts +5 -3
- package/dist/internal/testing.d.ts +5 -3
- package/dist/internal/testing.mjs +23 -8
- package/dist/internal/testing.mjs.map +1 -0
- package/dist/plugins/index.cjs +4 -4
- package/dist/plugins/index.cjs.map +1 -0
- package/dist/plugins/index.d.cts +2 -2
- package/dist/plugins/index.d.mts +2 -2
- package/dist/plugins/index.d.ts +2 -2
- package/dist/plugins/index.mjs +4 -4
- package/dist/plugins/index.mjs.map +1 -0
- package/dist/shared/{indexer.2416906c.cjs → indexer.03c9f151.cjs} +8 -5
- package/dist/shared/indexer.03c9f151.cjs.map +1 -0
- package/dist/shared/{indexer.ff25c953.mjs → indexer.2673dcb1.mjs} +7 -4
- package/dist/shared/indexer.2673dcb1.mjs.map +1 -0
- package/dist/shared/{indexer.077335f3.cjs → indexer.479ae593.cjs} +6 -0
- package/dist/shared/indexer.479ae593.cjs.map +1 -0
- package/dist/shared/{indexer.a55ad619.mjs → indexer.75773ef1.mjs} +6 -1
- package/dist/shared/indexer.75773ef1.mjs.map +1 -0
- package/dist/shared/{indexer.fedcd831.d.cts → indexer.806c605c.d.cts} +15 -16
- package/dist/shared/{indexer.fedcd831.d.mts → indexer.806c605c.d.mts} +15 -16
- package/dist/shared/{indexer.fedcd831.d.ts → indexer.806c605c.d.ts} +15 -16
- package/dist/testing/index.cjs +19 -10
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +11 -7
- package/dist/testing/index.d.mts +11 -7
- package/dist/testing/index.d.ts +11 -7
- package/dist/testing/index.mjs +20 -11
- package/dist/testing/index.mjs.map +1 -0
- package/dist/vcr/index.cjs +3 -1
- package/dist/vcr/index.cjs.map +1 -0
- package/dist/vcr/index.d.cts +1 -1
- package/dist/vcr/index.d.mts +1 -1
- package/dist/vcr/index.d.ts +1 -1
- package/dist/vcr/index.mjs +3 -1
- package/dist/vcr/index.mjs.map +1 -0
- package/package.json +3 -3
- package/src/indexer.ts +78 -28
- package/src/internal/testing.ts +34 -11
- package/src/otel.ts +29 -2
- package/src/plugins/context.ts +1 -1
- package/src/plugins/logger.ts +11 -2
- package/src/testing/index.ts +30 -6
- package/dist/shared/indexer.601ceab0.cjs +0 -7
- package/dist/shared/indexer.9b21ddd2.mjs +0 -5
- package/src/compose.test.ts +0 -76
- package/src/indexer.test.ts +0 -430
package/src/indexer.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type Client,
|
|
3
3
|
ClientError,
|
|
4
|
+
type CreateClientOptions,
|
|
4
5
|
type Cursor,
|
|
5
6
|
type DataFinality,
|
|
7
|
+
type DataProduction,
|
|
6
8
|
type Finalize,
|
|
7
|
-
type Heartbeat,
|
|
8
9
|
type Invalidate,
|
|
10
|
+
ServerError,
|
|
9
11
|
Status,
|
|
10
12
|
type StreamConfig,
|
|
11
13
|
type StreamDataOptions,
|
|
@@ -28,14 +30,16 @@ import {
|
|
|
28
30
|
indexerAsyncContext,
|
|
29
31
|
useIndexerContext,
|
|
30
32
|
} from "./context";
|
|
31
|
-
import {
|
|
33
|
+
import { createIndexerMetrics, createTracer } from "./otel";
|
|
32
34
|
import type { IndexerPlugin } from "./plugins";
|
|
35
|
+
import { useInternalContext } from "./plugins/context";
|
|
33
36
|
|
|
34
37
|
export type UseMiddlewareFunction = (
|
|
35
38
|
fn: MiddlewareFunction<IndexerContext>,
|
|
36
39
|
) => void;
|
|
37
40
|
|
|
38
41
|
export interface IndexerHooks<TFilter, TBlock> {
|
|
42
|
+
"plugins:init": () => void;
|
|
39
43
|
"run:before": () => void;
|
|
40
44
|
"run:after": () => void;
|
|
41
45
|
"connect:before": ({
|
|
@@ -47,7 +51,9 @@ export interface IndexerHooks<TFilter, TBlock> {
|
|
|
47
51
|
}) => void;
|
|
48
52
|
"connect:after": ({
|
|
49
53
|
request,
|
|
50
|
-
}: {
|
|
54
|
+
}: {
|
|
55
|
+
request: StreamDataRequest<TFilter>;
|
|
56
|
+
}) => void;
|
|
51
57
|
"connect:factory": ({
|
|
52
58
|
request,
|
|
53
59
|
endCursor,
|
|
@@ -59,7 +65,7 @@ export interface IndexerHooks<TFilter, TBlock> {
|
|
|
59
65
|
message: ({ message }: { message: StreamDataResponse<TBlock> }) => void;
|
|
60
66
|
"message:invalidate": ({ message }: { message: Invalidate }) => void;
|
|
61
67
|
"message:finalize": ({ message }: { message: Finalize }) => void;
|
|
62
|
-
"message:heartbeat": (
|
|
68
|
+
"message:heartbeat": () => void;
|
|
63
69
|
"message:systemMessage": ({ message }: { message: SystemMessage }) => void;
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -77,23 +83,24 @@ export type IndexerStartingCursor =
|
|
|
77
83
|
startingBlock?: never;
|
|
78
84
|
};
|
|
79
85
|
|
|
86
|
+
export type HandlerArgs<TBlock> = {
|
|
87
|
+
block: TBlock;
|
|
88
|
+
cursor?: Cursor | undefined;
|
|
89
|
+
endCursor?: Cursor | undefined;
|
|
90
|
+
finality: DataFinality;
|
|
91
|
+
production: DataProduction;
|
|
92
|
+
context: IndexerContext;
|
|
93
|
+
};
|
|
94
|
+
|
|
80
95
|
export type IndexerConfig<TFilter, TBlock> = {
|
|
81
96
|
streamUrl: string;
|
|
82
97
|
filter: TFilter;
|
|
83
98
|
finality?: DataFinality;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
context,
|
|
87
|
-
}: { block: TBlock; context: IndexerContext }) => Promise<{
|
|
99
|
+
clientOptions?: CreateClientOptions;
|
|
100
|
+
factory?: (args: HandlerArgs<TBlock>) => Promise<{
|
|
88
101
|
filter?: TFilter;
|
|
89
102
|
}>;
|
|
90
|
-
transform: (args:
|
|
91
|
-
block: TBlock;
|
|
92
|
-
cursor?: Cursor | undefined;
|
|
93
|
-
endCursor?: Cursor | undefined;
|
|
94
|
-
finality: DataFinality;
|
|
95
|
-
context: IndexerContext;
|
|
96
|
-
}) => Promise<void>;
|
|
103
|
+
transform: (args: HandlerArgs<TBlock>) => Promise<void>;
|
|
97
104
|
hooks?: NestedHooks<IndexerHooks<TFilter, TBlock>>;
|
|
98
105
|
plugins?: ReadonlyArray<IndexerPlugin<TFilter, TBlock>>;
|
|
99
106
|
debug?: boolean;
|
|
@@ -179,13 +186,18 @@ export async function runWithReconnect<TFilter, TBlock>(
|
|
|
179
186
|
|
|
180
187
|
retryCount++;
|
|
181
188
|
|
|
182
|
-
if (error instanceof ClientError) {
|
|
189
|
+
if (error instanceof ClientError || error instanceof ServerError) {
|
|
190
|
+
const isServerError = error instanceof ServerError;
|
|
191
|
+
|
|
183
192
|
if (error.code === Status.INTERNAL) {
|
|
184
193
|
if (retryCount < maxRetries) {
|
|
185
194
|
consola.error(
|
|
186
|
-
|
|
187
|
-
|
|
195
|
+
`Internal ${isServerError ? "server" : "client"} error: ${
|
|
196
|
+
error.message
|
|
197
|
+
}`,
|
|
188
198
|
);
|
|
199
|
+
consola.start("Reconnecting...");
|
|
200
|
+
console.log();
|
|
189
201
|
|
|
190
202
|
// Add jitter to the retry delay to avoid all clients retrying at the same time.
|
|
191
203
|
const delay = Math.random() * (retryDelay * 0.2) + retryDelay;
|
|
@@ -214,10 +226,22 @@ export async function run<TFilter, TBlock>(
|
|
|
214
226
|
) {
|
|
215
227
|
await indexerAsyncContext.callAsync({}, async () => {
|
|
216
228
|
const context = useIndexerContext();
|
|
229
|
+
|
|
230
|
+
if (indexer.options.debug) {
|
|
231
|
+
context.debug = true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
await indexer.hooks.callHook("plugins:init");
|
|
235
|
+
|
|
217
236
|
const middleware = await registerMiddleware(indexer);
|
|
218
237
|
|
|
238
|
+
const indexerMetrics = createIndexerMetrics();
|
|
239
|
+
const tracer = createTracer();
|
|
240
|
+
|
|
219
241
|
await indexer.hooks.callHook("run:before");
|
|
220
242
|
|
|
243
|
+
const { indexerName: indexerId } = useInternalContext();
|
|
244
|
+
|
|
221
245
|
const isFactoryMode = indexer.options.factory !== undefined;
|
|
222
246
|
|
|
223
247
|
// Give priority to startingCursor over startingBlock.
|
|
@@ -235,13 +259,13 @@ export async function run<TFilter, TBlock>(
|
|
|
235
259
|
}
|
|
236
260
|
|
|
237
261
|
// if factory mode we add a empty filter at the end of the filter array.
|
|
238
|
-
const request =
|
|
262
|
+
const request = {
|
|
239
263
|
filter: isFactoryMode
|
|
240
264
|
? [indexer.options.filter, {} as TFilter]
|
|
241
265
|
: [indexer.options.filter],
|
|
242
266
|
finality: indexer.options.finality,
|
|
243
267
|
startingCursor,
|
|
244
|
-
}
|
|
268
|
+
} as StreamDataRequest<TFilter>;
|
|
245
269
|
|
|
246
270
|
const options: StreamDataOptions = {};
|
|
247
271
|
|
|
@@ -282,17 +306,25 @@ export async function run<TFilter, TBlock>(
|
|
|
282
306
|
case "data": {
|
|
283
307
|
await tracer.startActiveSpan("message data", async (span) => {
|
|
284
308
|
const blocks = message.data.data;
|
|
285
|
-
const { cursor, endCursor, finality } = message.data;
|
|
309
|
+
const { cursor, endCursor, finality, production } = message.data;
|
|
286
310
|
|
|
287
311
|
context.cursor = cursor;
|
|
288
312
|
context.endCursor = endCursor;
|
|
289
313
|
context.finality = finality;
|
|
290
314
|
|
|
315
|
+
// Record current block number being processed
|
|
316
|
+
indexerMetrics.currentBlockGauge.record(
|
|
317
|
+
Number(endCursor?.orderKey),
|
|
318
|
+
{
|
|
319
|
+
indexer_id: indexerId,
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
|
|
291
323
|
await middleware(context, async () => {
|
|
292
324
|
let block: TBlock | null;
|
|
293
325
|
|
|
294
326
|
// when factory mode
|
|
295
|
-
if (isFactoryMode) {
|
|
327
|
+
if (isFactoryMode && finality !== "pending") {
|
|
296
328
|
assert(indexer.options.factory !== undefined);
|
|
297
329
|
|
|
298
330
|
const [factoryBlock, mainBlock] = blocks;
|
|
@@ -302,6 +334,10 @@ export async function run<TFilter, TBlock>(
|
|
|
302
334
|
if (factoryBlock !== null) {
|
|
303
335
|
const { filter } = await indexer.options.factory({
|
|
304
336
|
block: factoryBlock,
|
|
337
|
+
cursor,
|
|
338
|
+
endCursor,
|
|
339
|
+
finality,
|
|
340
|
+
production,
|
|
305
341
|
context,
|
|
306
342
|
});
|
|
307
343
|
|
|
@@ -315,11 +351,11 @@ export async function run<TFilter, TBlock>(
|
|
|
315
351
|
);
|
|
316
352
|
|
|
317
353
|
// create request with new filters
|
|
318
|
-
const request =
|
|
354
|
+
const request = {
|
|
319
355
|
filter: [indexer.options.filter, mainFilter],
|
|
320
356
|
finality: indexer.options.finality,
|
|
321
357
|
startingCursor: cursor,
|
|
322
|
-
}
|
|
358
|
+
} as StreamDataRequest<TFilter>;
|
|
323
359
|
|
|
324
360
|
await indexer.hooks.callHook("connect:factory", {
|
|
325
361
|
request,
|
|
@@ -353,6 +389,7 @@ export async function run<TFilter, TBlock>(
|
|
|
353
389
|
cursor,
|
|
354
390
|
endCursor,
|
|
355
391
|
finality,
|
|
392
|
+
production,
|
|
356
393
|
context,
|
|
357
394
|
});
|
|
358
395
|
|
|
@@ -364,6 +401,11 @@ export async function run<TFilter, TBlock>(
|
|
|
364
401
|
span.end();
|
|
365
402
|
});
|
|
366
403
|
|
|
404
|
+
// Record processed block metric
|
|
405
|
+
indexerMetrics.processedBlockCounter.add(1, {
|
|
406
|
+
indexer_id: indexerId,
|
|
407
|
+
});
|
|
408
|
+
|
|
367
409
|
context.cursor = undefined;
|
|
368
410
|
context.endCursor = undefined;
|
|
369
411
|
context.finality = undefined;
|
|
@@ -372,21 +414,29 @@ export async function run<TFilter, TBlock>(
|
|
|
372
414
|
}
|
|
373
415
|
case "invalidate": {
|
|
374
416
|
await tracer.startActiveSpan("message invalidate", async (span) => {
|
|
375
|
-
|
|
417
|
+
// Record reorg metric
|
|
418
|
+
indexerMetrics.reorgCounter.add(1, {
|
|
419
|
+
indexer_id: indexerId,
|
|
420
|
+
});
|
|
421
|
+
await indexer.hooks.callHook("message:invalidate", {
|
|
422
|
+
message: message.invalidate,
|
|
423
|
+
});
|
|
376
424
|
span.end();
|
|
377
425
|
});
|
|
378
426
|
break;
|
|
379
427
|
}
|
|
380
428
|
case "finalize": {
|
|
381
429
|
await tracer.startActiveSpan("message finalize", async (span) => {
|
|
382
|
-
await indexer.hooks.callHook("message:finalize", {
|
|
430
|
+
await indexer.hooks.callHook("message:finalize", {
|
|
431
|
+
message: message.finalize,
|
|
432
|
+
});
|
|
383
433
|
span.end();
|
|
384
434
|
});
|
|
385
435
|
break;
|
|
386
436
|
}
|
|
387
437
|
case "heartbeat": {
|
|
388
438
|
await tracer.startActiveSpan("message heartbeat", async (span) => {
|
|
389
|
-
await indexer.hooks.callHook("message:heartbeat"
|
|
439
|
+
await indexer.hooks.callHook("message:heartbeat");
|
|
390
440
|
span.end();
|
|
391
441
|
});
|
|
392
442
|
break;
|
|
@@ -409,7 +459,7 @@ export async function run<TFilter, TBlock>(
|
|
|
409
459
|
}
|
|
410
460
|
|
|
411
461
|
await indexer.hooks.callHook("message:systemMessage", {
|
|
412
|
-
message,
|
|
462
|
+
message: message.systemMessage,
|
|
413
463
|
});
|
|
414
464
|
span.end();
|
|
415
465
|
},
|
package/src/internal/testing.ts
CHANGED
|
@@ -5,10 +5,9 @@ import {
|
|
|
5
5
|
MockStream,
|
|
6
6
|
type MockStreamResponse,
|
|
7
7
|
} from "@apibara/protocol/testing";
|
|
8
|
-
|
|
9
8
|
import { useIndexerContext } from "../context";
|
|
10
9
|
import { type IndexerConfig, createIndexer, defineIndexer } from "../indexer";
|
|
11
|
-
import { defineIndexerPlugin } from "../plugins";
|
|
10
|
+
import { defineIndexerPlugin, logger } from "../plugins";
|
|
12
11
|
import { type InternalContext, internalContext } from "./plugins";
|
|
13
12
|
|
|
14
13
|
export type MockMessagesOptions = {
|
|
@@ -20,6 +19,8 @@ export type MockMessagesOptions = {
|
|
|
20
19
|
finalizeToIndex: number;
|
|
21
20
|
finalizeTriggerIndex: number;
|
|
22
21
|
};
|
|
22
|
+
uniqueKey?: boolean;
|
|
23
|
+
baseBlockNumber?: bigint;
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
export function generateMockMessages(
|
|
@@ -30,33 +31,50 @@ export function generateMockMessages(
|
|
|
30
31
|
const finalizeAt = options?.finalize;
|
|
31
32
|
const messages: MockStreamResponse[] = [];
|
|
32
33
|
|
|
34
|
+
const baseBlockNumber = options?.baseBlockNumber ?? BigInt(5_000_000);
|
|
35
|
+
|
|
33
36
|
for (let i = 0; i < count; i++) {
|
|
37
|
+
const currentBlockNumber = baseBlockNumber + BigInt(i);
|
|
38
|
+
const uniqueKey = uniqueKeyFromOrderKey(currentBlockNumber);
|
|
34
39
|
if (invalidateAt && i === invalidateAt.invalidateTriggerIndex) {
|
|
40
|
+
const invalidateToBlock =
|
|
41
|
+
baseBlockNumber + BigInt(invalidateAt.invalidateFromIndex);
|
|
35
42
|
messages.push({
|
|
36
43
|
_tag: "invalidate",
|
|
37
44
|
invalidate: {
|
|
38
45
|
cursor: {
|
|
39
|
-
orderKey:
|
|
46
|
+
orderKey: invalidateToBlock,
|
|
47
|
+
uniqueKey: options?.uniqueKey
|
|
48
|
+
? uniqueKeyFromOrderKey(invalidateToBlock)
|
|
49
|
+
: undefined,
|
|
40
50
|
},
|
|
41
|
-
},
|
|
42
|
-
}
|
|
51
|
+
} as Invalidate,
|
|
52
|
+
});
|
|
43
53
|
} else if (finalizeAt && i === finalizeAt.finalizeTriggerIndex) {
|
|
54
|
+
const fianlizedToBlock =
|
|
55
|
+
baseBlockNumber + BigInt(finalizeAt.finalizeToIndex);
|
|
44
56
|
messages.push({
|
|
45
57
|
_tag: "finalize",
|
|
46
58
|
finalize: {
|
|
47
59
|
cursor: {
|
|
48
|
-
orderKey:
|
|
60
|
+
orderKey: fianlizedToBlock,
|
|
61
|
+
uniqueKey: options?.uniqueKey
|
|
62
|
+
? uniqueKeyFromOrderKey(fianlizedToBlock)
|
|
63
|
+
: undefined,
|
|
49
64
|
},
|
|
50
|
-
},
|
|
51
|
-
}
|
|
65
|
+
} as Finalize,
|
|
66
|
+
});
|
|
52
67
|
} else {
|
|
53
68
|
messages.push({
|
|
54
69
|
_tag: "data",
|
|
55
70
|
data: {
|
|
56
|
-
cursor: { orderKey:
|
|
71
|
+
cursor: { orderKey: currentBlockNumber - 1n },
|
|
57
72
|
finality: "accepted",
|
|
58
|
-
data: [{ data: `${
|
|
59
|
-
endCursor: {
|
|
73
|
+
data: [{ data: `${baseBlockNumber + BigInt(i)}` }],
|
|
74
|
+
endCursor: {
|
|
75
|
+
orderKey: currentBlockNumber,
|
|
76
|
+
uniqueKey: options?.uniqueKey ? uniqueKey : undefined,
|
|
77
|
+
},
|
|
60
78
|
production: "backfill",
|
|
61
79
|
},
|
|
62
80
|
});
|
|
@@ -66,6 +84,10 @@ export function generateMockMessages(
|
|
|
66
84
|
return messages;
|
|
67
85
|
}
|
|
68
86
|
|
|
87
|
+
function uniqueKeyFromOrderKey(orderKey: bigint): `0x${string}` {
|
|
88
|
+
return `0xff00${orderKey.toString()}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
69
91
|
type MockIndexerParams = {
|
|
70
92
|
internalContext?: InternalContext;
|
|
71
93
|
override?: Partial<IndexerConfig<MockFilter, MockBlock>>;
|
|
@@ -82,6 +104,7 @@ export function getMockIndexer(params?: MockIndexerParams) {
|
|
|
82
104
|
filter: {},
|
|
83
105
|
async transform() {},
|
|
84
106
|
plugins: [
|
|
107
|
+
logger(),
|
|
85
108
|
internalContext(
|
|
86
109
|
contextParams ??
|
|
87
110
|
({
|
package/src/otel.ts
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
-
import { trace } from "@opentelemetry/api";
|
|
1
|
+
import { metrics, trace } from "@opentelemetry/api";
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export function createTracer() {
|
|
4
|
+
return trace.getTracer("@apibara/indexer");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createIndexerMetrics() {
|
|
8
|
+
const meter = metrics.getMeter("@apibara/indexer");
|
|
9
|
+
|
|
10
|
+
const currentBlockGauge = meter.createGauge("current_block", {
|
|
11
|
+
description: "Current block number being processed",
|
|
12
|
+
unit: "{block}",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const processedBlockCounter = meter.createCounter("processed_blocks", {
|
|
16
|
+
description: "Number of blocks processed",
|
|
17
|
+
unit: "{blocks}",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const reorgCounter = meter.createCounter("reorgs", {
|
|
21
|
+
description: "Number of reorgs (invalidate messages) received",
|
|
22
|
+
unit: "{reorgs}",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
currentBlockGauge,
|
|
27
|
+
processedBlockCounter,
|
|
28
|
+
reorgCounter,
|
|
29
|
+
};
|
|
30
|
+
}
|
package/src/plugins/context.ts
CHANGED
|
@@ -7,7 +7,7 @@ export function internalContext<TFilter, TBlock, TTxnParams>(
|
|
|
7
7
|
values: Record<string, unknown>,
|
|
8
8
|
) {
|
|
9
9
|
return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
|
|
10
|
-
indexer.hooks.hook("
|
|
10
|
+
indexer.hooks.hook("plugins:init", () => {
|
|
11
11
|
try {
|
|
12
12
|
const ctx = useIndexerContext();
|
|
13
13
|
ctx[INTERNAL_CONTEXT_PROPERTY] = {
|
package/src/plugins/logger.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type ConsolaInstance,
|
|
3
|
+
type ConsolaReporter,
|
|
4
|
+
LogLevels,
|
|
5
|
+
consola,
|
|
6
|
+
} from "consola";
|
|
2
7
|
import { useIndexerContext } from "../context";
|
|
3
8
|
import { defineIndexerPlugin } from "./config";
|
|
4
9
|
|
|
@@ -8,7 +13,7 @@ export function logger<TFilter, TBlock, TTxnParams>({
|
|
|
8
13
|
logger,
|
|
9
14
|
}: { logger?: ConsolaReporter } = {}) {
|
|
10
15
|
return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
|
|
11
|
-
indexer.hooks.hook("
|
|
16
|
+
indexer.hooks.hook("plugins:init", () => {
|
|
12
17
|
const ctx = useIndexerContext();
|
|
13
18
|
|
|
14
19
|
if (logger) {
|
|
@@ -16,6 +21,10 @@ export function logger<TFilter, TBlock, TTxnParams>({
|
|
|
16
21
|
} else {
|
|
17
22
|
ctx.logger = consola.create({});
|
|
18
23
|
}
|
|
24
|
+
|
|
25
|
+
if (ctx.debug) {
|
|
26
|
+
ctx.logger.level = LogLevels.debug;
|
|
27
|
+
}
|
|
19
28
|
});
|
|
20
29
|
});
|
|
21
30
|
}
|
package/src/testing/index.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createAuthenticatedClient } from "@apibara/protocol";
|
|
2
2
|
import ci from "ci-info";
|
|
3
|
-
import { type
|
|
3
|
+
import { type NestedHooks, mergeHooks } from "hookable";
|
|
4
|
+
import { useIndexerContext } from "../context";
|
|
5
|
+
import {
|
|
6
|
+
type IndexerHooks,
|
|
7
|
+
type IndexerWithStreamConfig,
|
|
8
|
+
createIndexer,
|
|
9
|
+
} from "../indexer";
|
|
4
10
|
import { type InternalContext, internalContext } from "../plugins/context";
|
|
5
11
|
import { logger } from "../plugins/logger";
|
|
6
12
|
import type { CassetteOptions, VcrConfig } from "../vcr/config";
|
|
@@ -8,24 +14,36 @@ import { isCassetteAvailable } from "../vcr/helper";
|
|
|
8
14
|
import { record } from "../vcr/record";
|
|
9
15
|
import { replay } from "../vcr/replay";
|
|
10
16
|
|
|
17
|
+
export type VcrResult = Record<string, unknown>;
|
|
18
|
+
|
|
11
19
|
export function createVcr() {
|
|
20
|
+
let result: VcrResult;
|
|
21
|
+
|
|
12
22
|
return {
|
|
13
23
|
async run<TFilter, TBlock>(
|
|
14
24
|
cassetteName: string,
|
|
15
25
|
indexerConfig: IndexerWithStreamConfig<TFilter, TBlock>,
|
|
16
|
-
|
|
26
|
+
config: {
|
|
27
|
+
range: { fromBlock: bigint; toBlock: bigint };
|
|
28
|
+
hooks?: NestedHooks<IndexerHooks<TFilter, TBlock>>;
|
|
29
|
+
},
|
|
17
30
|
) {
|
|
18
31
|
const vcrConfig: VcrConfig = {
|
|
19
32
|
cassetteDir: "cassettes",
|
|
20
33
|
};
|
|
21
34
|
|
|
35
|
+
indexerConfig.hooks = mergeHooks(
|
|
36
|
+
indexerConfig.hooks ?? {},
|
|
37
|
+
config.hooks ?? {},
|
|
38
|
+
);
|
|
39
|
+
|
|
22
40
|
const cassetteOptions: CassetteOptions = {
|
|
23
41
|
name: cassetteName,
|
|
24
42
|
startingCursor: {
|
|
25
|
-
orderKey: range.fromBlock,
|
|
43
|
+
orderKey: config.range.fromBlock,
|
|
26
44
|
},
|
|
27
45
|
endingCursor: {
|
|
28
|
-
orderKey: range.toBlock,
|
|
46
|
+
orderKey: config.range.toBlock,
|
|
29
47
|
},
|
|
30
48
|
};
|
|
31
49
|
|
|
@@ -40,12 +58,16 @@ export function createVcr() {
|
|
|
40
58
|
|
|
41
59
|
const indexer = createIndexer(indexerConfig);
|
|
42
60
|
|
|
61
|
+
indexer.hooks.hook("run:after", () => {
|
|
62
|
+
result = useIndexerContext();
|
|
63
|
+
});
|
|
64
|
+
|
|
43
65
|
if (!isCassetteAvailable(vcrConfig, cassetteName)) {
|
|
44
66
|
if (ci.isCI) {
|
|
45
67
|
throw new Error("Cannot record cassette in CI");
|
|
46
68
|
}
|
|
47
69
|
|
|
48
|
-
const client =
|
|
70
|
+
const client = createAuthenticatedClient(
|
|
49
71
|
indexer.streamConfig,
|
|
50
72
|
indexer.options.streamUrl,
|
|
51
73
|
);
|
|
@@ -53,6 +75,8 @@ export function createVcr() {
|
|
|
53
75
|
} else {
|
|
54
76
|
await replay(vcrConfig, indexer, cassetteName);
|
|
55
77
|
}
|
|
78
|
+
|
|
79
|
+
return result;
|
|
56
80
|
},
|
|
57
81
|
};
|
|
58
82
|
}
|
package/src/compose.test.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { type MiddlewareFunction, type NextFunction, compose } from "./compose";
|
|
3
|
-
|
|
4
|
-
type C = {
|
|
5
|
-
bag: Record<string, unknown>;
|
|
6
|
-
finalized: boolean;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
type MiddlewareTuple = MiddlewareFunction<C>;
|
|
10
|
-
|
|
11
|
-
describe("compose", () => {
|
|
12
|
-
async function a(context: C, next: NextFunction) {
|
|
13
|
-
context.bag.log = "log";
|
|
14
|
-
await next();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async function b(context: C, next: NextFunction) {
|
|
18
|
-
await next();
|
|
19
|
-
context.bag.headers = "custom-header";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function c(context: C, next: NextFunction) {
|
|
23
|
-
context.bag.xxx = "yyy";
|
|
24
|
-
await next();
|
|
25
|
-
context.bag.zzz = context.bag.xxx;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function handler(context: C, next: NextFunction) {
|
|
29
|
-
context.bag.log = `${context.bag.log} message`;
|
|
30
|
-
await next();
|
|
31
|
-
context.bag.message = "new response";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const middleware: MiddlewareTuple[] = [];
|
|
35
|
-
|
|
36
|
-
middleware.push(a);
|
|
37
|
-
middleware.push(b);
|
|
38
|
-
middleware.push(c);
|
|
39
|
-
middleware.push(handler);
|
|
40
|
-
|
|
41
|
-
it("composes", async () => {
|
|
42
|
-
const context: C = {
|
|
43
|
-
bag: {},
|
|
44
|
-
finalized: false,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const composed = compose<C>(middleware);
|
|
48
|
-
await composed(context);
|
|
49
|
-
|
|
50
|
-
expect(context.bag.log).toBeDefined();
|
|
51
|
-
expect(context.bag.log).toBe("log message");
|
|
52
|
-
expect(context.bag.headers).toBe("custom-header");
|
|
53
|
-
expect(context.bag.xxx).toBe("yyy");
|
|
54
|
-
expect(context.bag.zzz).toBe("yyy");
|
|
55
|
-
expect(context.finalized).toBe(false);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("accepts a next function", async () => {
|
|
59
|
-
const context: C = {
|
|
60
|
-
bag: {},
|
|
61
|
-
finalized: false,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const composed = compose<C>(middleware);
|
|
65
|
-
await composed(context, async () => {
|
|
66
|
-
context.finalized = true;
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
expect(context.bag.log).toBeDefined();
|
|
70
|
-
expect(context.bag.log).toBe("log message");
|
|
71
|
-
expect(context.bag.headers).toBe("custom-header");
|
|
72
|
-
expect(context.bag.xxx).toBe("yyy");
|
|
73
|
-
expect(context.bag.zzz).toBe("yyy");
|
|
74
|
-
expect(context.finalized).toBe(true);
|
|
75
|
-
});
|
|
76
|
-
});
|