@apibara/plugin-sqlite 2.0.0-beta.27 → 2.0.0-beta.29

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 CHANGED
@@ -68,6 +68,21 @@ class KeyValueStore {
68
68
  this.db.prepare(statements$1.del).run(Number(this.endCursor.orderKey), key);
69
69
  }
70
70
  }
71
+ function finalizeKV(db, cursor) {
72
+ assertInTransaction(db);
73
+ db.prepare(statements$1.finalize).run(
74
+ Number(cursor.orderKey)
75
+ );
76
+ }
77
+ function invalidateKV(db, cursor) {
78
+ assertInTransaction(db);
79
+ db.prepare(statements$1.invalidateDelete).run(
80
+ Number(cursor.orderKey)
81
+ );
82
+ db.prepare(statements$1.invalidateUpdate).run(
83
+ Number(cursor.orderKey)
84
+ );
85
+ }
71
86
  const statements$1 = {
72
87
  createTable: `
73
88
  CREATE TABLE IF NOT EXISTS kvs (
@@ -91,40 +106,51 @@ const statements$1 = {
91
106
  del: `
92
107
  UPDATE kvs
93
108
  SET to_block = ?
94
- WHERE k = ? AND to_block IS NULL`
109
+ WHERE k = ? AND to_block IS NULL`,
110
+ finalize: `
111
+ DELETE FROM kvs
112
+ WHERE to_block <= ?`,
113
+ invalidateDelete: `
114
+ DELETE FROM kvs
115
+ WHERE from_block > ?`,
116
+ invalidateUpdate: `
117
+ UPDATE kvs
118
+ SET to_block = NULL
119
+ WHERE to_block > ?`
95
120
  };
96
121
 
97
- const DEFAULT_INDEXER_ID = "default";
98
122
  function initializePersistentState(db) {
99
123
  assertInTransaction(db);
100
124
  db.exec(statements.createCheckpointsTable);
101
125
  db.exec(statements.createFiltersTable);
102
126
  }
