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

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,7 +106,17 @@ 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
122
  const DEFAULT_INDEXER_ID = "default";
@@ -100,31 +125,33 @@ function initializePersistentState(db) {
100
125
  db.exec(statements.createCheckpointsTable);
101
126
  db.exec(statements.createFiltersTable);
102
127
  }
103
- function persistState(db, endCursor, filter) {
128
+ function persistState(props) {
129
+ const { db, endCursor, filter, indexerName = DEFAULT_INDEXER_ID } = props;
104
130
  assertInTransaction(db);
105
131
  db.prepare(statements.putCheckpoint).run(
106
- DEFAULT_INDEXER_ID,
132
+ indexerName,
107
133
  Number(endCursor.orderKey),
108
134
  endCursor.uniqueKey
109
135
  );
110
136
  if (filter) {
111
137
  db.prepare(statements.updateFilterToBlock).run(
112
138
  Number(endCursor.orderKey),
113
- DEFAULT_INDEXER_ID
139
+ indexerName
114
140
  );
115
141
  db.prepare(statements.insertFilter).run(
116
- DEFAULT_INDEXER_ID,
142
+ indexerName,
117
143
  serialize(filter),
118
144
  Number(endCursor.orderKey)
119
145
  );
120
146
  }
121
147
  }
122
- function getState(db) {
148
+ function getState(props) {
149
+ const { db, indexerName = DEFAULT_INDEXER_ID } = props;
123
150
  assertInTransaction(db);
124
151
  const storedCursor = db.prepare(
125
152
  statements.getCheckpoint
126
- ).get(DEFAULT_INDEXER_ID);
127
- const storedFilter = db.prepare(statements.getFilter).get(DEFAULT_INDEXER_ID);
153
+ ).get(indexerName);
154
+ const storedFilter = db.prepare(statements.getFilter).get(indexerName);
128
155
  let cursor;
129
156
  let filter;
130
157
  if (storedCursor?.order_key) {
@@ -138,6 +165,26 @@ function getState(db) {
138
165
  }
139
166
  return { cursor, filter };
140
167
  }
168
+ function finalizeState(props) {
169
+ const { cursor, db, indexerName = DEFAULT_INDEXER_ID } = props;
170
+ assertInTransaction(db);
171
+ db.prepare(statements.finalizeFilter).run(
172
+ indexerName,
173
+ Number(cursor.orderKey)
174
+ );
175
+ }
176
+ function invalidateState(props) {
177
+ const { cursor, db, indexerName = DEFAULT_INDEXER_ID } = props;
178
+ assertInTransaction(db);
179
+ db.prepare(statements.invalidateFilterDelete).run(
180
+ indexerName,
181
+ Number(cursor.orderKey)
182
+ );
183
+ db.prepare(statements.invalidateFilterUpdate).run(
184
+ indexerName,
185
+ Number(cursor.orderKey)
186
+ );
187
+ }
141
188
  const statements = {
142
189
  createCheckpointsTable: `
143
190
  CREATE TABLE IF NOT EXISTS checkpoints (
@@ -182,7 +229,17 @@ const statements = {
182
229
  from_block = excluded.from_block`,
183
230
  delFilter: `
184
231
  DELETE FROM filters
185
- WHERE id = ?`
232
+ WHERE id = ?`,
233
+ finalizeFilter: `
234
+ DELETE FROM filters
235
+ WHERE id = ? AND to_block < ?`,
236
+ invalidateFilterDelete: `
237
+ DELETE FROM filters
238
+ WHERE id = ? AND from_block > ?`,
239
+ invalidateFilterUpdate: `
240
+ UPDATE filters
241
+ SET to_block = NULL
242
+ WHERE id = ? AND to_block > ?`
186
243
  };
187
244
 
188
245
  const KV_PROPERTY = "_kv_sqlite";
@@ -200,7 +257,8 @@ function sqliteStorage({
200
257
  persistState: enablePersistState = true,
201
258
  keyValueStore: enableKeyValueStore = true,
202
259
  serialize: serializeFn = serialize,
203
- deserialize: deserializeFn = deserialize
260
+ deserialize: deserializeFn = deserialize,
261
+ indexerName
204
262
  }) {
205
263
  return plugins.defineIndexerPlugin((indexer) => {
206
264
  indexer.hooks.hook("run:before", async () => {
@@ -218,7 +276,7 @@ function sqliteStorage({
218
276
  return;
219
277
  }
220
278
  return await withTransaction(database, async (db) => {
221
- const { cursor, filter } = getState(db);
279
+ const { cursor, filter } = getState({ db, indexerName });
222
280
  if (cursor) {
223
281
  request.startingCursor = cursor;
224
282
  }
@@ -227,6 +285,62 @@ function sqliteStorage({
227
285
  }
228
286
  });
229
287
  });
288
+ indexer.hooks.hook("connect:after", async ({ request }) => {
289
+ const cursor = request.startingCursor;
290
+ if (!cursor) {
291
+ return;
292
+ }
293
+ await withTransaction(database, async (db) => {
294
+ if (enablePersistState) {
295
+ invalidateState({ db, cursor, indexerName });
296
+ }
297
+ if (enableKeyValueStore) {
298
+ invalidateKV(db, cursor);
299
+ }
300
+ });
301
+ });
302
+ indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
303
+ if (!enablePersistState) {
304
+ return;
305
+ }
306
+ assertInTransaction(database);
307
+ if (endCursor && request.filter[1]) {
308
+ persistState({
309
+ db: database,
310
+ endCursor,
311
+ indexerName,
312
+ filter: request.filter[1]
313
+ });
314
+ }
315
+ });
316
+ indexer.hooks.hook("message:finalize", async ({ message }) => {
317
+ const { cursor } = message.finalize;
318
+ if (!cursor) {
319
+ throw new SqliteStorageError("finalized cursor is undefined");
320
+ }
321
+ await withTransaction(database, async (db) => {
322
+ if (enablePersistState) {
323
+ finalizeState({ db, cursor, indexerName });
324
+ }
325
+ if (enableKeyValueStore) {
326
+ finalizeKV(db, cursor);
327
+ }
328
+ });
329
+ });
330
+ indexer.hooks.hook("message:invalidate", async ({ message }) => {
331
+ const { cursor } = message.invalidate;
332
+ if (!cursor) {
333
+ throw new SqliteStorageError("invalidate cursor is undefined");
334
+ }
335
+ await withTransaction(database, async (db) => {
336
+ if (enablePersistState) {
337
+ invalidateState({ db, cursor, indexerName });
338
+ }
339
+ if (enableKeyValueStore) {
340
+ invalidateKV(db, cursor);
341
+ }
342
+ });
343
+ });
230
344
  indexer.hooks.hook("handler:middleware", ({ use }) => {
231
345
  use(async (ctx, next) => {
232
346
  if (!ctx.finality) {
@@ -249,7 +363,7 @@ function sqliteStorage({
249
363
  }
250
364
  await next();
251
365
  if (enablePersistState) {
252
- persistState(db, ctx.endCursor);
366
+ persistState({ db, endCursor: ctx.endCursor, indexerName });
253
367
  }
254
368
  if (enableKeyValueStore) {
255
369
  delete ctx[KV_PROPERTY];
@@ -257,15 +371,6 @@ function sqliteStorage({
257
371
  });
258
372
  });
259
373
  });
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
374
  });
270
375
  }
271
376
 
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,7 +104,17 @@ 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
120
  const DEFAULT_INDEXER_ID = "default";
@@ -98,31 +123,33 @@ function initializePersistentState(db) {
98
123
  db.exec(statements.createCheckpointsTable);
99
124
  db.exec(statements.createFiltersTable);
100
125
  }
101
- function persistState(db, endCursor, filter) {
126
+ function persistState(props) {
127
+ const { db, endCursor, filter, indexerName = DEFAULT_INDEXER_ID } = props;
102
128
  assertInTransaction(db);
103
129
  db.prepare(statements.putCheckpoint).run(
104
- DEFAULT_INDEXER_ID,
130
+ indexerName,
105
131
  Number(endCursor.orderKey),
106
132
  endCursor.uniqueKey
107
133
  );
108
134
  if (filter) {
109
135
  db.prepare(statements.updateFilterToBlock).run(
110
136
  Number(endCursor.orderKey),
111
- DEFAULT_INDEXER_ID
137
+ indexerName
112
138
  );
113
139
  db.prepare(statements.insertFilter).run(
114
- DEFAULT_INDEXER_ID,
140
+ indexerName,
115
141
  serialize(filter),
116
142
  Number(endCursor.orderKey)
117
143
  );
118
144
  }
119
145
  }
120
- function getState(db) {
146
+ function getState(props) {
147
+ const { db, indexerName = DEFAULT_INDEXER_ID } = props;
121
148
  assertInTransaction(db);
122
149
  const storedCursor = db.prepare(
123
150
  statements.getCheckpoint
124
- ).get(DEFAULT_INDEXER_ID);
125
- const storedFilter = db.prepare(statements.getFilter).get(DEFAULT_INDEXER_ID);
151
+ ).get(indexerName);
152
+ const storedFilter = db.prepare(statements.getFilter).get(indexerName);
126
153
  let cursor;
127
154
  let filter;
128
155
  if (storedCursor?.order_key) {
@@ -136,6 +163,26 @@ function getState(db) {
136
163
  }
137
164
  return { cursor, filter };
138
165
  }
166
+ function finalizeState(props) {
167
+ const { cursor, db, indexerName = DEFAULT_INDEXER_ID } = props;
168
+ assertInTransaction(db);
169
+ db.prepare(statements.finalizeFilter).run(
170
+ indexerName,
171
+ Number(cursor.orderKey)
172
+ );
173
+ }
174
+ function invalidateState(props) {
175
+ const { cursor, db, indexerName = DEFAULT_INDEXER_ID } = props;
176
+ assertInTransaction(db);
177
+ db.prepare(statements.invalidateFilterDelete).run(
178
+ indexerName,
179
+ Number(cursor.orderKey)
180
+ );
181
+ db.prepare(statements.invalidateFilterUpdate).run(
182
+ indexerName,
183
+ Number(cursor.orderKey)
184
+ );
185
+ }
139
186
  const statements = {
140
187
  createCheckpointsTable: `
141
188
  CREATE TABLE IF NOT EXISTS checkpoints (
@@ -180,7 +227,17 @@ const statements = {
180
227
  from_block = excluded.from_block`,
181
228
  delFilter: `
182
229
  DELETE FROM filters
183
- WHERE id = ?`
230
+ WHERE id = ?`,
231
+ finalizeFilter: `
232
+ DELETE FROM filters
233
+ WHERE id = ? AND to_block < ?`,
234
+ invalidateFilterDelete: `
235
+ DELETE FROM filters
236
+ WHERE id = ? AND from_block > ?`,
237
+ invalidateFilterUpdate: `
238
+ UPDATE filters
239
+ SET to_block = NULL
240
+ WHERE id = ? AND to_block > ?`
184
241
  };
185
242
 
186
243
  const KV_PROPERTY = "_kv_sqlite";
@@ -198,7 +255,8 @@ function sqliteStorage({
198
255
  persistState: enablePersistState = true,
199
256
  keyValueStore: enableKeyValueStore = true,
200
257
  serialize: serializeFn = serialize,
201
- deserialize: deserializeFn = deserialize
258
+ deserialize: deserializeFn = deserialize,
259
+ indexerName
202
260
  }) {
203
261
  return defineIndexerPlugin((indexer) => {
204
262
  indexer.hooks.hook("run:before", async () => {
@@ -216,7 +274,7 @@ function sqliteStorage({
216
274
  return;
217
275
  }
218
276
  return await withTransaction(database, async (db) => {
219
- const { cursor, filter } = getState(db);
277
+ const { cursor, filter } = getState({ db, indexerName });
220
278
  if (cursor) {
221
279
  request.startingCursor = cursor;
222
280
  }
@@ -225,6 +283,62 @@ function sqliteStorage({
225
283
  }
226
284
  });
227
285
  });
286
+ indexer.hooks.hook("connect:after", async ({ request }) => {
287
+ const cursor = request.startingCursor;
288
+ if (!cursor) {
289
+ return;
290
+ }
291
+ await withTransaction(database, async (db) => {
292
+ if (enablePersistState) {
293
+ invalidateState({ db, cursor, indexerName });
294
+ }
295
+ if (enableKeyValueStore) {
296
+ invalidateKV(db, cursor);
297
+ }
298
+ });
299
+ });
300
+ indexer.hooks.hook("connect:factory", ({ request, endCursor }) => {
301
+ if (!enablePersistState) {
302
+ return;
303
+ }
304
+ assertInTransaction(database);
305
+ if (endCursor && request.filter[1]) {
306
+ persistState({
307
+ db: database,
308
+ endCursor,
309
+ indexerName,
310
+ filter: request.filter[1]
311
+ });
312
+ }
313
+ });
314
+ indexer.hooks.hook("message:finalize", async ({ message }) => {
315
+ const { cursor } = message.finalize;
316
+ if (!cursor) {
317
+ throw new SqliteStorageError("finalized cursor is undefined");
318
+ }
319
+ await withTransaction(database, async (db) => {
320
+ if (enablePersistState) {
321
+ finalizeState({ db, cursor, indexerName });
322
+ }
323
+ if (enableKeyValueStore) {
324
+ finalizeKV(db, cursor);
325
+ }
326
+ });
327
+ });
328
+ indexer.hooks.hook("message:invalidate", async ({ message }) => {
329
+ const { cursor } = message.invalidate;
330
+ if (!cursor) {
331
+ throw new SqliteStorageError("invalidate cursor is undefined");
332
+ }
333
+ await withTransaction(database, async (db) => {
334
+ if (enablePersistState) {
335
+ invalidateState({ db, cursor, indexerName });
336
+ }
337
+ if (enableKeyValueStore) {
338
+ invalidateKV(db, cursor);
339
+ }
340
+ });
341
+ });
228
342
  indexer.hooks.hook("handler:middleware", ({ use }) => {
229
343
  use(async (ctx, next) => {
230
344
  if (!ctx.finality) {
@@ -247,7 +361,7 @@ function sqliteStorage({
247
361
  }
248
362
  await next();
249
363
  if (enablePersistState) {
250
- persistState(db, ctx.endCursor);
364
+ persistState({ db, endCursor: ctx.endCursor, indexerName });
251
365
  }
252
366
  if (enableKeyValueStore) {
253
367
  delete ctx[KV_PROPERTY];
@@ -255,15 +369,6 @@ function sqliteStorage({
255
369
  });
256
370
  });
257
371
  });
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
372
  });
268
373
  }
269
374
 
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.28",
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.29",
39
+ "@apibara/protocol": "2.0.0-beta.29"
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,
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
  };
@@ -11,15 +11,18 @@ export function initializePersistentState(db: Database) {
11
11
  db.exec(statements.createFiltersTable);
12
12
  }
13
13
 
14
- export function persistState<TFilter>(
15
- db: Database,
16
- endCursor: Cursor,
17
- filter?: TFilter,
18
- ) {
14
+ export function persistState<TFilter>(props: {
15
+ db: Database;
16
+ endCursor: Cursor;
17
+ filter?: TFilter;
18
+ indexerName?: string;
19
+ }) {
20
+ const { db, endCursor, filter, indexerName = DEFAULT_INDEXER_ID } = props;
21
+
19
22
  assertInTransaction(db);
20
23
 
21
24
  db.prepare(statements.putCheckpoint).run(
22
- DEFAULT_INDEXER_ID,
25
+ indexerName,
23
26
  Number(endCursor.orderKey),
24
27
  endCursor.uniqueKey,
25
28
  );
@@ -27,26 +30,30 @@ export function persistState<TFilter>(
27
30
  if (filter) {
28
31
  db.prepare(statements.updateFilterToBlock).run(
29
32
  Number(endCursor.orderKey),
30
- DEFAULT_INDEXER_ID,
33
+ indexerName,
31
34
  );
32
35
  db.prepare(statements.insertFilter).run(
33
- DEFAULT_INDEXER_ID,
36
+ indexerName,
34
37
  serialize(filter as Record<string, unknown>),
35
38
  Number(endCursor.orderKey),
36
39
  );
37
40
  }
38
41
  }
39
42
 
40
- export function getState<TFilter>(db: Database) {
43
+ export function getState<TFilter>(props: {
44
+ db: Database;
45
+ indexerName?: string;
46
+ }) {
47
+ const { db, indexerName = DEFAULT_INDEXER_ID } = props;
41
48
  assertInTransaction(db);
42
49
  const storedCursor = db
43
50
  .prepare<string, { order_key?: number; unique_key?: string }>(
44
51
  statements.getCheckpoint,
45
52
  )
46
- .get(DEFAULT_INDEXER_ID);
53
+ .get(indexerName);
47
54
  const storedFilter = db
48
55
  .prepare<string, { filter: string }>(statements.getFilter)
49
- .get(DEFAULT_INDEXER_ID);
56
+ .get(indexerName);
50
57
 
51
58
  let cursor: Cursor | undefined;
52
59
  let filter: TFilter | undefined;
@@ -65,6 +72,36 @@ export function getState<TFilter>(db: Database) {
65
72
  return { cursor, filter };
66
73
  }
67
74
 
75
+ export function finalizeState(props: {
76
+ db: Database;
77
+ cursor: Cursor;
78
+ indexerName?: string;
79
+ }) {
80
+ const { cursor, db, indexerName = DEFAULT_INDEXER_ID } = props;
81
+ assertInTransaction(db);
82
+ db.prepare<[string, number]>(statements.finalizeFilter).run(
83
+ indexerName,
84
+ Number(cursor.orderKey),
85
+ );
86
+ }
87
+
88
+ export function invalidateState(props: {
89
+ db: Database;
90
+ cursor: Cursor;
91
+ indexerName?: string;
92
+ }) {
93
+ const { cursor, db, indexerName = DEFAULT_INDEXER_ID } = props;
94
+ assertInTransaction(db);
95
+ db.prepare<[string, number]>(statements.invalidateFilterDelete).run(
96
+ indexerName,
97
+ Number(cursor.orderKey),
98
+ );
99
+ db.prepare<[string, number]>(statements.invalidateFilterUpdate).run(
100
+ indexerName,
101
+ Number(cursor.orderKey),
102
+ );
103
+ }
104
+
68
105
  const statements = {
69
106
  createCheckpointsTable: `
70
107
  CREATE TABLE IF NOT EXISTS checkpoints (
@@ -110,4 +147,14 @@ const statements = {
110
147
  delFilter: `
111
148
  DELETE FROM filters
112
149
  WHERE id = ?`,
150
+ finalizeFilter: `
151
+ DELETE FROM filters
152
+ WHERE id = ? AND to_block < ?`,
153
+ invalidateFilterDelete: `
154
+ DELETE FROM filters
155
+ WHERE id = ? AND from_block > ?`,
156
+ invalidateFilterUpdate: `
157
+ UPDATE filters
158
+ SET to_block = NULL
159
+ WHERE id = ? AND to_block > ?`,
113
160
  };