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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/index.cjs +270 -0
  2. package/dist/index.d.cts +3 -0
  3. package/dist/index.d.mts +3 -0
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.mjs +259 -0
  6. package/dist/internal/testing.cjs +109 -0
  7. package/dist/internal/testing.d.cts +40 -0
  8. package/dist/internal/testing.d.mts +40 -0
  9. package/dist/internal/testing.d.ts +40 -0
  10. package/dist/internal/testing.mjs +104 -0
  11. package/dist/plugins/index.cjs +43 -0
  12. package/dist/plugins/index.d.cts +18 -0
  13. package/dist/plugins/index.d.mts +18 -0
  14. package/dist/plugins/index.d.ts +18 -0
  15. package/dist/plugins/index.mjs +38 -0
  16. package/dist/shared/indexer.077335f3.cjs +15 -0
  17. package/dist/shared/indexer.2416906c.cjs +29 -0
  18. package/dist/shared/indexer.601ceab0.cjs +7 -0
  19. package/dist/shared/indexer.8939ecc8.d.cts +91 -0
  20. package/dist/shared/indexer.8939ecc8.d.mts +91 -0
  21. package/dist/shared/indexer.8939ecc8.d.ts +91 -0
  22. package/dist/shared/indexer.9b21ddd2.mjs +5 -0
  23. package/dist/shared/indexer.a55ad619.mjs +12 -0
  24. package/dist/shared/indexer.ff25c953.mjs +26 -0
  25. package/dist/testing/index.cjs +58 -0
  26. package/dist/testing/index.d.cts +12 -0
  27. package/dist/testing/index.d.mts +12 -0
  28. package/dist/testing/index.d.ts +12 -0
  29. package/dist/testing/index.mjs +52 -0
  30. package/dist/vcr/index.cjs +92 -0
  31. package/dist/vcr/index.d.cts +27 -0
  32. package/dist/vcr/index.d.mts +27 -0
  33. package/dist/vcr/index.d.ts +27 -0
  34. package/dist/vcr/index.mjs +78 -0
  35. package/package.json +31 -41
  36. package/src/compose.test.ts +76 -0
  37. package/src/compose.ts +71 -0
  38. package/src/context.ts +14 -8
  39. package/src/index.ts +0 -5
  40. package/src/indexer.test.ts +109 -186
  41. package/src/indexer.ts +244 -144
  42. package/src/internal/testing.ts +135 -0
  43. package/src/plugins/config.ts +4 -4
  44. package/src/plugins/index.ts +8 -1
  45. package/src/plugins/logger.ts +30 -0
  46. package/src/plugins/persistence.ts +24 -187
  47. package/src/testing/index.ts +50 -3
  48. package/src/vcr/record.ts +6 -4
  49. package/src/vcr/replay.ts +8 -18
  50. package/src/hooks/index.ts +0 -2
  51. package/src/hooks/useKVStore.ts +0 -12
  52. package/src/hooks/useSink.ts +0 -13
  53. package/src/plugins/kv.test.ts +0 -120
  54. package/src/plugins/kv.ts +0 -132
  55. package/src/plugins/persistence.test.ts +0 -151
  56. package/src/sink.ts +0 -36
  57. package/src/sinks/csv.test.ts +0 -65
  58. package/src/sinks/csv.ts +0 -159
  59. package/src/sinks/drizzle/Int8Range.ts +0 -52
  60. package/src/sinks/drizzle/delete.ts +0 -42
  61. package/src/sinks/drizzle/drizzle.test.ts +0 -239
  62. package/src/sinks/drizzle/drizzle.ts +0 -115
  63. package/src/sinks/drizzle/index.ts +0 -6
  64. package/src/sinks/drizzle/insert.ts +0 -39
  65. package/src/sinks/drizzle/select.ts +0 -44
  66. package/src/sinks/drizzle/transaction.ts +0 -49
  67. package/src/sinks/drizzle/update.ts +0 -47
  68. package/src/sinks/drizzle/utils.ts +0 -36
  69. package/src/sinks/sqlite.test.ts +0 -99
  70. package/src/sinks/sqlite.ts +0 -170
  71. package/src/testing/helper.ts +0 -13
  72. package/src/testing/indexer.ts +0 -35
  73. package/src/testing/setup.ts +0 -59
  74. package/src/testing/vcr.ts +0 -54
