@apibara/indexer 2.0.0-beta.0 → 2.0.0-beta.4
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/package.json +30 -19
- package/src/context.ts +8 -3
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useSink.ts +13 -0
- package/src/index.ts +1 -0
- package/src/indexer.test.ts +70 -41
- package/src/indexer.ts +168 -168
- package/src/plugins/config.ts +4 -4
- package/src/plugins/kv.ts +2 -2
- package/src/plugins/persistence.test.ts +10 -6
- package/src/plugins/persistence.ts +3 -3
- package/src/sink.ts +21 -24
- package/src/sinks/csv.test.ts +15 -3
- package/src/sinks/csv.ts +68 -7
- package/src/sinks/drizzle/Int8Range.ts +52 -0
- package/src/sinks/drizzle/delete.ts +42 -0
- package/src/sinks/drizzle/drizzle.test.ts +239 -0
- package/src/sinks/drizzle/drizzle.ts +115 -0
- package/src/sinks/drizzle/index.ts +6 -0
- package/src/sinks/drizzle/insert.ts +39 -0
- package/src/sinks/drizzle/select.ts +44 -0
- package/src/sinks/drizzle/transaction.ts +49 -0
- package/src/sinks/drizzle/update.ts +47 -0
- package/src/sinks/drizzle/utils.ts +36 -0
- package/src/sinks/sqlite.test.ts +13 -1
- package/src/sinks/sqlite.ts +65 -5
- package/src/testing/indexer.ts +15 -8
- package/src/testing/setup.ts +5 -5
- package/src/testing/vcr.ts +42 -4
- package/src/vcr/record.ts +2 -2
- package/src/vcr/replay.ts +3 -3
- package/.turbo/turbo-build.log +0 -37
- package/CHANGELOG.md +0 -83
- package/LICENSE.txt +0 -202
- package/build.config.ts +0 -16
- package/dist/index.cjs +0 -34
- package/dist/index.d.cts +0 -21
- package/dist/index.d.mts +0 -21
- package/dist/index.d.ts +0 -21
- package/dist/index.mjs +0 -19
- package/dist/shared/indexer.371c0482.mjs +0 -15
- package/dist/shared/indexer.3852a4d3.d.ts +0 -91
- package/dist/shared/indexer.50aa7ab0.mjs +0 -268
- package/dist/shared/indexer.7c118fb5.d.cts +0 -28
- package/dist/shared/indexer.7c118fb5.d.mts +0 -28
- package/dist/shared/indexer.7c118fb5.d.ts +0 -28
- package/dist/shared/indexer.a27bcb35.d.cts +0 -91
- package/dist/shared/indexer.c8ef02ea.cjs +0 -289
- package/dist/shared/indexer.e05aedca.cjs +0 -19
- package/dist/shared/indexer.f7dd57e5.d.mts +0 -91
- package/dist/sinks/csv.cjs +0 -66
- package/dist/sinks/csv.d.cts +0 -34
- package/dist/sinks/csv.d.mts +0 -34
- package/dist/sinks/csv.d.ts +0 -34
- package/dist/sinks/csv.mjs +0 -59
- package/dist/sinks/sqlite.cjs +0 -71
- package/dist/sinks/sqlite.d.cts +0 -41
- package/dist/sinks/sqlite.d.mts +0 -41
- package/dist/sinks/sqlite.d.ts +0 -41
- package/dist/sinks/sqlite.mjs +0 -68
- package/dist/testing/index.cjs +0 -63
- package/dist/testing/index.d.cts +0 -29
- package/dist/testing/index.d.mts +0 -29
- package/dist/testing/index.d.ts +0 -29
- package/dist/testing/index.mjs +0 -59
- package/tsconfig.json +0 -11
package/src/indexer.ts
CHANGED
|
@@ -16,12 +16,16 @@ import {
|
|
|
16
16
|
} from "hookable";
|
|
17
17
|
|
|
18
18
|
import assert from "node:assert";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
type IndexerContext,
|
|
21
|
+
indexerAsyncContext,
|
|
22
|
+
useIndexerContext,
|
|
23
|
+
} from "./context";
|
|
20
24
|
import { tracer } from "./otel";
|
|
21
25
|
import type { IndexerPlugin } from "./plugins";
|
|
22
|
-
import { type Sink,
|
|
26
|
+
import { type Sink, defaultSink } from "./sink";
|
|
23
27
|
|
|
24
|
-
export interface IndexerHooks<TFilter, TBlock
|
|
28
|
+
export interface IndexerHooks<TFilter, TBlock> {
|
|
25
29
|
"run:before": () => void;
|
|
26
30
|
"run:after": () => void;
|
|
27
31
|
"connect:before": ({
|
|
@@ -48,63 +52,80 @@ export interface IndexerHooks<TFilter, TBlock, TRet> {
|
|
|
48
52
|
finality: DataFinality;
|
|
49
53
|
endCursor?: Cursor;
|
|
50
54
|
}) => void;
|
|
51
|
-
"handler:after": ({
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"sink:flush": ({
|
|
55
|
+
"handler:after": ({
|
|
56
|
+
block,
|
|
57
|
+
finality,
|
|
55
58
|
endCursor,
|
|
59
|
+
}: {
|
|
60
|
+
block: TBlock;
|
|
61
|
+
finality: DataFinality;
|
|
62
|
+
endCursor?: Cursor;
|
|
63
|
+
}) => void;
|
|
64
|
+
"transaction:commit": ({
|
|
56
65
|
finality,
|
|
57
|
-
|
|
66
|
+
endCursor,
|
|
67
|
+
}: {
|
|
68
|
+
finality: DataFinality;
|
|
69
|
+
endCursor?: Cursor;
|
|
70
|
+
}) => void;
|
|
71
|
+
"handler:exception": ({ error }: { error: Error }) => void;
|
|
58
72
|
message: ({ message }: { message: StreamDataResponse<TBlock> }) => void;
|
|
59
73
|
}
|
|
60
74
|
|
|
61
|
-
export interface IndexerConfig<TFilter, TBlock,
|
|
75
|
+
export interface IndexerConfig<TFilter, TBlock, TTxnParams> {
|
|
62
76
|
streamUrl: string;
|
|
63
77
|
filter: TFilter;
|
|
64
78
|
finality?: DataFinality;
|
|
65
79
|
startingCursor?: Cursor;
|
|
66
|
-
|
|
80
|
+
sink?: Sink<TTxnParams>;
|
|
81
|
+
factory?: ({
|
|
82
|
+
block,
|
|
83
|
+
context,
|
|
84
|
+
}: { block: TBlock; context: IndexerContext<TTxnParams> }) => Promise<{
|
|
85
|
+
filter?: TFilter;
|
|
86
|
+
}>;
|
|
67
87
|
transform: (args: {
|
|
68
88
|
block: TBlock;
|
|
69
89
|
cursor?: Cursor | undefined;
|
|
70
90
|
endCursor?: Cursor | undefined;
|
|
71
91
|
finality: DataFinality;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
context: IndexerContext<TTxnParams>;
|
|
93
|
+
}) => Promise<void>;
|
|
94
|
+
hooks?: NestedHooks<IndexerHooks<TFilter, TBlock>>;
|
|
95
|
+
plugins?: ReadonlyArray<IndexerPlugin<TFilter, TBlock, TTxnParams>>;
|
|
75
96
|
debug?: boolean;
|
|
76
97
|
}
|
|
77
98
|
|
|
78
|
-
export interface IndexerWithStreamConfig<TFilter, TBlock,
|
|
79
|
-
extends IndexerConfig<TFilter, TBlock,
|
|
99
|
+
export interface IndexerWithStreamConfig<TFilter, TBlock, TTxnParams>
|
|
100
|
+
extends IndexerConfig<TFilter, TBlock, TTxnParams> {
|
|
80
101
|
streamConfig: StreamConfig<TFilter, TBlock>;
|
|
81
102
|
}
|
|
82
103
|
|
|
83
104
|
export function defineIndexer<TFilter, TBlock>(
|
|
84
105
|
streamConfig: StreamConfig<TFilter, TBlock>,
|
|
85
106
|
) {
|
|
86
|
-
return <
|
|
87
|
-
config: IndexerConfig<TFilter, TBlock,
|
|
88
|
-
): IndexerWithStreamConfig<TFilter, TBlock,
|
|
107
|
+
return <TTxnParams>(
|
|
108
|
+
config: IndexerConfig<TFilter, TBlock, TTxnParams>,
|
|
109
|
+
): IndexerWithStreamConfig<TFilter, TBlock, TTxnParams> => ({
|
|
89
110
|
streamConfig,
|
|
90
111
|
...config,
|
|
91
112
|
});
|
|
92
113
|
}
|
|
93
114
|
|
|
94
|
-
export interface Indexer<TFilter, TBlock,
|
|
115
|
+
export interface Indexer<TFilter, TBlock, TTxnParams> {
|
|
95
116
|
streamConfig: StreamConfig<TFilter, TBlock>;
|
|
96
|
-
options: IndexerConfig<TFilter, TBlock,
|
|
97
|
-
hooks: Hookable<IndexerHooks<TFilter, TBlock
|
|
117
|
+
options: IndexerConfig<TFilter, TBlock, TTxnParams>;
|
|
118
|
+
hooks: Hookable<IndexerHooks<TFilter, TBlock>>;
|
|
98
119
|
}
|
|
99
120
|
|
|
100
|
-
export function createIndexer<TFilter, TBlock,
|
|
121
|
+
export function createIndexer<TFilter, TBlock, TTxnParams>({
|
|
101
122
|
streamConfig,
|
|
102
123
|
...options
|
|
103
|
-
}: IndexerWithStreamConfig<TFilter, TBlock,
|
|
104
|
-
const indexer: Indexer<TFilter, TBlock,
|
|
124
|
+
}: IndexerWithStreamConfig<TFilter, TBlock, TTxnParams>) {
|
|
125
|
+
const indexer: Indexer<TFilter, TBlock, TTxnParams> = {
|
|
105
126
|
options,
|
|
106
127
|
streamConfig,
|
|
107
|
-
hooks: createHooks<IndexerHooks<TFilter, TBlock
|
|
128
|
+
hooks: createHooks<IndexerHooks<TFilter, TBlock>>(),
|
|
108
129
|
};
|
|
109
130
|
|
|
110
131
|
if (indexer.options.debug) {
|
|
@@ -120,22 +141,18 @@ export function createIndexer<TFilter, TBlock, TRet>({
|
|
|
120
141
|
return indexer;
|
|
121
142
|
}
|
|
122
143
|
|
|
123
|
-
export async function run<TFilter, TBlock,
|
|
144
|
+
export async function run<TFilter, TBlock, TTxnParams>(
|
|
124
145
|
client: Client<TFilter, TBlock>,
|
|
125
|
-
indexer: Indexer<TFilter, TBlock,
|
|
126
|
-
sinkArg?: Sink,
|
|
146
|
+
indexer: Indexer<TFilter, TBlock, TTxnParams>,
|
|
127
147
|
) {
|
|
128
148
|
await indexerAsyncContext.callAsync({}, async () => {
|
|
129
|
-
|
|
149
|
+
const context = useIndexerContext<TTxnParams>();
|
|
130
150
|
|
|
131
|
-
const sink =
|
|
151
|
+
const sink = indexer.options.sink ?? defaultSink();
|
|
132
152
|
|
|
133
|
-
sink
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
sink.hook("flush", async ({ endCursor, finality }) => {
|
|
137
|
-
await indexer.hooks.callHook("sink:flush", { endCursor, finality });
|
|
138
|
-
});
|
|
153
|
+
context.sink = sink as Sink<TTxnParams>;
|
|
154
|
+
|
|
155
|
+
await indexer.hooks.callHook("run:before");
|
|
139
156
|
|
|
140
157
|
// Check if the it's factory mode or not
|
|
141
158
|
const isFactoryMode = indexer.options.factory !== undefined;
|
|
@@ -151,7 +168,6 @@ export async function run<TFilter, TBlock, TRet>(
|
|
|
151
168
|
|
|
152
169
|
const options: StreamDataOptions = {};
|
|
153
170
|
|
|
154
|
-
// TODO persistence plugin filter
|
|
155
171
|
await indexer.hooks.callHook("connect:before", { request, options });
|
|
156
172
|
|
|
157
173
|
// store main filter, so later it can be merged
|
|
@@ -161,162 +177,146 @@ export async function run<TFilter, TBlock, TRet>(
|
|
|
161
177
|
}
|
|
162
178
|
|
|
163
179
|
// create stream
|
|
164
|
-
let stream
|
|
180
|
+
let stream: AsyncIterator<
|
|
181
|
+
StreamDataResponse<TBlock>,
|
|
182
|
+
StreamDataResponse<TBlock>
|
|
183
|
+
> = client.streamData(request, options)[Symbol.asyncIterator]();
|
|
165
184
|
|
|
166
185
|
await indexer.hooks.callHook("connect:after");
|
|
167
186
|
|
|
168
|
-
// on state ->
|
|
169
|
-
// normal: iterate as usual
|
|
170
|
-
// recover: reconnect after updating filter
|
|
171
|
-
let state: { _tag: "normal" } | { _tag: "recover"; data?: TRet[] } = {
|
|
172
|
-
_tag: "normal",
|
|
173
|
-
};
|
|
174
|
-
|
|
175
187
|
while (true) {
|
|
176
|
-
|
|
177
|
-
await indexer.hooks.callHook("message", { message });
|
|
178
|
-
|
|
179
|
-
switch (message._tag) {
|
|
180
|
-
case "data": {
|
|
181
|
-
await tracer.startActiveSpan("message data", async (span) => {
|
|
182
|
-
const blocks = message.data.data;
|
|
183
|
-
const { cursor, endCursor, finality } = message.data;
|
|
184
|
-
|
|
185
|
-
let block: TBlock | null;
|
|
186
|
-
|
|
187
|
-
// combine output of factory and transform function
|
|
188
|
-
const output: TRet[] = [];
|
|
189
|
-
|
|
190
|
-
// when factory mode
|
|
191
|
-
if (isFactoryMode) {
|
|
192
|
-
assert(indexer.options.factory !== undefined);
|
|
193
|
-
|
|
194
|
-
const [factoryBlock, mainBlock] = blocks;
|
|
195
|
-
|
|
196
|
-
block = mainBlock;
|
|
197
|
-
|
|
198
|
-
if (state._tag === "normal" && factoryBlock !== null) {
|
|
199
|
-
const { data, filter } =
|
|
200
|
-
await indexer.options.factory(factoryBlock);
|
|
201
|
-
|
|
202
|
-
// write returned data from factory function if filter is not defined
|
|
203
|
-
if (!filter) {
|
|
204
|
-
output.push(...(data ?? []));
|
|
205
|
-
} else {
|
|
206
|
-
// when filter is defined
|
|
207
|
-
// merge old and new filters
|
|
208
|
-
mainFilter = indexer.streamConfig.mergeFilter(
|
|
209
|
-
mainFilter,
|
|
210
|
-
filter,
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
// create request with new filters
|
|
214
|
-
const request = indexer.streamConfig.Request.make({
|
|
215
|
-
filter: [indexer.options.filter, mainFilter],
|
|
216
|
-
finality: indexer.options.finality,
|
|
217
|
-
startingCursor: cursor,
|
|
218
|
-
});
|
|
188
|
+
const { value: message, done } = await stream.next();
|
|
219
189
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
190
|
+
if (done) {
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await indexer.hooks.callHook("message", { message });
|
|
195
|
+
|
|
196
|
+
switch (message._tag) {
|
|
197
|
+
case "data": {
|
|
198
|
+
await tracer.startActiveSpan("message data", async (span) => {
|
|
199
|
+
const blocks = message.data.data;
|
|
200
|
+
const { cursor, endCursor, finality } = message.data;
|
|
201
|
+
|
|
202
|
+
await sink.transaction(
|
|
203
|
+
{ cursor, endCursor, finality },
|
|
204
|
+
async (txn) => {
|
|
205
|
+
// attach transaction to context
|
|
206
|
+
context.sinkTransaction = txn as TTxnParams;
|
|
207
|
+
|
|
208
|
+
let block: TBlock | null;
|
|
209
|
+
|
|
210
|
+
// when factory mode
|
|
211
|
+
if (isFactoryMode) {
|
|
212
|
+
assert(indexer.options.factory !== undefined);
|
|
213
|
+
|
|
214
|
+
const [factoryBlock, mainBlock] = blocks;
|
|
215
|
+
|
|
216
|
+
block = mainBlock;
|
|
217
|
+
|
|
218
|
+
if (factoryBlock !== null) {
|
|
219
|
+
const { filter } = await indexer.options.factory({
|
|
220
|
+
block: factoryBlock,
|
|
221
|
+
context,
|
|
223
222
|
});
|
|
224
223
|
|
|
225
|
-
//
|
|
226
|
-
|
|
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
|
+
);
|
|
227
232
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
+
});
|
|
233
239
|
|
|
234
|
-
|
|
240
|
+
await indexer.hooks.callHook("connect:factory", {
|
|
241
|
+
request,
|
|
242
|
+
endCursor,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// create new stream with new request
|
|
246
|
+
stream = client
|
|
247
|
+
.streamData(request, options)
|
|
248
|
+
[Symbol.asyncIterator]();
|
|
249
|
+
|
|
250
|
+
const { value: message } = await stream.next();
|
|
251
|
+
|
|
252
|
+
assert(message._tag === "data");
|
|
253
|
+
|
|
254
|
+
const [_factoryBlock, _block] = message.data.data;
|
|
255
|
+
|
|
256
|
+
block = _block;
|
|
257
|
+
}
|
|
235
258
|
}
|
|
259
|
+
} else {
|
|
260
|
+
// when not in factory mode
|
|
261
|
+
block = blocks[0];
|
|
236
262
|
}
|
|
237
|
-
// after restart when state in recover mode
|
|
238
|
-
else if (state._tag === "recover") {
|
|
239
|
-
// we write data to output
|
|
240
|
-
output.push(...(state.data ?? []));
|
|
241
|
-
// change state back to normal to avoid infinite loop
|
|
242
|
-
state = { _tag: "normal" };
|
|
243
|
-
}
|
|
244
|
-
} else {
|
|
245
|
-
// when not in factory mode
|
|
246
|
-
block = blocks[0];
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// if block is not null
|
|
250
|
-
if (block) {
|
|
251
|
-
await tracer.startActiveSpan("handler", async (span) => {
|
|
252
|
-
await indexer.hooks.callHook("handler:before", {
|
|
253
|
-
block,
|
|
254
|
-
endCursor,
|
|
255
|
-
finality,
|
|
256
|
-
});
|
|
257
263
|
|
|
258
|
-
|
|
259
|
-
|
|
264
|
+
// if block is not null
|
|
265
|
+
if (block) {
|
|
266
|
+
await tracer.startActiveSpan("handler", async (span) => {
|
|
267
|
+
await indexer.hooks.callHook("handler:before", {
|
|
260
268
|
block,
|
|
261
|
-
cursor,
|
|
262
269
|
endCursor,
|
|
263
270
|
finality,
|
|
264
271
|
});
|
|
265
272
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
endCursor,
|
|
289
|
-
finality,
|
|
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();
|
|
290
295
|
});
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
await indexer.hooks.callHook("transaction:commit", {
|
|
300
|
+
finality,
|
|
301
|
+
endCursor,
|
|
296
302
|
});
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
consola.warn("unexpected message", message);
|
|
301
|
-
throw new Error("not implemented");
|
|
302
|
-
}
|
|
303
|
+
span.end();
|
|
304
|
+
});
|
|
305
|
+
break;
|
|
303
306
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
307
|
+
case "invalidate": {
|
|
308
|
+
await tracer.startActiveSpan("message invalidate", async (span) => {
|
|
309
|
+
await sink.invalidate(message.invalidate.cursor);
|
|
310
|
+
});
|
|
308
311
|
break;
|
|
309
312
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
continue;
|
|
313
|
+
default: {
|
|
314
|
+
consola.warn("unexpected message", message);
|
|
315
|
+
throw new Error("not implemented");
|
|
316
|
+
}
|
|
315
317
|
}
|
|
316
318
|
|
|
317
319
|
await indexer.hooks.callHook("run:after");
|
|
318
|
-
|
|
319
|
-
break;
|
|
320
320
|
}
|
|
321
321
|
});
|
|
322
322
|
}
|
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, TTxnParams> = (
|
|
4
|
+
indexer: Indexer<TFilter, TBlock, TTxnParams>,
|
|
5
5
|
) => void;
|
|
6
6
|
|
|
7
|
-
export function defineIndexerPlugin<TFilter, TBlock,
|
|
8
|
-
def: IndexerPlugin<TFilter, TBlock,
|
|
7
|
+
export function defineIndexerPlugin<TFilter, TBlock, TTxnParams>(
|
|
8
|
+
def: IndexerPlugin<TFilter, TBlock, TTxnParams>,
|
|
9
9
|
) {
|
|
10
10
|
return def;
|
|
11
11
|
}
|
package/src/plugins/kv.ts
CHANGED
|
@@ -5,10 +5,10 @@ import { useIndexerContext } from "../context";
|
|
|
5
5
|
import { deserialize, serialize } from "../vcr";
|
|
6
6
|
import { defineIndexerPlugin } from "./config";
|
|
7
7
|
|
|
8
|
-
export function kv<TFilter, TBlock,
|
|
8
|
+
export function kv<TFilter, TBlock, TTxnParams>({
|
|
9
9
|
database,
|
|
10
10
|
}: { database: SqliteDatabase }) {
|
|
11
|
-
return defineIndexerPlugin<TFilter, TBlock,
|
|
11
|
+
return defineIndexerPlugin<TFilter, TBlock, TTxnParams>((indexer) => {
|
|
12
12
|
indexer.hooks.hook("run:before", () => {
|
|
13
13
|
KVStore.initialize(database);
|
|
14
14
|
});
|
|
@@ -9,7 +9,7 @@ import { klona } from "klona/full";
|
|
|
9
9
|
import { describe, expect, it } from "vitest";
|
|
10
10
|
import { run } from "../indexer";
|
|
11
11
|
import { generateMockMessages } from "../testing";
|
|
12
|
-
import {
|
|
12
|
+
import { getMockIndexer } from "../testing/indexer";
|
|
13
13
|
import { SqlitePersistence, sqlitePersistence } from "./persistence";
|
|
14
14
|
|
|
15
15
|
describe("Persistence", () => {
|
|
@@ -120,12 +120,16 @@ describe("Persistence", () => {
|
|
|
120
120
|
|
|
121
121
|
const db = new Database(":memory:");
|
|
122
122
|
|
|
123
|
-
const persistence = sqlitePersistence<MockFilter, MockBlock, MockRet>({
|
|
124
|
-
database: db,
|
|
125
|
-
});
|
|
126
|
-
|
|
127
123
|
// create mock indexer with persistence plugin
|
|
128
|
-
const indexer = klona(
|
|
124
|
+
const indexer = klona(
|
|
125
|
+
getMockIndexer({
|
|
126
|
+
plugins: [
|
|
127
|
+
sqlitePersistence({
|
|
128
|
+
database: db,
|
|
129
|
+
}),
|
|
130
|
+
],
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
129
133
|
|
|
130
134
|
await run(client, indexer);
|
|
131
135
|
|
|
@@ -3,10 +3,10 @@ import type { Database as SqliteDatabase, Statement } from "better-sqlite3";
|
|
|
3
3
|
import { deserialize, serialize } from "../vcr";
|
|
4
4
|
import { defineIndexerPlugin } from "./config";
|
|
5
5
|
|
|
6
|
-
export function sqlitePersistence<TFilter, TBlock,
|
|
6
|
+
export function sqlitePersistence<TFilter, TBlock, TTxnParams>({
|
|
7
7
|
database,
|
|
8
8
|
}: { database: SqliteDatabase }) {
|
|
9
|
-
return defineIndexerPlugin<TFilter, TBlock,
|
|
9
|
+
return defineIndexerPlugin<TFilter, TBlock, TTxnParams>((indexer) => {
|
|
10
10
|
let store: SqlitePersistence<TFilter>;
|
|
11
11
|
|
|
12
12
|
indexer.hooks.hook("run:before", () => {
|
|
@@ -27,7 +27,7 @@ export function sqlitePersistence<TFilter, TBlock, TRet>({
|
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
indexer.hooks.hook("
|
|
30
|
+
indexer.hooks.hook("transaction:commit", ({ endCursor }) => {
|
|
31
31
|
if (endCursor) {
|
|
32
32
|
store.put({ cursor: endCursor });
|
|
33
33
|
}
|
package/src/sink.ts
CHANGED
|
@@ -1,36 +1,33 @@
|
|
|
1
1
|
import type { Cursor, DataFinality } from "@apibara/protocol";
|
|
2
|
-
import
|
|
2
|
+
import consola from "consola";
|
|
3
3
|
|
|
4
4
|
export type SinkData = Record<string, unknown>;
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
endCursor,
|
|
10
|
-
finality,
|
|
11
|
-
}: { endCursor?: Cursor; finality: DataFinality }): void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export type SinkWriteArgs = {
|
|
15
|
-
data: SinkData[];
|
|
16
|
-
cursor?: Cursor | undefined;
|
|
17
|
-
endCursor?: Cursor | undefined;
|
|
6
|
+
export type SinkCursorParams = {
|
|
7
|
+
cursor?: Cursor;
|
|
8
|
+
endCursor?: Cursor;
|
|
18
9
|
finality: DataFinality;
|
|
19
10
|
};
|
|
20
11
|
|
|
21
|
-
export abstract class Sink
|
|
22
|
-
abstract
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
12
|
+
export abstract class Sink<TTxnParams = unknown> {
|
|
13
|
+
abstract transaction(
|
|
14
|
+
{ cursor, endCursor, finality }: SinkCursorParams,
|
|
15
|
+
cb: (params: TTxnParams) => Promise<void>,
|
|
16
|
+
): Promise<void>;
|
|
17
|
+
|
|
18
|
+
abstract invalidate(cursor?: Cursor): Promise<void>;
|
|
28
19
|
}
|
|
29
20
|
|
|
30
|
-
export class DefaultSink extends Sink {
|
|
31
|
-
async
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
export class DefaultSink extends Sink<unknown> {
|
|
22
|
+
async transaction(
|
|
23
|
+
{ cursor, endCursor, finality }: SinkCursorParams,
|
|
24
|
+
cb: (params: unknown) => Promise<void>,
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
await cb({});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async invalidate(cursor?: Cursor) {
|
|
30
|
+
consola.info(`Invalidating cursor ${cursor?.orderKey}`);
|
|
34
31
|
}
|
|
35
32
|
}
|
|
36
33
|
|
package/src/sinks/csv.test.ts
CHANGED
|
@@ -5,10 +5,11 @@ import {
|
|
|
5
5
|
type MockFilter,
|
|
6
6
|
} from "@apibara/protocol/testing";
|
|
7
7
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
8
|
+
import { useSink } from "../hooks";
|
|
8
9
|
import { run } from "../indexer";
|
|
9
10
|
import {} from "../plugins/persistence";
|
|
10
11
|
import { generateMockMessages } from "../testing";
|
|
11
|
-
import {
|
|
12
|
+
import { getMockIndexer } from "../testing/indexer";
|
|
12
13
|
import { csv } from "./csv";
|
|
13
14
|
|
|
14
15
|
describe("Run Test", () => {
|
|
@@ -31,8 +32,19 @@ describe("Run Test", () => {
|
|
|
31
32
|
return generateMockMessages();
|
|
32
33
|
});
|
|
33
34
|
|
|
34
|
-
const sink = csv
|
|
35
|
-
await run(
|
|
35
|
+
const sink = csv({ filepath: "test.csv" });
|
|
36
|
+
await run(
|
|
37
|
+
client,
|
|
38
|
+
getMockIndexer({
|
|
39
|
+
sink,
|
|
40
|
+
override: {
|
|
41
|
+
transform: async ({ context, endCursor, block: { data } }) => {
|
|
42
|
+
const { writer } = useSink({ context });
|
|
43
|
+
writer.insert([{ data }]);
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
36
48
|
|
|
37
49
|
const csvData = await fs.readFile("test.csv", "utf-8");
|
|
38
50
|
|