@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.
- package/dist/index.cjs +270 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.mjs +259 -0
- package/dist/internal/testing.cjs +109 -0
- package/dist/internal/testing.d.cts +40 -0
- package/dist/internal/testing.d.mts +40 -0
- package/dist/internal/testing.d.ts +40 -0
- package/dist/internal/testing.mjs +104 -0
- package/dist/plugins/index.cjs +43 -0
- package/dist/plugins/index.d.cts +18 -0
- package/dist/plugins/index.d.mts +18 -0
- package/dist/plugins/index.d.ts +18 -0
- package/dist/plugins/index.mjs +38 -0
- package/dist/shared/indexer.077335f3.cjs +15 -0
- package/dist/shared/indexer.2416906c.cjs +29 -0
- package/dist/shared/indexer.601ceab0.cjs +7 -0
- package/dist/shared/indexer.8939ecc8.d.cts +91 -0
- package/dist/shared/indexer.8939ecc8.d.mts +91 -0
- package/dist/shared/indexer.8939ecc8.d.ts +91 -0
- package/dist/shared/indexer.9b21ddd2.mjs +5 -0
- package/dist/shared/indexer.a55ad619.mjs +12 -0
- package/dist/shared/indexer.ff25c953.mjs +26 -0
- package/dist/testing/index.cjs +58 -0
- package/dist/testing/index.d.cts +12 -0
- package/dist/testing/index.d.mts +12 -0
- package/dist/testing/index.d.ts +12 -0
- package/dist/testing/index.mjs +52 -0
- package/dist/vcr/index.cjs +92 -0
- package/dist/vcr/index.d.cts +27 -0
- package/dist/vcr/index.d.mts +27 -0
- package/dist/vcr/index.d.ts +27 -0
- package/dist/vcr/index.mjs +78 -0
- package/package.json +31 -41
- package/src/compose.test.ts +76 -0
- package/src/compose.ts +71 -0
- package/src/context.ts +14 -8
- package/src/index.ts +0 -5
- package/src/indexer.test.ts +109 -186
- package/src/indexer.ts +244 -144
- package/src/internal/testing.ts +135 -0
- package/src/plugins/config.ts +4 -4
- package/src/plugins/index.ts +8 -1
- package/src/plugins/logger.ts +30 -0
- package/src/plugins/persistence.ts +24 -187
- package/src/testing/index.ts +50 -3
- package/src/vcr/record.ts +6 -4
- package/src/vcr/replay.ts +8 -18
- package/src/hooks/index.ts +0 -2
- package/src/hooks/useKVStore.ts +0 -12
- package/src/hooks/useSink.ts +0 -13
- package/src/plugins/kv.test.ts +0 -120
- package/src/plugins/kv.ts +0 -132
- package/src/plugins/persistence.test.ts +0 -151
- package/src/sink.ts +0 -36
- package/src/sinks/csv.test.ts +0 -65
- package/src/sinks/csv.ts +0 -159
- package/src/sinks/drizzle/Int8Range.ts +0 -52
- package/src/sinks/drizzle/delete.ts +0 -42
- package/src/sinks/drizzle/drizzle.test.ts +0 -239
- package/src/sinks/drizzle/drizzle.ts +0 -115
- package/src/sinks/drizzle/index.ts +0 -6
- package/src/sinks/drizzle/insert.ts +0 -39
- package/src/sinks/drizzle/select.ts +0 -44
- package/src/sinks/drizzle/transaction.ts +0 -49
- package/src/sinks/drizzle/update.ts +0 -47
- package/src/sinks/drizzle/utils.ts +0 -36
- package/src/sinks/sqlite.test.ts +0 -99
- package/src/sinks/sqlite.ts +0 -170
- package/src/testing/helper.ts +0 -13
- package/src/testing/indexer.ts +0 -35
- package/src/testing/setup.ts +0 -59
- package/src/testing/vcr.ts +0 -54
package/src/indexer.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
Client,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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": (
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
82
|
+
context: IndexerContext;
|
|
93
83
|
}) => Promise<void>;
|
|
94
84
|
hooks?: NestedHooks<IndexerHooks<TFilter, TBlock>>;
|
|
95
|
-
plugins?: ReadonlyArray<IndexerPlugin<TFilter, TBlock
|
|
85
|
+
plugins?: ReadonlyArray<IndexerPlugin<TFilter, TBlock>>;
|
|
96
86
|
debug?: boolean;
|
|
97
87
|
}
|
|
98
88
|
|
|
99
|
-
export interface IndexerWithStreamConfig<TFilter, TBlock
|
|
100
|
-
extends IndexerConfig<TFilter, TBlock
|
|
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
|
|
108
|
-
config: IndexerConfig<TFilter, TBlock
|
|
109
|
-
): IndexerWithStreamConfig<TFilter, TBlock
|
|
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
|
|
105
|
+
export interface Indexer<TFilter, TBlock> {
|
|
116
106
|
streamConfig: StreamConfig<TFilter, TBlock>;
|
|
117
|
-
options: IndexerConfig<TFilter, TBlock
|
|
107
|
+
options: IndexerConfig<TFilter, TBlock>;
|
|
118
108
|
hooks: Hookable<IndexerHooks<TFilter, TBlock>>;
|
|
119
109
|
}
|
|
120
110
|
|
|
121
|
-
export function createIndexer<TFilter, TBlock
|
|
111
|
+
export function createIndexer<TFilter, TBlock>({
|
|
122
112
|
streamConfig,
|
|
123
113
|
...options
|
|
124
|
-
}: IndexerWithStreamConfig<TFilter, TBlock
|
|
125
|
-
const indexer: Indexer<TFilter, TBlock
|
|
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
|
|
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
|
|
142
|
+
indexer: Indexer<TFilter, TBlock>,
|
|
143
|
+
options: ReconnectOptions = {},
|
|
147
144
|
) {
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
151
|
+
const runOptions: RunOptions = {
|
|
152
|
+
onConnect() {
|
|
153
|
+
retryCount = 0;
|
|
154
|
+
},
|
|
155
|
+
};
|
|
152
156
|
|
|
153
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
264
|
+
await middleware(context, async () => {
|
|
265
|
+
let block: TBlock | null;
|
|
209
266
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
267
|
+
// when factory mode
|
|
268
|
+
if (isFactoryMode) {
|
|
269
|
+
assert(indexer.options.factory !== undefined);
|
|
213
270
|
|
|
214
|
-
|
|
271
|
+
const [factoryBlock, mainBlock] = blocks;
|
|
215
272
|
|
|
216
|
-
|
|
273
|
+
block = mainBlock;
|
|
217
274
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
297
|
+
await indexer.hooks.callHook("connect:factory", {
|
|
298
|
+
request,
|
|
299
|
+
endCursor,
|
|
300
|
+
});
|
|
244
301
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
302
|
+
// create new stream with new request
|
|
303
|
+
stream = client
|
|
304
|
+
.streamData(request, options)
|
|
305
|
+
[Symbol.asyncIterator]();
|
|
249
306
|
|
|
250
|
-
|
|
307
|
+
const { value: message } = await stream.next();
|
|
251
308
|
|
|
252
|
-
|
|
309
|
+
assert(message._tag === "data");
|
|
253
310
|
|
|
254
|
-
|
|
311
|
+
const [_factoryBlock, _block] = message.data.data;
|
|
255
312
|
|
|
256
|
-
|
|
257
|
-
}
|
|
313
|
+
block = _block;
|
|
258
314
|
}
|
|
259
|
-
} else {
|
|
260
|
-
// when not in factory mode
|
|
261
|
-
block = blocks[0];
|
|
262
315
|
}
|
|
263
|
-
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
package/src/plugins/config.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { Indexer } from "../indexer";
|
|
2
2
|
|
|
3
|
-
export type IndexerPlugin<TFilter, TBlock
|
|
4
|
-
indexer: Indexer<TFilter, TBlock
|
|
3
|
+
export type IndexerPlugin<TFilter, TBlock> = (
|
|
4
|
+
indexer: Indexer<TFilter, TBlock>,
|
|
5
5
|
) => void;
|
|
6
6
|
|
|
7
|
-
export function defineIndexerPlugin<TFilter, TBlock
|
|
8
|
-
def: IndexerPlugin<TFilter, TBlock
|
|
7
|
+
export function defineIndexerPlugin<TFilter, TBlock>(
|
|
8
|
+
def: IndexerPlugin<TFilter, TBlock>,
|
|
9
9
|
) {
|
|
10
10
|
return def;
|
|
11
11
|
}
|
package/src/plugins/index.ts
CHANGED