@@ -0,0 +1,30 @@
1
+ import { type ConsolaInstance, type ConsolaReporter, consola } from "consola";
2
+ import { useIndexerContext } from "../context";
3
+ import { defineIndexerPlugin } from "./config";
4
+
5
+ export type { ConsolaReporter, ConsolaInstance } from "consola";
6
+
7
+ export function logger<TFilter, TBlock, TTxnParams>({
8
+ logger,
9
+ }: { logger?: ConsolaReporter } = {}) {
10
+ return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
11
+ indexer.hooks.hook("run:before", () => {
12
+ const ctx = useIndexerContext();
13
+
14
+ if (logger) {
15
+ ctx.logger = consola.create({ reporters: [logger] });
16
+ } else {
17
+ ctx.logger = consola.create({});
18
+ }
19
+ });
20
+ });
21
+ }
22
+
23
+ export function useLogger(): ConsolaInstance {
24
+ const ctx = useIndexerContext();
25
+
26
+ if (!ctx?.logger)
27
+ throw new Error("Logger plugin is not available in context");
28
+
29
+ return ctx.logger;
30
+ }
@@ -1,202 +1,39 @@
1
- import type { Cursor } from "@apibara/protocol";
2
- import type { Database as SqliteDatabase, Statement } from "better-sqlite3";
3
- import { deserialize, serialize } from "../vcr";
4
- import { defineIndexerPlugin } from "./config";
1
+ import { type Cursor, isCursor } from "@apibara/protocol";
5
2
 