103
- function persistState(db, endCursor, filter) {
127
+ function persistState(props) {
128
+ const { db, endCursor, filter, indexerName } = props;
104
129
  assertInTransaction(db);
105
130
  db.prepare(statements.putCheckpoint).run(
106
- DEFAULT_INDEXER_ID,
131
+ indexerName,
107
132
  Number(endCursor.orderKey),
108
133
  endCursor.uniqueKey
109
134
  );
110
135
  if (filter) {
111
136
  db.prepare(statements.updateFilterToBlock).run(
112
137
  Number(endCursor.orderKey),
113
- DEFAULT_INDEXER_ID
138
+ indexerName
114
139
  );
115
140
  db.prepare(statements.insertFilter).run(
116
- DEFAULT_INDEXER_ID,
141
+ indexerName,
117
142
  serialize(filter),
118
143
  Number(endCursor.orderKey)
119
144
  );
120
145
  }
121
146
  }
122
- function getState(db) {
147
+ function getState(props) {
148
+ const { db, indexerName } = props;
123
149
  assertInTransaction(db);
124
150
  const storedCursor = db.prepare(
125
151
  statements.getCheckpoint
126
- ).get(DEFAULT_INDEXER_ID);
127
- const storedFilter = db.prepare(statements.getFilter).get(DEFAULT_INDEXER_ID);
152
+ ).get(indexerName);
153
+ const storedFilter = db.prepare(statements.getFilter).get(indexerName);
128
154
  let cursor;
129
155
  let filter;
130
156
  if (storedCursor?.order_key) {
@@ -138,6 +164,26 @@ function getState(db) {
138
164
  }
139
165
  return { cursor, filter };
140
166
  }
167
+ function finalizeState(props) {
168
+ const { cursor, db, indexerName } = props;
169
+ assertInTransaction(db);
170
+ db.prepare(statements.finalizeFilter).run(
171
+ indexerName,
172
+ Number(cursor.orderKey)
173
+ );
174
+ }
175
+ function invalidateState(props) {
176
+ const { cursor, db, indexerName } = props;
177
+ assertInTransaction(db);
178
+ db.prepare(statements.invalidateFilterDelete).run(
179
+ indexerName,
180
+ Number(cursor.orderKey)
181
+ );
182
+ db.prepare(statements.invalidateFilterUpdate).run(
183
+ indexerName,
184
+ Number(cursor.orderKey)
185
+ );
186
+ }
141
187
  const statements = {
142
188
  createCheckpointsTable: `
143
189
  CREATE TABLE IF NOT EXISTS checkpoints (
@@ -182,7 +228,17 @@ const statements = {
182
228
  from_block = excluded.from_block`,
183
229
  delFilter: `
184
230
  DELETE FROM filters
185
- WHERE id = ?`
231
+ WHERE id = ?`,
232
+ finalizeFilter: `
233
+ DELETE FROM filters
234
+ WHERE id = ? AND to_block <= ?`,
235
+ invalidateFilterDelete: `
236
+ DELETE FROM filters
237
+ WHERE id = ? AND from_block > ?`,
238
+ invalidateFilterUpdate: `
239
+ UPDATE filters
240
+ SET to_block = NULL
241
+ WHERE id = ? AND to_block > ?`
186
242
  };
187
243
 
188
244
  const KV_PROPERTY = "_kv_sqlite";
@@ -200,7 +256,8 @@ function sqliteStorage({
200
256
  persistState: enablePersistState = true,
201
257
  keyValueStore: enableKeyValueStore = true,
202
258
  serialize: serializeFn = serialize,
203
- deserialize: deserializeFn = deserialize
259
+ deserialize: deserializeFn = deserialize,
260
+ indexerName = "default"
204
261
  }) {
205
262
  return plugins.defineIndexerPlugin((indexer) => {
206
263
  indexer.hooks.hook("run:before", async () => {
@@ -218,7 +275,7 @@ function sqliteStorage({
218
275
  return;
219
276
  }
220
277
  return await withTransaction(database, async (db) => {
221
- const { cursor, filter } = getState(db);
278
+ const { cursor, filter } = getState({ db, indexerName });
222
279
  if (cursor) {
223
280
  request.startingCursor = cursor;
224
281
  }
@@ -227,6 +284,62 @@ function sqliteStorage({
227
284
  }
228
285
  });
229
286
  });
287
+ indexer.hooks.hook("connect:after", async ({ request }) => {
288
+ const cursor = request.startingCursor;
289
+ if (!cursor) {
290
+ return;
291
+ }
292
+ await withTransaction(database, async (db) => {
293
+ if (enablePersistState) {
294
+ invalidateState({ db, cursor, indexerName });
295
+ }
296
+ if (enableKeyValueStore) {
297
+ invalidateKV(db, cursor);
298
+ }
299
+ });
300
+ });
301
+ indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
302
+ if (!enablePersistState) {
303
+ return;
304
+ }
305
+ assertInTransaction(database);
306
+ if (endCursor && request.filter[1]) {
307
+ persistState({
308
+ db: database,
309
+ endCursor,
310
+ indexerName,
311
+ filter: request.filter[1]
312
+ });
313
+ }
314
+ });
315
+ indexer.hooks.hook("message:finalize", async ({ message }) => {
316
+ const { cursor } = message.finalize;
317
+ if (!cursor) {
318
+ throw new SqliteStorageError("finalized cursor is undefined");
319
+ }
320
+ await withTransaction(database, async (db) => {
321
+ if (enablePersistState) {
322
+ finalizeState({ db, cursor, indexerName });
323
+ }
324
+ if (enableKeyValueStore) {
325
+ finalizeKV(db, cursor);
326
+ }
327
+ });
328
+ });
329
+ indexer.hooks.hook("message:invalidate", async ({ message }) => {
330
+ const { cursor } = message.invalidate;
331
+ if (!cursor) {
332
+ throw new SqliteStorageError("invalidate cursor is undefined");
333
+ }
334
+ await withTransaction(database, async (db) => {
335
+ if (enablePersistState) {
336
+ invalidateState({ db, cursor, indexerName });
337
+ }
338
+ if (enableKeyValueStore) {
339
+ invalidateKV(db, cursor);
340
+ }
341
+ });
342
+ });
230
343
  indexer.hooks.hook("handler:middleware", ({ use }) => {
231
344
  use(async (ctx, next) => {
232
345
  if (!ctx.finality) {
@@ -249,7 +362,7 @@ function sqliteStorage({
249
362
  }
250
363
  await next();
251
364
  if (enablePersistState) {
252
- persistState(db, ctx.endCursor);
365
+ persistState({ db, endCursor: ctx.endCursor, indexerName });
253
366
  }
254
367
  if (enableKeyValueStore) {
255
368
  delete ctx[KV_PROPERTY];
@@ -257,15 +370,6 @@ function sqliteStorage({
257
370
  });
258
371
  });
259
372
  });
260
- indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
261
- if (!enablePersistState) {
262
- return;
263
- }
264
- assertInTransaction(database);
265
- if (endCursor && request.filter[1]) {
266
- persistState(database, endCursor, request.filter[1]);
267
- }
268
- });
269
373
  });
270
374
  }
271
375
 
package/dist/index.d.cts CHANGED
@@ -22,6 +22,7 @@ type SqliteStorageOptions = {
22
22
  database: Database;
23
23
  keyValueStore?: boolean;
24
24
  persistState?: boolean;
25
+ indexerName?: string;
25
26
  serialize?: SerializeFn;
26
27
  deserialize?: DeserializeFn;
27
28
  };
@@ -34,7 +35,8 @@ type SqliteStorageOptions = {
34
35
  * @param options.keyValueStore - Whether to enable the Key-Value store. Defaults to true.
35
36
  * @param options.serialize - A function to serialize the value to the KV.
36
37
  * @param options.deserialize - A function to deserialize the value from the KV.
38
+ * @param options.indexerName - The name of the indexer. Defaults value is 'default'.
37
39
  */
38
- declare function sqliteStorage<TFilter, TBlock>({ database, persistState: enablePersistState, keyValueStore: enableKeyValueStore, serialize: serializeFn, deserialize: deserializeFn, }: SqliteStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
40
+ declare function sqliteStorage<TFilter, TBlock>({ database, persistState: enablePersistState, keyValueStore: enableKeyValueStore, serialize: serializeFn, deserialize: deserializeFn, indexerName, }: SqliteStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
39
41
 
40
42
  export { KeyValueStore, type SqliteStorageOptions, sqliteStorage, useSqliteKeyValueStore };
package/dist/index.d.mts CHANGED
@@ -22,6 +22,7 @@ type SqliteStorageOptions = {
22
22
  database: Database;
23
23
  keyValueStore?: boolean;
24
24
  persistState?: boolean;
25
+ indexerName?: string;
25
26
  serialize?: SerializeFn;
26
27
  deserialize?: DeserializeFn;
27
28
  };
@@ -34,7 +35,8 @@ type SqliteStorageOptions = {
34
35
  * @param options.keyValueStore - Whether to enable the Key-Value store. Defaults to true.
35
36
  * @param options.serialize - A function to serialize the value to the KV.
36
37
  * @param options.deserialize - A function to deserialize the value from the KV.
38
+ * @param options.indexerName - The name of the indexer. Defaults value is 'default'.
37
39
  */
38
- declare function sqliteStorage<TFilter, TBlock>({ database, persistState: enablePersistState, keyValueStore: enableKeyValueStore, serialize: serializeFn, deserialize: deserializeFn, }: SqliteStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
40
+ declare function sqliteStorage<TFilter, TBlock>({ database, persistState: enablePersistState, keyValueStore: enableKeyValueStore, serialize: serializeFn, deserialize: deserializeFn, indexerName, }: SqliteStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
39
41
 
40
42
  export { KeyValueStore, type SqliteStorageOptions, sqliteStorage, useSqliteKeyValueStore };
package/dist/index.d.ts CHANGED
@@ -22,6 +22,7 @@ type SqliteStorageOptions = {
22
22
  database: Database;
23
23
  keyValueStore?: boolean;
24
24
  persistState?: boolean;
25
+ indexerName?: string;
25
26
  serialize?: SerializeFn;
26
27
  deserialize?: DeserializeFn;
27
28
  };
@@ -34,7 +35,8 @@ type SqliteStorageOptions = {
34
35
  * @param options.keyValueStore - Whether to enable the Key-Value store. Defaults to true.
35
36
  * @param options.serialize - A function to serialize the value to the KV.
36
37
  * @param options.deserialize - A function to deserialize the value from the KV.
38
+ * @param options.indexerName - The name of the indexer. Defaults value is 'default'.
37
39
  */
38
- declare function sqliteStorage<TFilter, TBlock>({ database, persistState: enablePersistState, keyValueStore: enableKeyValueStore, serialize: serializeFn, deserialize: deserializeFn, }: SqliteStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
40
+ declare function sqliteStorage<TFilter, TBlock>({ database, persistState: enablePersistState, keyValueStore: enableKeyValueStore, serialize: serializeFn, deserialize: deserializeFn, indexerName, }: SqliteStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
39
41
 
40
42
  export { KeyValueStore, type SqliteStorageOptions, sqliteStorage, useSqliteKeyValueStore };
package/dist/index.mjs CHANGED
@@ -66,6 +66,21 @@ class KeyValueStore {
66
66
  this.db.prepare(statements$1.del).run(Number(this.endCursor.orderKey), key);
67
67
  }
68
68
  }
69
+ function finalizeKV(db, cursor) {
70
+ assertInTransaction(db);
71
+ db.prepare(statements$1.finalize).run(
72
+ Number(cursor.orderKey)
73
+ );
74
+ }
75
+ function invalidateKV(db, cursor) {
76
+ assertInTransaction(db);
77
+ db.prepare(statements$1.invalidateDelete).run(
78
+ Number(cursor.orderKey)
79
+ );
80
+ db.prepare(statements$1.invalidateUpdate).run(
81
+ Number(cursor.orderKey)
82
+ );
83
+ }
69
84
  const statements$1 = {
70
85
  createTable: `
71
86
  CREATE TABLE IF NOT EXISTS kvs (
@@ -89,40 +104,51 @@ const statements$1 = {
89
104
  del: `
90
105
  UPDATE kvs
91
106
  SET to_block = ?
92
- WHERE k = ? AND to_block IS NULL`
107
+ WHERE k = ? AND to_block IS NULL`,
108
+ finalize: `
109
+ DELETE FROM kvs
110
+ WHERE to_block <= ?`,
111
+ invalidateDelete: `
112
+ DELETE FROM kvs
113
+ WHERE from_block > ?`,
114
+ invalidateUpdate: `
115
+ UPDATE kvs
116
+ SET to_block = NULL
117
+ WHERE to_block > ?`
93
118
  };
94
119
 
95
- const DEFAULT_INDEXER_ID = "default";
96
120
  function initializePersistentState(db) {
97
121
  assertInTransaction(db);
98
122
  db.exec(statements.createCheckpointsTable);
99
123
  db.exec(statements.createFiltersTable);
100
124
  }
101
- function persistState(db, endCursor, filter) {
125
+ function persistState(props) {
126
+ const { db, endCursor, filter, indexerName } = props;
102
127
  assertInTransaction(db);
103
128
  db.prepare(statements.putCheckpoint).run(
104
- DEFAULT_INDEXER_ID,
129
+ indexerName,
105
130
  Number(endCursor.orderKey),
106
131
  endCursor.uniqueKey
107
132
  );
108
133
  if (filter) {
109
134
  db.prepare(statements.updateFilterToBlock).run(
110
135
  Number(endCursor.orderKey),
111
- DEFAULT_INDEXER_ID
136
+ indexerName
112
137
  );
113
138
  db.prepare(statements.insertFilter).run(
114
- DEFAULT_INDEXER_ID,
139
+ indexerName,
115
140
  serialize(filter),
116
141
  Number(endCursor.orderKey)
117
142
  );
118
143
  }
119
144
  }
120
- function getState(db) {
145
+ function getState(props) {
146
+ const { db, indexerName } = props;
121
147
  assertInTransaction(db);
122
148
  const storedCursor = db.prepare(
123
149
  statements.getCheckpoint
124
- ).get(DEFAULT_INDEXER_ID);
125
- const storedFilter = db.prepare(statements.getFilter).get(DEFAULT_INDEXER_ID);
150
+ ).get(indexerName);
151
+ const storedFilter = db.prepare(statements.getFilter).get(indexerName);
126
152
  let cursor;
127
153
  let filter;
128
154
  if (storedCursor?.order_key) {
@@ -136,6 +162,26 @@ function getState(db) {
136
162
  }
137
163
  return { cursor, filter };
138
164
  }
165
+ function finalizeState(props) {
166
+ const { cursor, db, indexerName } = props;
167
+ assertInTransaction(db);
168
+ db.prepare(statements.finalizeFilter).run(
169
+ indexerName,
170
+ Number(cursor.orderKey)
171
+ );
172
+ }
173
+ function invalidateState(props) {
174
+ const { cursor, db, indexerName } = props;
175
+ assertInTransaction(db);
176
+ db.prepare(statements.invalidateFilterDelete).run(
177
+ indexerName,
178
+ Number(cursor.orderKey)
179
+ );
180
+ db.prepare(statements.invalidateFilterUpdate).run(
181
+ indexerName,
182
+ Number(cursor.orderKey)
183
+ );
184
+ }
139
185
  const statements = {
140
186
  createCheckpointsTable: `
141
187
  CREATE TABLE IF NOT EXISTS checkpoints (
@@ -180,7 +226,17 @@ const statements = {
180
226
  from_block = excluded.from_block`,
181
227
  delFilter: `
182
228
  DELETE FROM filters
183
- WHERE id = ?`
229
+ WHERE id = ?`,
230
+ finalizeFilter: `
231
+ DELETE FROM filters
232
+ WHERE id = ? AND to_block <= ?`,
233
+ invalidateFilterDelete: `
234
+ DELETE FROM filters
235
+ WHERE id = ? AND from_block > ?`,
236
+ invalidateFilterUpdate: `
237
+ UPDATE filters
238
+ SET to_block = NULL
239
+ WHERE id = ? AND to_block > ?`
184
240
  };
185
241
 
186
242
  const KV_PROPERTY = "_kv_sqlite";
@@ -198,7 +254,8 @@ function sqliteStorage({
198
254
  persistState: enablePersistState = true,
199
255
  keyValueStore: enableKeyValueStore = true,
200
256
  serialize: serializeFn = serialize,
201
- deserialize: deserializeFn = deserialize
257
+ deserialize: deserializeFn = deserialize,
258
+ indexerName = "default"
202
259
  }) {
203
260
  return defineIndexerPlugin((indexer) => {
204
261
  indexer.hooks.hook("run:before", async () => {
@@ -216,7 +273,7 @@ function sqliteStorage({
216
273
  return;
217
274
  }
218
275
  return await withTransaction(database, async (db) => {
219
- const { cursor, filter } = getState(db);
276
+ const { cursor, filter } = getState({ db, indexerName });
220
277
  if (cursor) {
221
278
  request.startingCursor = cursor;
222
279
  }
@@ -225,6 +282,62 @@ function sqliteStorage({
225
282
  }
226
283
  });
227
284
  });
285
+ indexer.hooks.hook("connect:after", async ({ request }) => {
286
+ const cursor = request.startingCursor;
287
+ if (!cursor) {
288
+ return;
289
+ }
290
+ await withTransaction(database, async (db) => {
291
+ if (enablePersistState) {
292
+ invalidateState({ db, cursor, indexerName });
293
+ }
294
+ if (enableKeyValueStore) {
295
+ invalidateKV(db, cursor);
296
+ }
297
+ });
298
+ });
299
+ indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
300
+ if (!enablePersistState) {
301
+ return;
302
+ }
303
+ assertInTransaction(database);
304
+ if (endCursor && request.filter[1]) {
305
+ persistState({
306
+ db: database,
307
+ endCursor,
308
+ indexerName,
309
+ filter: request.filter[1]
310
+ });
311
+ }
312
+ });
313
+ indexer.hooks.hook("message:finalize", async ({ message }) => {
314
+ const { cursor } = message.finalize;
315
+ if (!cursor) {
316
+ throw new SqliteStorageError("finalized cursor is undefined");
317
+ }
318
+ await withTransaction(database, async (db) => {
319
+ if (enablePersistState) {
320
+ finalizeState({ db, cursor, indexerName });
321
+ }
322
+ if (enableKeyValueStore) {
323
+ finalizeKV(db, cursor);
324
+ }
325
+ });
326
+ });
327
+ indexer.hooks.hook("message:invalidate", async ({ message }) => {
328
+ const { cursor } = message.invalidate;
329
+ if (!cursor) {
330
+ throw new SqliteStorageError("invalidate cursor is undefined");
331
+ }
332
+ await withTransaction(database, async (db) => {
333
+ if (enablePersistState) {
334
+ invalidateState({ db, cursor, indexerName });
335
+ }
336
+ if (enableKeyValueStore) {
337
+ invalidateKV(db, cursor);
338
+ }
339
+ });
340
+ });
228
341
  indexer.hooks.hook("handler:middleware", ({ use }) => {
229
342
  use(async (ctx, next) => {
230
343
  if (!ctx.finality) {
@@ -247,7 +360,7 @@ function sqliteStorage({
247
360
  }
248
361
  await next();
249
362
  if (enablePersistState) {
250
- persistState(db, ctx.endCursor);
363
+ persistState({ db, endCursor: ctx.endCursor, indexerName });
251
364
  }
252
365
  if (enableKeyValueStore) {
253
366
  delete ctx[KV_PROPERTY];
@@ -255,15 +368,6 @@ function sqliteStorage({
255
368
  });
256
369
  });
257
370
  });
258
- indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
259
- if (!enablePersistState) {
260
- return;
261
- }
262
- assertInTransaction(database);
263
- if (endCursor && request.filter[1]) {
264
- persistState(database, endCursor, request.filter[1]);
265
- }
266
- });
267
371
  });
268
372
  }
269
373
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apibara/plugin-sqlite",
3
- "version": "2.0.0-beta.27",
3
+ "version": "2.0.0-beta.29",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -35,7 +35,7 @@
35
35
  "better-sqlite3": "^9.0.0"
36
36
  },
37
37
  "dependencies": {
38
- "@apibara/indexer": "2.0.0-beta.28",
39
- "@apibara/protocol": "2.0.0-beta.28"
38
+ "@apibara/indexer": "2.0.0-beta.30",
39
+ "@apibara/protocol": "2.0.0-beta.30"
40
40
  }
41
41
  }
package/src/index.ts CHANGED
@@ -3,10 +3,17 @@ import { defineIndexerPlugin } from "@apibara/indexer/plugins";
3
3
  import { isCursor } from "@apibara/protocol";
4
4
  import type { Database as SqliteDatabase } from "better-sqlite3";
5
5
 
6
- import { KeyValueStore, initializeKeyValueStore } from "./kv";
7
6
  import {
7
+ KeyValueStore,
8
+ finalizeKV,
9
+ initializeKeyValueStore,
10
+ invalidateKV,
11
+ } from "./kv";
12
+ import {
13
+ finalizeState,
8
14
  getState,
9
15
  initializePersistentState,
16
+ invalidateState,
10
17
  persistState,
11
18
  } from "./persistence";
12
19
  import {
@@ -38,6 +45,7 @@ export type SqliteStorageOptions = {
38
45
  database: SqliteDatabase;
39
46
  keyValueStore?: boolean;
40
47
  persistState?: boolean;
48
+ indexerName?: string;
41
49
 
42
50
  serialize?: SerializeFn;
43
51
  deserialize?: DeserializeFn;
@@ -52,6 +60,7 @@ export type SqliteStorageOptions = {
52
60
  * @param options.keyValueStore - Whether to enable the Key-Value store. Defaults to true.
53
61
  * @param options.serialize - A function to serialize the value to the KV.
54
62
  * @param options.deserialize - A function to deserialize the value from the KV.
63
+ * @param options.indexerName - The name of the indexer. Defaults value is 'default'.
55
64
  */
56
65
  export function sqliteStorage<TFilter, TBlock>({
57
66
  database,
@@ -59,6 +68,7 @@ export function sqliteStorage<TFilter, TBlock>({
59
68
  keyValueStore: enableKeyValueStore = true,
60
69
  serialize: serializeFn = serialize,
61
70
  deserialize: deserializeFn = deserialize,
71
+ indexerName = "default",
62
72
  }: SqliteStorageOptions) {
63
73
  return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
64
74
  indexer.hooks.hook("run:before", async () => {
@@ -79,7 +89,7 @@ export function sqliteStorage<TFilter, TBlock>({
79
89
  }
80
90
 
81
91
  return await withTransaction(database, async (db) => {
82
- const { cursor, filter } = getState<TFilter>(db);
92
+ const { cursor, filter } = getState<TFilter>({ db, indexerName });
83
93
 
84
94
  if (cursor) {
85
95
  request.startingCursor = cursor;
@@ -91,6 +101,80 @@ export function sqliteStorage<TFilter, TBlock>({
91
101
  });
92
102
  });
93
103
 
104
+ indexer.hooks.hook("connect:after", async ({ request }) => {
105
+ // On restart, we need to invalidate data for blocks that were processed but not persisted.
106
+ const cursor = request.startingCursor;
107
+
108
+ if (!cursor) {
109
+ return;
110
+ }
111
+
112
+ await withTransaction(database, async (db) => {
113
+ if (enablePersistState) {
114
+ invalidateState({ db, cursor, indexerName });
115
+ }
116
+
117
+ if (enableKeyValueStore) {
118
+ invalidateKV(db, cursor);
119
+ }
120
+ });
121
+ });
122
+
123
+ indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
124
+ if (!enablePersistState) {
125
+ return;
126
+ }
127
+
128
+ // The connect factory hook is called while indexing a block, so the database should be in a transaction
129
+ // created by the middleware.
130
+ assertInTransaction(database);
131
+
132
+ if (endCursor && request.filter[1]) {
133
+ persistState({
134
+ db: database,
135
+ endCursor,
136
+ indexerName,
137
+ filter: request.filter[1],
138
+ });
139
+ }
140
+ });
141
+
142
+ indexer.hooks.hook("message:finalize", async ({ message }) => {
143
+ const { cursor } = message.finalize;
144
+
145
+ if (!cursor) {
146
+ throw new SqliteStorageError("finalized cursor is undefined");
147
+ }
148
+
149
+ await withTransaction(database, async (db) => {
150
+ if (enablePersistState) {
151
+ finalizeState({ db, cursor, indexerName });
152
+ }
153
+
154
+ if (enableKeyValueStore) {
155
+ finalizeKV(db, cursor);
156
+ }
157
+ });
158
+ });
159
+
160
+ indexer.hooks.hook("message:invalidate", async ({ message }) => {
161
+ const { cursor } = message.invalidate;
162
+
163
+ if (!cursor) {
164
+ throw new SqliteStorageError("invalidate cursor is undefined");
165
+ }
166
+
167
+ await withTransaction(database, async (db) => {
168
+ if (enablePersistState) {
169
+ invalidateState({ db, cursor, indexerName });
170
+ }
171
+
172
+ if (enableKeyValueStore) {
173
+ invalidateKV(db, cursor);
174
+ }
175
+ });
176
+ });
177
+
94
178
  indexer.hooks.hook("handler:middleware", ({ use }) => {
95
179
  use(async (ctx, next) => {
96
180
  if (!ctx.finality) {
@@ -117,7 +201,7 @@ export function sqliteStorage<TFilter, TBlock>({
117
201
  await next();
118
202
 
119
203
  if (enablePersistState) {
120
- persistState(db, ctx.endCursor);
204
+ persistState({ db, endCursor: ctx.endCursor, indexerName });
121
205
  }
122
206
 
123
207
  if (enableKeyValueStore) {
@@ -126,19 +210,5 @@ export function sqliteStorage<TFilter, TBlock>({
126
210
  });
127
211
  });
128
212
  });
129
-
130
- indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
131
- if (!enablePersistState) {
132
- return;
133
- }
134
-
135
- // The connect factory hook is called while indexing a block, so the database should be in a transaction
136
- // created by the middleware.
137
- assertInTransaction(database);
138
-
139
- if (endCursor && request.filter[1]) {
140
- persistState(database, endCursor, request.filter[1]);
141
- }
142
- });
143
213
  });
144
214
  }
package/src/kv.ts CHANGED
@@ -50,6 +50,28 @@ export class KeyValueStore {
50
50
  }
51
51
  }
52
52
 
53
+ export function finalizeKV(db: Database, cursor: Cursor) {
54
+ assertInTransaction(db);
55
+
56
+ db.prepare<[number], KeyValueRow>(statements.finalize).run(
57
+ Number(cursor.orderKey),
58
+ );
59
+ }
60
+
61
+ export function invalidateKV(db: Database, cursor: Cursor) {
62
+ assertInTransaction(db);
63
+
64
+ // Delete entries that started after the invalidation cursor
65
+ db.prepare<[number], KeyValueRow>(statements.invalidateDelete).run(
66
+ Number(cursor.orderKey),
67
+ );
68
+
69
+ // Update entries that were supposed to end after the invalidation cursor
70
+ db.prepare<[number], KeyValueRow>(statements.invalidateUpdate).run(
71
+ Number(cursor.orderKey),
72
+ );
73
+ }
74
+
53
75
  type KeyValueRow = {
54
76
  from_block: number;
55
77
  to_block: number;
@@ -81,4 +103,14 @@ const statements = {
81
103
  UPDATE kvs
82
104
  SET to_block = ?
83
105
  WHERE k = ? AND to_block IS NULL`,
106
+ finalize: `
107
+ DELETE FROM kvs
108
+ WHERE to_block <= ?`,
109
+ invalidateDelete: `
110
+ DELETE FROM kvs
111
+ WHERE from_block > ?`,
112
+ invalidateUpdate: `
113
+ UPDATE kvs
114
+ SET to_block = NULL
115
+ WHERE to_block > ?`,
84
116
  };
@@ -3,23 +3,24 @@ import type { Database } from "better-sqlite3";
3
3
 
4
4
  import { assertInTransaction, deserialize, serialize } from "./utils";
5
5
 
6
- const DEFAULT_INDEXER_ID = "default";
7
-
8
6
  export function initializePersistentState(db: Database) {
9
7
  assertInTransaction(db);
10
8
  db.exec(statements.createCheckpointsTable);
11
9
  db.exec(statements.createFiltersTable);
12
10
  }
13
11
 
14
- export function persistState<TFilter>(
15
- db: Database,
16
- endCursor: Cursor,
17
- filter?: TFilter,
18
- ) {
12
+ export function persistState<TFilter>(props: {
13
+ db: Database;
14
+ endCursor: Cursor;
15
+ filter?: TFilter;
16
+ indexerName: string;
17
+ }) {
18
+ const { db, endCursor, filter, indexerName } = props;
19
+
19
20
  assertInTransaction(db);
20
21
 
21
22
  db.prepare(statements.putCheckpoint).run(
22
- DEFAULT_INDEXER_ID,
23
+ indexerName,
23
24
  Number(endCursor.orderKey),
24
25
  endCursor.uniqueKey,
25
26
  );
@@ -27,26 +28,30 @@ export function persistState<TFilter>(
27
28
  if (filter) {
28
29
  db.prepare(statements.updateFilterToBlock).run(
29
30
  Number(endCursor.orderKey),
30
- DEFAULT_INDEXER_ID,
31
+ indexerName,
31
32
  );
32
33
  db.prepare(statements.insertFilter).run(
33
- DEFAULT_INDEXER_ID,
34
+ indexerName,
34
35
  serialize(filter as Record<string, unknown>),
35
36
  Number(endCursor.orderKey),
36
37
  );
37
38
  }
38
39
  }
39
40
 
40
- export function getState<TFilter>(db: Database) {
41
+ export function getState<TFilter>(props: {
42
+ db: Database;
43
+ indexerName: string;
44
+ }) {
45
+ const { db, indexerName } = props;
41
46
  assertInTransaction(db);
42
47
  const storedCursor = db
43
48
  .prepare<string, { order_key?: number; unique_key?: string }>(
44
49
  statements.getCheckpoint,
45
50
  )
46
- .get(DEFAULT_INDEXER_ID);
51
+ .get(indexerName);
47
52
  const storedFilter = db
48
53
  .prepare<string, { filter: string }>(statements.getFilter)
49
- .get(DEFAULT_INDEXER_ID);
54
+ .get(indexerName);
50
55
 
51
56
  let cursor: Cursor | undefined;
52
57
  let filter: TFilter | undefined;
@@ -65,6 +70,36 @@ export function getState<TFilter>(db: Database) {
65
70
  return { cursor, filter };
66
71
  }
67
72
 
73
+ export function finalizeState(props: {
74
+ db: Database;
75
+ cursor: Cursor;
76
+ indexerName: string;
77
+ }) {
78
+ const { cursor, db, indexerName } = props;
79
+ assertInTransaction(db);
80
+ db.prepare<[string, number]>(statements.finalizeFilter).run(
81
+ indexerName,
82
+ Number(cursor.orderKey),
83
+ );
84
+ }
85
+
86
+ export function invalidateState(props: {
87
+ db: Database;
88
+ cursor: Cursor;
89
+ indexerName: string;
90
+ }) {
91
+ const { cursor, db, indexerName } = props;
92
+ assertInTransaction(db);
93
+ db.prepare<[string, number]>(statements.invalidateFilterDelete).run(
94
+ indexerName,
95
+ Number(cursor.orderKey),
96
+ );
97
+ db.prepare<[string, number]>(statements.invalidateFilterUpdate).run(
98
+ indexerName,
99
+ Number(cursor.orderKey),
100
+ );
101
+ }
102
+
68
103
  const statements = {
69
104
  createCheckpointsTable: `
70
105
  CREATE TABLE IF NOT EXISTS checkpoints (
@@ -110,4 +145,14 @@ const statements = {
110
145
  delFilter: `
111
146
  DELETE FROM filters
112
147
  WHERE id = ?`,
148
+ finalizeFilter: `
149
+ DELETE FROM filters
150
+ WHERE id = ? AND to_block <= ?`,
151
+ invalidateFilterDelete: `
152
+ DELETE FROM filters
153
+ WHERE id = ? AND from_block > ?`,
154
+ invalidateFilterUpdate: `
155
+ UPDATE filters
156
+ SET to_block = NULL
157
+ WHERE id = ? AND to_block > ?`,
113
158
  };