@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.
Files changed (66) hide show
  1. package/package.json +30 -19
  2. package/src/context.ts +8 -3
  3. package/src/hooks/index.ts +1 -0
  4. package/src/hooks/useSink.ts +13 -0
  5. package/src/index.ts +1 -0
  6. package/src/indexer.test.ts +70 -41
  7. package/src/indexer.ts +168 -168
  8. package/src/plugins/config.ts +4 -4
  9. package/src/plugins/kv.ts +2 -2
  10. package/src/plugins/persistence.test.ts +10 -6
  11. package/src/plugins/persistence.ts +3 -3
  12. package/src/sink.ts +21 -24
  13. package/src/sinks/csv.test.ts +15 -3
  14. package/src/sinks/csv.ts +68 -7
  15. package/src/sinks/drizzle/Int8Range.ts +52 -0
  16. package/src/sinks/drizzle/delete.ts +42 -0
  17. package/src/sinks/drizzle/drizzle.test.ts +239 -0
  18. package/src/sinks/drizzle/drizzle.ts +115 -0
  19. package/src/sinks/drizzle/index.ts +6 -0
  20. package/src/sinks/drizzle/insert.ts +39 -0
  21. package/src/sinks/drizzle/select.ts +44 -0
  22. package/src/sinks/drizzle/transaction.ts +49 -0
  23. package/src/sinks/drizzle/update.ts +47 -0
  24. package/src/sinks/drizzle/utils.ts +36 -0
  25. package/src/sinks/sqlite.test.ts +13 -1
  26. package/src/sinks/sqlite.ts +65 -5
  27. package/src/testing/indexer.ts +15 -8
  28. package/src/testing/setup.ts +5 -5
  29. package/src/testing/vcr.ts +42 -4
  30. package/src/vcr/record.ts +2 -2
  31. package/src/vcr/replay.ts +3 -3
  32. package/.turbo/turbo-build.log +0 -37
  33. package/CHANGELOG.md +0 -83
  34. package/LICENSE.txt +0 -202
  35. package/build.config.ts +0 -16
  36. package/dist/index.cjs +0 -34
  37. package/dist/index.d.cts +0 -21
  38. package/dist/index.d.mts +0 -21
  39. package/dist/index.d.ts +0 -21
  40. package/dist/index.mjs +0 -19
  41. package/dist/shared/indexer.371c0482.mjs +0 -15
  42. package/dist/shared/indexer.3852a4d3.d.ts +0 -91
  43. package/dist/shared/indexer.50aa7ab0.mjs +0 -268
  44. package/dist/shared/indexer.7c118fb5.d.cts +0 -28
  45. package/dist/shared/indexer.7c118fb5.d.mts +0 -28
  46. package/dist/shared/indexer.7c118fb5.d.ts +0 -28
  47. package/dist/shared/indexer.a27bcb35.d.cts +0 -91
  48. package/dist/shared/indexer.c8ef02ea.cjs +0 -289
  49. package/dist/shared/indexer.e05aedca.cjs +0 -19
  50. package/dist/shared/indexer.f7dd57e5.d.mts +0 -91
  51. package/dist/sinks/csv.cjs +0 -66
  52. package/dist/sinks/csv.d.cts +0 -34
  53. package/dist/sinks/csv.d.mts +0 -34
  54. package/dist/sinks/csv.d.ts +0 -34
  55. package/dist/sinks/csv.mjs +0 -59
  56. package/dist/sinks/sqlite.cjs +0 -71
  57. package/dist/sinks/sqlite.d.cts +0 -41
  58. package/dist/sinks/sqlite.d.mts +0 -41
  59. package/dist/sinks/sqlite.d.ts +0 -41
  60. package/dist/sinks/sqlite.mjs +0 -68
  61. package/dist/testing/index.cjs +0 -63
  62. package/dist/testing/index.d.cts +0 -29
  63. package/dist/testing/index.d.mts +0 -29
  64. package/dist/testing/index.d.ts +0 -29
  65. package/dist/testing/index.mjs +0 -59
  66. 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 { indexerAsyncContext } from "./context";
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, type SinkData, defaultSink } from "./sink";
26
+ import { type Sink, defaultSink } from "./sink";
23
27
 