6
- export function sqlitePersistence<TFilter, TBlock, TTxnParams>({
7
- database,
8
- }: { database: SqliteDatabase }) {
9
- return defineIndexerPlugin<TFilter, TBlock, TTxnParams>((indexer) => {
10
- let store: SqlitePersistence<TFilter>;
3
+ import { defineIndexerPlugin } from "./config";
11
4
 
12
- indexer.hooks.hook("run:before", () => {
13
- SqlitePersistence.initialize(database);
14
-
15
- store = new SqlitePersistence(database);
16
- });
5
+ /**
6
+ * A plugin that persists the last cursor and filter to memory.
7
+ */
8
+ export function inMemoryPersistence<TFilter, TBlock>() {
9
+ return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
10
+ let lastCursor: Cursor | undefined;
11
+ let lastFilter: TFilter | undefined;
17
12
 
18
13
  indexer.hooks.hook("connect:before", ({ request }) => {
19
- const { cursor, filter } = store.get();
20
-
21
- if (cursor) {
22
- request.startingCursor = cursor;
14
+ if (lastCursor) {
15
+ request.startingCursor = lastCursor;
23
16
  }
24
17
 
25
- if (filter) {
26
- request.filter[1] = filter;
27
- }
28
- });
29
-
30
- indexer.hooks.hook("transaction:commit", ({ endCursor }) => {
31
- if (endCursor) {
32
- store.put({ cursor: endCursor });
18
+ if (lastFilter) {
19
+ request.filter[1] = lastFilter;
33
20
  }
34
21
  });
35
22
 
36
23
  indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
37
24
  if (request.filter[1]) {
38
- store.put({ cursor: endCursor, filter: request.filter[1] });
25
+ lastCursor = endCursor;
26
+ lastFilter = request.filter[1];
39
27
  }
40
28
  });
41
- });
42
- }
43
-
44
- export class SqlitePersistence<TFilter> {
45
- /** Sqlite Queries Prepare Statements */
46
- private _getCheckpointQuery: Statement<string, CheckpointRow>;
47
- private _putCheckpointQuery: Statement<
48
- [string, number, `0x${string}` | undefined]
49
- >;
50
- private _delCheckpointQuery: Statement<string>;
51
- private _getFilterQuery: Statement<string, FilterRow>;
52
- private _updateFilterToBlockQuery: Statement<[number, string]>;
53
- private _insertFilterQuery: Statement<[string, string, number]>;
54
- private _delFilterQuery: Statement<string>;
55
-
56
- constructor(private _db: SqliteDatabase) {
57
- this._getCheckpointQuery = this._db.prepare(statements.getCheckpoint);
58
- this._putCheckpointQuery = this._db.prepare(statements.putCheckpoint);
59
- this._delCheckpointQuery = this._db.prepare(statements.delCheckpoint);
60
- this._getFilterQuery = this._db.prepare(statements.getFilter);
61
- this._updateFilterToBlockQuery = this._db.prepare(
62
- statements.updateFilterToBlock,
63
- );
64
- this._insertFilterQuery = this._db.prepare(statements.insertFilter);
65
- this._delFilterQuery = this._db.prepare(statements.delFilter);
66
- }
67
-
68
- static initialize(db: SqliteDatabase) {
69
- db.prepare(statements.createCheckpointsTable).run();
70
- db.prepare(statements.createFiltersTable).run();
71
- }
72
-
73
- public get(): { cursor?: Cursor; filter?: TFilter } {
74
- const cursor = this._getCheckpoint();
75
- const filter = this._getFilter();
76
-
77
- return { cursor, filter };
78
- }
79
-
80
- public put({ cursor, filter }: { cursor?: Cursor; filter?: TFilter }) {
81
- if (cursor) {
82
- this._putCheckpoint(cursor);
83
-
84
- if (filter) {
85
- this._putFilter(filter, cursor);
86
- }
87
- }
88
- }
89
29
 
90
- public del() {
91
- this._delCheckpoint();
92
- this._delFilter();
93
- }
94
-
95
- // --- CHECKPOINTS TABLE METHODS ---
96
-
97
- private _getCheckpoint(): Cursor | undefined {
98
- const row = this._getCheckpointQuery.get("default");
99
-
100
- if (!row) return undefined;
101
-
102
- return { orderKey: BigInt(row.order_key), uniqueKey: row.unique_key };
103
- }
104
-
105
- private _putCheckpoint(cursor: Cursor) {
106
- this._putCheckpointQuery.run(
107
- "default",
108
- Number(cursor.orderKey),
109
- cursor.uniqueKey,
110
- );
111
- }
112
-
113
- private _delCheckpoint() {
114
- this._delCheckpointQuery.run("default");
115
- }
116
-
117
- // --- FILTERS TABLE METHODS ---
118
-
119
- private _getFilter(): TFilter | undefined {
120
- const row = this._getFilterQuery.get("default");
121
-
122
- if (!row) return undefined;
123
-
124
- return deserialize(row.filter) as TFilter;
125
- }
126
-
127
- private _putFilter(filter: TFilter, endCursor: Cursor) {
128
- this._updateFilterToBlockQuery.run(Number(endCursor.orderKey), "default");
129
- this._insertFilterQuery.run(
130
- "default",
131
- serialize(filter as Record<string, unknown>),
132
- Number(endCursor.orderKey),
133
- );
134
- }
135
-
136
- private _delFilter() {
137
- this._delFilterQuery.run("default");
138
- }
30
+ indexer.hooks.hook("handler:middleware", ({ use }) => {
31
+ use(async (context, next) => {
32
+ await next();
33
+ if (context.endCursor && isCursor(context.endCursor)) {
34
+ lastCursor = context.endCursor;
35
+ }
36
+ });
37
+ });
38
+ });
139
39
  }