24
- export interface IndexerHooks<TFilter, TBlock, TRet> {
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": ({ output }: { output: TRet[] }) => void;
52
- "handler:exception": ({ error }: { error: Error }) => void;
53
- "sink:write": ({ data }: { data: TRet[] }) => void;
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
- }: { endCursor?: Cursor; finality: DataFinality }) => void;
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, TRet> {
75
+ export interface IndexerConfig<TFilter, TBlock, TTxnParams> {
62
76
  streamUrl: string;
63
77
  filter: TFilter;
64
78
  finality?: DataFinality;
65
79
  startingCursor?: Cursor;
66
- factory?: (block: TBlock) => Promise<{ filter?: TFilter; data?: TRet[] }>;
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
- }) => Promise<TRet[]>;
73
- hooks?: NestedHooks<IndexerHooks<TFilter, TBlock, TRet>>;
74
- plugins?: ReadonlyArray<IndexerPlugin<TFilter, TBlock, TRet>>;
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, TRet>
79
- extends IndexerConfig<TFilter, TBlock, TRet> {
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 <TRet>(
87
- config: IndexerConfig<TFilter, TBlock, TRet>,
88
- ): IndexerWithStreamConfig<TFilter, TBlock, TRet> => ({
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, TRet> {
115
+ export interface Indexer<TFilter, TBlock, TTxnParams> {
95
116
  streamConfig: StreamConfig<TFilter, TBlock>;
96
- options: IndexerConfig<TFilter, TBlock, TRet>;
97
- hooks: Hookable<IndexerHooks<TFilter, TBlock, TRet>>;
117
+ options: IndexerConfig<TFilter, TBlock, TTxnParams>;
118
+ hooks: Hookable<IndexerHooks<TFilter, TBlock>>;
98
119
  }
99
120
 
100
- export function createIndexer<TFilter, TBlock, TRet>({
121
+ export function createIndexer<TFilter, TBlock, TTxnParams>({
101
122
  streamConfig,
102
123
  ...options
103
- }: IndexerWithStreamConfig<TFilter, TBlock, TRet>) {
104
- const indexer: Indexer<TFilter, TBlock, TRet> = {
124
+ }: IndexerWithStreamConfig<TFilter, TBlock, TTxnParams>) {
125
+ const indexer: Indexer<TFilter, TBlock, TTxnParams> = {
105
126
  options,
106
127
  streamConfig,
107
- hooks: createHooks<IndexerHooks<TFilter, TBlock, TRet>>(),
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, TRet>(
144
+ export async function run<TFilter, TBlock, TTxnParams>(
124
145
  client: Client<TFilter, TBlock>,
125
- indexer: Indexer<TFilter, TBlock, TRet>,
126
- sinkArg?: Sink,
146
+ indexer: Indexer<TFilter, TBlock, TTxnParams>,
127
147
  ) {
128
148
  await indexerAsyncContext.callAsync({}, async () => {
129
- await indexer.hooks.callHook("run:before");
149
+ const context = useIndexerContext<TTxnParams>();
130
150
 
131
- const sink = sinkArg ?? defaultSink();
151
+ const sink = indexer.options.sink ?? defaultSink();
132
152
 
133
- sink.hook("write", async ({ data }) => {
134
- await indexer.hooks.callHook("sink:write", { data: data as TRet[] });
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 = client.streamData(request, options);
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
- for await (const message of stream) {
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
- await indexer.hooks.callHook("connect:factory", {
221
- request,
222
- endCursor,
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
- // create new stream with new request
226
- stream = client.streamData(request, options);
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
- // change state to recover mode
229
- state = {
230
- _tag: "recover",
231
- data,
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
- return;
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
- try {
259
- const transformOutput = await indexer.options.transform({
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
- // write transformed data to output
267
- output.push(...transformOutput);
268
-
269
- await indexer.hooks.callHook("handler:after", { output });
270
- } catch (error) {
271
- assert(error instanceof Error);
272
- await indexer.hooks.callHook("handler:exception", {
273
- error,
274
- });
275
- throw error;
276
- }
277
-
278
- span.end();
279
- });
280
- }
281
-
282
- // if output has data, write it to sink
283
- if (output.length > 0) {
284
- await tracer.startActiveSpan("sink write", async (span) => {
285
- await sink.write({
286
- data: output as SinkData[],
287
- cursor,
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
- span.end();
293
- });
294
- }
295
- span.end();
296
+ }
297
+ },
298
+ );
299
+ await indexer.hooks.callHook("transaction:commit", {
300
+ finality,
301
+ endCursor,
296
302
  });
297
- break;
298
- }
299
- default: {
300
- consola.warn("unexpected message", message);
301
- throw new Error("not implemented");
302
- }
303
+ span.end();
304
+ });
305
+ break;
303
306
  }
304
-
305
- // if stream needs a restart
306
- // break out of the current stream iterator
307
- if (state._tag !== "normal") {
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
- // when restarting stream we continue while loop again
313
- if (state._tag !== "normal") {
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
  }
@@ -1,11 +1,11 @@
1
1
  import type { Indexer } from "../indexer";
2
2
 
3
- export type IndexerPlugin<TFilter, TBlock, TRet> = (
4
- indexer: Indexer<TFilter, TBlock, TRet>,
3
+ export type IndexerPlugin<TFilter, TBlock, TTxnParams> = (
4
+ indexer: Indexer<TFilter, TBlock, TTxnParams>,
5
5
  ) => void;
6
6
 
7
- export function defineIndexerPlugin<TFilter, TBlock, TRet>(
8
- def: IndexerPlugin<TFilter, TBlock, TRet>,
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, TRet>({
8
+ export function kv<TFilter, TBlock, TTxnParams>({
9
9
  database,
10
10
  }: { database: SqliteDatabase }) {
11
- return defineIndexerPlugin<TFilter, TBlock, TRet>((indexer) => {
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 { type MockRet, getMockIndexer } from "../testing/indexer";
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(getMockIndexer([persistence]));
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, TRet>({
6
+ export function sqlitePersistence<TFilter, TBlock, TTxnParams>({
7
7
  database,
8
8
  }: { database: SqliteDatabase }) {
9
- return defineIndexerPlugin<TFilter, TBlock, TRet>((indexer) => {
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("sink:flush", ({ endCursor }) => {
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 { Hookable } from "hookable";
2
+ import consola from "consola";
3
3
 
4
4
  export type SinkData = Record<string, unknown>;
5
5
 
6
- export interface SinkEvents {
7
- write({ data }: { data: SinkData[] }): void;
8
- flush({
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 extends Hookable<SinkEvents> {
22
- abstract write({
23
- data,
24
- cursor,
25
- endCursor,
26
- finality,
27
- }: SinkWriteArgs): Promise<void>;
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 write({ data, endCursor, finality }: SinkWriteArgs) {
32
- await this.callHook("write", { data });
33
- await this.callHook("flush", { endCursor, finality });
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
 
@@ -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 { type MockRet, getMockIndexer } from "../testing/indexer";
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<MockRet>({ filepath: "test.csv" });
35
- await run(client, getMockIndexer(), sink);
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