140
-
141
- const statements = {
142
- beginTxn: "BEGIN TRANSACTION",
143
- commitTxn: "COMMIT TRANSACTION",
144
- rollbackTxn: "ROLLBACK TRANSACTION",
145
- createCheckpointsTable: `
146
- CREATE TABLE IF NOT EXISTS checkpoints (
147
- id TEXT NOT NULL PRIMARY KEY,
148
- order_key INTEGER NOT NULL,
149
- unique_key TEXT
150
- );`,
151
- createFiltersTable: `
152
- CREATE TABLE IF NOT EXISTS filters (
153
- id TEXT NOT NULL,
154
- filter BLOB NOT NULL,
155
- from_block INTEGER NOT NULL,
156
- to_block INTEGER,
157
- PRIMARY KEY (id, from_block)
158
- );`,
159
- getCheckpoint: `
160
- SELECT *
161
- FROM checkpoints
162
- WHERE id = ?`,
163
- putCheckpoint: `
164
- INSERT INTO checkpoints (id, order_key, unique_key)
165
- VALUES (?, ?, ?)
166
- ON CONFLICT(id) DO UPDATE SET
167
- order_key = excluded.order_key,
168
- unique_key = excluded.unique_key`,
169
- delCheckpoint: `
170
- DELETE FROM checkpoints
171
- WHERE id = ?`,
172
- getFilter: `
173
- SELECT *
174
- FROM filters
175
- WHERE id = ? AND to_block IS NULL`,
176
- updateFilterToBlock: `
177
- UPDATE filters
178
- SET to_block = ?
179
- WHERE id = ? AND to_block IS NULL`,
180
- insertFilter: `
181
- INSERT INTO filters (id, filter, from_block)
182
- VALUES (?, ?, ?)
183
- ON CONFLICT(id, from_block) DO UPDATE SET
184
- filter = excluded.filter,
185
- from_block = excluded.from_block`,
186
- delFilter: `
187
- DELETE FROM filters
188
- WHERE id = ?`,
189
- };
190
-
191
- export type CheckpointRow = {
192
- id: string;
193
- order_key: number;
194
- unique_key?: `0x${string}`;
195
- };
196
-
197
- export type FilterRow = {
198
- id: string;
199
- filter: string;
200
- from_block: number;
201
- to_block?: number;
202
- };
@@ -1,3 +1,50 @@
1
- export * from "./setup";
2
- export * from "./vcr";
3
- export * from "./helper";
1
+ import { createClient } from "@apibara/protocol";
2
+ import ci from "ci-info";
3
+ import { type IndexerWithStreamConfig, createIndexer } from "../indexer";
4
+ import { logger } from "../plugins/logger";
5
+ import type { CassetteOptions, VcrConfig } from "../vcr/config";
6
+ import { isCassetteAvailable } from "../vcr/helper";
7
+ import { record } from "../vcr/record";
8
+ import { replay } from "../vcr/replay";
9
+
10
+ export function createVcr() {
11
+ return {
12
+ async run<TFilter, TBlock>(
13
+ cassetteName: string,
14
+ indexerConfig: IndexerWithStreamConfig<TFilter, TBlock>,
15
+ range: { fromBlock: bigint; toBlock: bigint },
16
+ ) {
17
+ const vcrConfig: VcrConfig = {
18
+ cassetteDir: "cassettes",
19
+ };
20
+
21
+ const cassetteOptions: CassetteOptions = {
22
+ name: cassetteName,
23
+ startingCursor: {
24
+ orderKey: range.fromBlock,
25
+ },
26
+ endingCursor: {
27
+ orderKey: range.toBlock,
28
+ },
29
+ };
30
+
31
+ indexerConfig.plugins = [logger(), ...(indexerConfig.plugins ?? [])];
32
+
33
+ const indexer = createIndexer(indexerConfig);
34
+
35
+ if (!isCassetteAvailable(vcrConfig, cassetteName)) {
36
+ if (ci.isCI) {
37
+ throw new Error("Cannot record cassette in CI");
38
+ }
39
+
40
+ const client = createClient(
41
+ indexer.streamConfig,
42
+ indexer.options.streamUrl,
43
+ );
44
+ await record(vcrConfig, client, indexer, cassetteOptions);
45
+ } else {
46
+ await replay(vcrConfig, indexer, cassetteName);
47
+ }
48
+ },
49
+ };
50
+ }
package/src/vcr/record.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { type Indexer, run } from "@apibara/indexer";
4
3
  import type { Client, StreamDataResponse } from "@apibara/protocol";
5
- import { klona } from "klona/full";
4
+ import { type Indexer, run } from "../indexer";
6
5
  import type { CassetteOptions, VcrConfig } from "./config";
7
6
  import { serialize } from "./helper";
8
7
 
@@ -14,10 +13,9 @@ export type CassetteDataType<TFilter, TBlock> = {
14
13
  export async function record<TFilter, TBlock, TTxnParams>(
15
14
  vcrConfig: VcrConfig,
16
15
  client: Client<TFilter, TBlock>,
17
- indexerArg: Indexer<TFilter, TBlock, TTxnParams>,
16
+ indexer: Indexer<TFilter, TBlock>,
18
17
  cassetteOptions: CassetteOptions,
19
18
  ) {
20
- const indexer = klona(indexerArg);
21
19
  const messages: StreamDataResponse<TBlock>[] = [];
22
20
 
23
21
  indexer.hooks.addHooks({
@@ -33,10 +31,14 @@ export async function record<TFilter, TBlock, TTxnParams>(
33
31
  filter: indexer.options.filter,
34
32
  messages: messages,
35
33
  };
34
+
35
+ await fs.mkdir(vcrConfig.cassetteDir, { recursive: true });
36
+
36
37
  const filePath = path.join(
37
38
  vcrConfig.cassetteDir,
38
39
  `${cassetteOptions.name}.json`,
39
40
  );
41
+
40
42
  await fs.writeFile(filePath, serialize(output), { flag: "w" });
41
43
  },
42
44
  });
package/src/vcr/replay.ts CHANGED
@@ -1,34 +1,21 @@
1
1
  import assert from "node:assert";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import type { Client, Cursor } from "@apibara/protocol";
4
+ import type { Client } from "@apibara/protocol";
5
5
  import { MockClient } from "@apibara/protocol/testing";
6
6
  import { type Indexer, run } from "../indexer";
7
- import type { SinkData } from "../sink";
8
- import { vcr } from "../testing/vcr";
9
7
  import { type CassetteDataType, deserialize } from "../vcr";
10
8
  import type { VcrConfig } from "./config";
11
9
 
12
10
  export async function replay<TFilter, TBlock, TTxnParams>(
13
11
  vcrConfig: VcrConfig,
14
- indexer: Indexer<TFilter, TBlock, TTxnParams>,
12
+ indexer: Indexer<TFilter, TBlock>,
15
13
  cassetteName: string,
16
- ): Promise<VcrReplayResult> {
14
+ ) {
17
15
  const client = loadCassette<TFilter, TBlock>(vcrConfig, cassetteName);
18
-
19
- const sink = vcr();
20
-
21
16
  await run(client, indexer);
22
-
23
- return {
24
- outputs: sink.result,
25
- };
26
17
  }
27
18
 
28
- export type VcrReplayResult = {
29
- outputs: Array<{ endCursor?: Cursor; data: SinkData[] }>;
30
- };
31
-
32
19
  export function loadCassette<TFilter, TBlock>(
33
20
  vcrConfig: VcrConfig,
34
21
  cassetteName: string,
@@ -41,11 +28,14 @@ export function loadCassette<TFilter, TBlock>(
41
28
  const { filter, messages } = cassetteData;
42
29
 
43
30
  return new MockClient<TFilter, TBlock>((request, options) => {
31
+ // Notice that the request filter is an array of filters,
32
+ // so we need to wrap the indexer filter in an array.
44
33
  assert.deepStrictEqual(
45
34
  request.filter,
46
- filter,
47
- "Request and Cassette filter mismatch",
35
+ [filter],
36
+ "Indexer and cassette filter mismatch. Hint: delete the cassette and run again.",
48
37
  );
38
+
49
39
  return messages;
50
40
  });
51
41
  }
@@ -1,2 +0,0 @@
1
- export * from "./useKVStore";
2
- export * from "./useSink";
@@ -1,12 +0,0 @@
1
- import { useIndexerContext } from "../context";
2
- import type { KVStore } from "../plugins/kv";
3
-
4
- export type UseKVStoreResult = InstanceType<typeof KVStore>;
5
-
6
- export function useKVStore(): UseKVStoreResult {
7
- const ctx = useIndexerContext();
8
-
9
- if (!ctx?.kv) throw new Error("KV Plugin is not available in context!");
10
-
11
- return ctx.kv;
12
- }
@@ -1,13 +0,0 @@
1
- import type { IndexerContext } from "../context";
2
-
3
- export function useSink<TTxnParams>({
4
- context,
5
- }: {
6
- context: IndexerContext<TTxnParams>;
7
- }) {
8
- if (!context.sinkTransaction) {
9
- throw new Error("Transaction context doesn't exist!");
10
- }
11
-
12
- return context.sinkTransaction;
13
- }
@@ -1,120 +0,0 @@
1
- import Database, { type Database as SqliteDatabase } from "better-sqlite3";
2
- import { afterAll, beforeAll, describe, expect, it } from "vitest";
3
- import { KVStore } from "./kv";
4
-
5
- type ValueType = { data: bigint };
6
-
7
- type DatabaseRowType = {
8
- from_block: number;
9
- k: string;
10
- to_block: number;
11
- v: unknown;
12
- };
13
-
14
- describe("KVStore", () => {
15
- let db: SqliteDatabase;
16
- let store: KVStore;
17
- const key = "test_key";
18
-
19
- beforeAll(() => {
20
- db = new Database(":memory:");
21
- KVStore.initialize(db);
22
- store = new KVStore(db, "finalized", { orderKey: 5_000_000n });
23
- });
24
-
25
- afterAll(async () => {
26
- db.close();
27
- });
28
-
29
- it("should begin transaction", () => {
30
- store.beginTransaction();
31
- });
32
-
33
- it("should put and get a value", async () => {
34
- const value = { data: 0n };
35
-
36
- store.put<ValueType>(key, value);
37
- const result = store.get<ValueType>(key);
38
-
39
- expect(result).toEqual(value);
40
- });
41
-
42
- it("should commit transaction", async () => {
43
- store.commitTransaction();
44
-
45
- const value = { data: 0n };
46
-
47
- const result = store.get<ValueType>(key);
48
-
49
- expect(result).toEqual(value);
50
- });
51
-
52
- it("should return undefined for non-existing key", async () => {
53
- const result = store.get<ValueType>("non_existent_key");
54
- expect(result).toBeUndefined();
55
- });
56
-
57
- it("should begin transaction", async () => {
58
- store.beginTransaction();
59
- });
60
-
61
- it("should update an existing value", async () => {
62
- store = new KVStore(db, "finalized", { orderKey: 5_000_020n });
63
-
64
- const value = { data: 50n };
65
-
66
- store.put<ValueType>(key, value);
67
- const result = store.get<ValueType>(key);
68
-
69
- expect(result).toEqual(value);
70
- });
71
-
72
- it("should delete a value", async () => {
73
- store.del(key);
74
- const result = store.get<ValueType>(key);
75
-
76
- expect(result).toBeUndefined();
77
-
78
- const rows = db
79
- .prepare<string, DatabaseRowType>(
80
- `
81
- SELECT from_block, to_block, k, v
82
- FROM kvs
83
- WHERE k = ?
84
- `,
85
- )
86
- .all(key);
87
-
88
- // Check that the old is correctly marked with to_block
89
- expect(rows[0].to_block).toBe(Number(5_000_020n));
90
- });
91
-
92
- it("should rollback transaction", async () => {
93
- await store.rollbackTransaction();
94
- });
95
-
96
- it("should revert the changes to last commit", async () => {
97
- const rows = db
98
- .prepare(
99
- `
100
- SELECT from_block, to_block, k, v
101
- FROM kvs
102
- WHERE k = ?
103
- `,
104
- )
105
- .all([key]);
106
-
107
- expect(rows).toMatchInlineSnapshot(`
108
- [
109
- {
110
- "from_block": 5000000,
111
- "k": "test_key",
112
- "to_block": null,
113
- "v": "{
114
- "data": "0n"
115
- }",
116
- },
117
- ]
118
- `);
119
- });
120
- });
package/src/plugins/kv.ts DELETED
@@ -1,132 +0,0 @@
1
- import assert from "node:assert";
2
- import type { Cursor, DataFinality } from "@apibara/protocol";
3
- import type { Database as SqliteDatabase, Statement } from "better-sqlite3";
4
- import { useIndexerContext } from "../context";
5
- import { deserialize, serialize } from "../vcr";
6
- import { defineIndexerPlugin } from "./config";
7
-
8
- export function kv<TFilter, TBlock, TTxnParams>({
9
- database,
10
- }: { database: SqliteDatabase }) {
11
- return defineIndexerPlugin<TFilter, TBlock, TTxnParams>((indexer) => {
12
- indexer.hooks.hook("run:before", () => {
13
- KVStore.initialize(database);
14
- });
15
-
16
- indexer.hooks.hook("handler:before", ({ finality, endCursor }) => {
17
- const ctx = useIndexerContext();
18
-
19
- assert(endCursor, new Error("endCursor cannot be undefined"));
20
-
21
- ctx.kv = new KVStore(database, finality, endCursor);
22
-
23
- ctx.kv.beginTransaction();
24
- });
25
-
26
- indexer.hooks.hook("handler:after", () => {
27
- const ctx = useIndexerContext();
28
-
29
- ctx.kv.commitTransaction();
30
-
31
- ctx.kv = null;
32
- });
33
-
34
- indexer.hooks.hook("handler:exception", () => {
35
- const ctx = useIndexerContext();
36
-
37
- ctx.kv.rollbackTransaction();
38
-
39
- ctx.kv = null;
40
- });
41
- });
42
- }
43
-
44
- export class KVStore {
45
- /** Sqlite Queries Prepare Statements */
46
- private _beginTxnQuery: Statement;
47
- private _commitTxnQuery: Statement;
48
- private _rollbackTxnQuery: Statement;
49
- private _getQuery: Statement<string, { v: string }>;
50
- private _updateToBlockQuery: Statement<[number, string]>;
51
- private _insertIntoKvsQuery: Statement<[number, string, string]>;
52
- private _delQuery: Statement<[number, string]>;
53
-
54
- constructor(
55
- private _db: SqliteDatabase,
56
- private _finality: DataFinality,
57
- private _endCursor: Cursor,
58
- ) {
59
- this._beginTxnQuery = this._db.prepare(statements.beginTxn);
60
- this._commitTxnQuery = this._db.prepare(statements.commitTxn);
61
- this._rollbackTxnQuery = this._db.prepare(statements.rollbackTxn);
62
- this._getQuery = this._db.prepare(statements.get);
63
- this._updateToBlockQuery = this._db.prepare(statements.updateToBlock);
64
- this._insertIntoKvsQuery = this._db.prepare(statements.insertIntoKvs);
65
- this._delQuery = this._db.prepare(statements.del);
66
- }
67
-
68
- static initialize(db: SqliteDatabase) {
69
- db.prepare(statements.createTable).run();
70
- }
71
-
72
- beginTransaction() {
73
- this._beginTxnQuery.run();
74
- }
75
-
76
- commitTransaction() {
77
- this._commitTxnQuery.run();
78
- }
79
-
80
- rollbackTransaction() {
81
- this._rollbackTxnQuery.run();
82
- }
83
-
84
- get<T>(key: string): T {
85
- const row = this._getQuery.get(key);
86
-
87
- return row ? deserialize(row.v) : undefined;
88
- }
89
-
90
- put<T>(key: string, value: T) {
91
- this._updateToBlockQuery.run(Number(this._endCursor.orderKey), key);
92
-
93
- this._insertIntoKvsQuery.run(
94
- Number(this._endCursor.orderKey),
95
- key,
96
- serialize(value as Record<string, unknown>),
97
- );
98
- }
99
-
100
- del(key: string) {
101
- this._delQuery.run(Number(this._endCursor.orderKey), key);
102
- }
103
- }
104
-
105
- const statements = {
106
- beginTxn: "BEGIN TRANSACTION",
107
- commitTxn: "COMMIT TRANSACTION",
108
- rollbackTxn: "ROLLBACK TRANSACTION",
109
- createTable: `
110
- CREATE TABLE IF NOT EXISTS kvs (
111
- from_block INTEGER NOT NULL,
112
- to_block INTEGER,
113
- k TEXT NOT NULL,
114
- v BLOB NOT NULL,
115
- PRIMARY KEY (from_block, k)
116
- );`,
117
- get: `
118
- SELECT v
119
- FROM kvs
120
- WHERE k = ? AND to_block IS NULL`,
121
- updateToBlock: `
122
- UPDATE kvs
123
- SET to_block = ?
124
- WHERE k = ? AND to_block IS NULL`,
125
- insertIntoKvs: `
126
- INSERT INTO kvs (from_block, to_block, k, v)
127
- VALUES (?, NULL, ?, ?)`,
128
- del: `
129
- UPDATE kvs
130
- SET to_block = ?
131
- WHERE k = ? AND to_block IS NULL`,
132
- };