@apibara/plugin-mongo 2.0.0-beta.28 → 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.
package/dist/index.cjs CHANGED
@@ -30,13 +30,108 @@ async function finalize(db, session, cursor, collections) {
30
30
  for (const collection of collections) {
31
31
  await db.collection(collection).deleteMany(
32
32
  {
33
- "_cursor.to": { $lt: orderKeyValue }
33
+ "_cursor.to": { $lte: orderKeyValue }
34
34
  },
35
35
  { session }
36
36
  );
37
37
  }
38
38
  }
39
39
 
40
+ const checkpointCollectionName = "checkpoints";
41
+ const filterCollectionName = "filters";
42
+ async function initializePersistentState(db, session) {
43
+ const checkpoint = await db.createCollection(
44
+ checkpointCollectionName,
45
+ { session }
46
+ );
47
+ const filter = await db.createCollection(filterCollectionName, {
48
+ session
49
+ });
50
+ await checkpoint.createIndex({ id: 1 }, { session });
51
+ await filter.createIndex({ id: 1, fromBlock: 1 }, { session });
52
+ }
53
+ async function persistState(props) {
54
+ const { db, session, endCursor, filter, indexerName } = props;
55
+ if (endCursor) {
56
+ await db.collection(checkpointCollectionName).updateOne(
57
+ { id: indexerName },
58
+ {
59
+ $set: {
60
+ orderKey: Number(endCursor.orderKey),
61
+ uniqueKey: endCursor.uniqueKey
62
+ }
63
+ },
64
+ { upsert: true, session }
65
+ );
66
+ if (filter) {
67
+ await db.collection(filterCollectionName).updateMany(
68
+ { id: indexerName, toBlock: null },
69
+ { $set: { toBlock: Number(endCursor.orderKey) } },
70
+ { session }
71
+ );
72
+ await db.collection(filterCollectionName).updateOne(
73
+ {
74
+ id: indexerName,
75
+ fromBlock: Number(endCursor.orderKey)
76
+ },
77
+ {
78
+ $set: {
79
+ filter,
80
+ fromBlock: Number(endCursor.orderKey),
81
+ toBlock: null
82
+ }
83
+ },
84
+ { upsert: true, session }
85
+ );
86
+ }
87
+ }
88
+ }
89
+ async function getState(props) {
90
+ const { db, session, indexerName } = props;
91
+ let cursor;
92
+ let filter;
93
+ const checkpointRow = await db.collection(checkpointCollectionName).findOne({ id: indexerName }, { session });
94
+ if (checkpointRow) {
95
+ cursor = {
96
+ orderKey: BigInt(checkpointRow.orderKey),
97
+ uniqueKey: checkpointRow.uniqueKey
98
+ };
99
+ }
100
+ const filterRow = await db.collection(filterCollectionName).findOne(
101
+ {
102
+ id: indexerName,
103
+ toBlock: null
104
+ },
105
+ { session }
106
+ );
107
+ if (filterRow) {
108
+ filter = filterRow.filter;
109
+ }
110
+ return { cursor, filter };
111
+ }
112
+ async function invalidateState(props) {
113
+ const { db, session, cursor, indexerName } = props;
114
+ await db.collection(filterCollectionName).deleteMany(
115
+ { id: indexerName, fromBlock: { $gt: Number(cursor.orderKey) } },
116
+ { session }
117
+ );
118
+ await db.collection(filterCollectionName).updateMany(
119
+ { id: indexerName, toBlock: { $gt: Number(cursor.orderKey) } },
120
+ { $set: { toBlock: null } },
121
+ { session }
122
+ );
123
+ }
124
+ async function finalizeState(props) {
125
+ const { db, session, cursor, indexerName } = props;
126
+ await db.collection(filterCollectionName).deleteMany(
127
+ {
128
+ id: indexerName,
129
+ toBlock: { $lte: Number(cursor.orderKey) }
130
+ },
131
+ { session }
132
+ );
133
+ }
134
+
40
135
  class MongoStorage {
41
136
  constructor(db, session, endCursor) {
42
137
  this.db = db;
@@ -236,9 +331,67 @@ function mongoStorage({
236
331
  dbName,
237
332
  dbOptions,
238
333
  collections,
239
- persistState: enablePersistence = true
334
+ persistState: enablePersistence = true,
335
+ indexerName = "default"
240
336
  }) {
241
337
  return plugins.defineIndexerPlugin((indexer) => {
338
+ indexer.hooks.hook("run:before", async () => {
339
+ await withTransaction(client, async (session) => {
340
+ const db = client.db(dbName, dbOptions);
341
+ if (enablePersistence) {
342
+ await initializePersistentState(db, session);
343
+ }
344
+ });
345
+ });
346
+ indexer.hooks.hook("connect:before", async ({ request }) => {
347
+ if (!enablePersistence) {
348
+ return;
349
+ }
350
+ await withTransaction(client, async (session) => {
351
+ const db = client.db(dbName, dbOptions);
352
+ const { cursor, filter } = await getState({
353
+ db,
354
+ session,
355
+ indexerName
356
+ });
357
+ if (cursor) {
358
+ request.startingCursor = cursor;
359
+ }
360
+ if (filter) {
361
+ request.filter[1] = filter;
362
+ }
363
+ });
364
+ });
365
+ indexer.hooks.hook("connect:after", async ({ request }) => {
366
+ const cursor = request.startingCursor;
367
+ if (!cursor) {
368
+ return;
369
+ }
370
+ await withTransaction(client, async (session) => {
371
+ const db = client.db(dbName, dbOptions);
372
+ await invalidate(db, session, cursor, collections);
373
+ if (enablePersistence) {
374
+ await invalidateState({ db, session, cursor, indexerName });
375
+ }
376
+ });
377
+ });
378
+ indexer.hooks.hook("connect:factory", async ({ request, endCursor }) => {
379
+ if (!enablePersistence) {
380
+ return;
381
+ }
382
+ await withTransaction(client, async (session) => {
383
+ const db = client.db(dbName, dbOptions);
384
+ if (endCursor && request.filter[1]) {
385
+ await persistState({
386
+ db,
387
+ endCursor,
388
+ session,
389
+ filter: request.filter[1],
390
+ indexerName
391
+ });
392
+ }
393
+ });
394
+ });
242
395
  indexer.hooks.hook("message:finalize", async ({ message }) => {
243
396
  const { cursor } = message.finalize;
244
397
  if (!cursor) {
@@ -247,6 +400,9 @@ function mongoStorage({
247
400
  await withTransaction(client, async (session) => {
248
401
  const db = client.db(dbName, dbOptions);
249
402
  await finalize(db, session, cursor, collections);
403
+ if (enablePersistence) {
404
+ await finalizeState({ db, session, cursor, indexerName });
405
+ }
250
406
  });
251
407
  });
252
408
  indexer.hooks.hook("message:invalidate", async ({ message }) => {
@@ -257,16 +413,9 @@ function mongoStorage({
257
413
  await withTransaction(client, async (session) => {
258
414
  const db = client.db(dbName, dbOptions);
259
415
  await invalidate(db, session, cursor, collections);
260
- });
261
- });
262
- indexer.hooks.hook("connect:after", async ({ request }) => {
263
- const cursor = request.startingCursor;
264
- if (!cursor) {
265
- return;
266
- }
267
- await withTransaction(client, async (session) => {
268
- const db = client.db(dbName, dbOptions);
269
- await invalidate(db, session, cursor, collections);
416
+ if (enablePersistence) {
417
+ await invalidateState({ db, session, cursor, indexerName });
418
+ }
270
419
  });
271
420
  });
272
421
  indexer.hooks.hook("handler:middleware", async ({ use }) => {
@@ -280,6 +429,9 @@ function mongoStorage({
280
429
  context[MONGO_PROPERTY] = new MongoStorage(db, session, endCursor);
281
430
  await next();
282
431
  delete context[MONGO_PROPERTY];
432
+ if (enablePersistence) {
433
+ await persistState({ db, endCursor, session, indexerName });
434
+ }
283
435
  });
284
436
  });
285
437
  });
package/dist/index.d.cts CHANGED
@@ -31,7 +31,19 @@ interface MongoStorageOptions {
31
31
  dbOptions?: DbOptions;
32
32
  collections: string[];
33
33
  persistState?: boolean;
34
+ indexerName?: string;
34
35
  }
35
- declare function mongoStorage<TFilter, TBlock>({ client, dbName, dbOptions, collections, persistState: enablePersistence, }: MongoStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
36
+ /**
37
+ * Creates a plugin that uses MongoDB as the storage layer.
38
+ *
39
+ * Supports storing the indexer's state and provides a simple Key-Value store.
40
+ * @param options.client - The MongoDB client instance.
41
+ * @param options.dbName - The name of the database.
42
+ * @param options.dbOptions - The database options.
43
+ * @param options.collections - The collections to use.
44
+ * @param options.persistState - Whether to persist the indexer's state. Defaults to true.
45
+ * @param options.indexerName - The name of the indexer. Defaults value is 'default'.
46
+ */
47
+ declare function mongoStorage<TFilter, TBlock>({ client, dbName, dbOptions, collections, persistState: enablePersistence, indexerName, }: MongoStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
36
48
 
37
49
  export { MongoCollection, MongoStorage, type MongoStorageOptions, mongoStorage, useMongoStorage };
package/dist/index.d.mts CHANGED
@@ -31,7 +31,19 @@ interface MongoStorageOptions {
31
31
  dbOptions?: DbOptions;
32
32
  collections: string[];
33
33
  persistState?: boolean;
34
+ indexerName?: string;
34
35
  }
35
- declare function mongoStorage<TFilter, TBlock>({ client, dbName, dbOptions, collections, persistState: enablePersistence, }: MongoStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
36
+ /**
37
+ * Creates a plugin that uses MongoDB as the storage layer.
38
+ *
39
+ * Supports storing the indexer's state and provides a simple Key-Value store.
40
+ * @param options.client - The MongoDB client instance.
41
+ * @param options.dbName - The name of the database.
42
+ * @param options.dbOptions - The database options.
43
+ * @param options.collections - The collections to use.
44
+ * @param options.persistState - Whether to persist the indexer's state. Defaults to true.
45
+ * @param options.indexerName - The name of the indexer. Defaults value is 'default'.
46
+ */
47
+ declare function mongoStorage<TFilter, TBlock>({ client, dbName, dbOptions, collections, persistState: enablePersistence, indexerName, }: MongoStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
36
48
 
37
49
  export { MongoCollection, MongoStorage, type MongoStorageOptions, mongoStorage, useMongoStorage };
package/dist/index.d.ts CHANGED
@@ -31,7 +31,19 @@ interface MongoStorageOptions {
31
31
  dbOptions?: DbOptions;
32
32
  collections: string[];
33
33
  persistState?: boolean;
34
+ indexerName?: string;
34
35
  }
35
- declare function mongoStorage<TFilter, TBlock>({ client, dbName, dbOptions, collections, persistState: enablePersistence, }: MongoStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
36
+ /**
37
+ * Creates a plugin that uses MongoDB as the storage layer.
38
+ *
39
+ * Supports storing the indexer's state and provides a simple Key-Value store.
40
+ * @param options.client - The MongoDB client instance.
41
+ * @param options.dbName - The name of the database.
42
+ * @param options.dbOptions - The database options.
43
+ * @param options.collections - The collections to use.
44
+ * @param options.persistState - Whether to persist the indexer's state. Defaults to true.
45
+ * @param options.indexerName - The name of the indexer. Defaults value is 'default'.
46
+ */
47
+ declare function mongoStorage<TFilter, TBlock>({ client, dbName, dbOptions, collections, persistState: enablePersistence, indexerName, }: MongoStorageOptions): _apibara_indexer_plugins.IndexerPlugin<TFilter, TBlock>;
36
48
 
37
49
  export { MongoCollection, MongoStorage, type MongoStorageOptions, mongoStorage, useMongoStorage };
package/dist/index.mjs CHANGED
@@ -28,13 +28,108 @@ async function finalize(db, session, cursor, collections) {
28
28
  for (const collection of collections) {
29
29
  await db.collection(collection).deleteMany(
30
30
  {
31
- "_cursor.to": { $lt: orderKeyValue }
31
+ "_cursor.to": { $lte: orderKeyValue }
32
32
  },
33
33
  { session }
34
34
  );
35
35
  }
36
36
  }
37
37
 
38
+ const checkpointCollectionName = "checkpoints";
39
+ const filterCollectionName = "filters";
40
+ async function initializePersistentState(db, session) {
41
+ const checkpoint = await db.createCollection(
42
+ checkpointCollectionName,
43
+ { session }
44
+ );
45
+ const filter = await db.createCollection(filterCollectionName, {
46
+ session
47
+ });
48
+ await checkpoint.createIndex({ id: 1 }, { session });
49
+ await filter.createIndex({ id: 1, fromBlock: 1 }, { session });
50
+ }
51
+ async function persistState(props) {
52
+ const { db, session, endCursor, filter, indexerName } = props;
53
+ if (endCursor) {
54
+ await db.collection(checkpointCollectionName).updateOne(
55
+ { id: indexerName },
56
+ {
57
+ $set: {
58
+ orderKey: Number(endCursor.orderKey),
59
+ uniqueKey: endCursor.uniqueKey
60
+ }
61
+ },
62
+ { upsert: true, session }
63
+ );
64
+ if (filter) {
65
+ await db.collection(filterCollectionName).updateMany(
66
+ { id: indexerName, toBlock: null },
67
+ { $set: { toBlock: Number(endCursor.orderKey) } },
68
+ { session }
69
+ );
70
+ await db.collection(filterCollectionName).updateOne(
71
+ {
72
+ id: indexerName,
73
+ fromBlock: Number(endCursor.orderKey)
74
+ },
75
+ {
76
+ $set: {
77
+ filter,
78
+ fromBlock: Number(endCursor.orderKey),
79
+ toBlock: null
80
+ }
81
+ },
82
+ { upsert: true, session }
83
+ );
84
+ }
85
+ }
86
+ }
87
+ async function getState(props) {
88
+ const { db, session, indexerName } = props;
89
+ let cursor;
90
+ let filter;
91
+ const checkpointRow = await db.collection(checkpointCollectionName).findOne({ id: indexerName }, { session });
92
+ if (checkpointRow) {
93
+ cursor = {
94
+ orderKey: BigInt(checkpointRow.orderKey),
95
+ uniqueKey: checkpointRow.uniqueKey
96
+ };
97
+ }
98
+ const filterRow = await db.collection(filterCollectionName).findOne(
99
+ {
100
+ id: indexerName,
101
+ toBlock: null
102
+ },
103
+ { session }
104
+ );
105
+ if (filterRow) {
106
+ filter = filterRow.filter;
107
+ }
108
+ return { cursor, filter };
109
+ }
110
+ async function invalidateState(props) {
111
+ const { db, session, cursor, indexerName } = props;
112
+ await db.collection(filterCollectionName).deleteMany(
113
+ { id: indexerName, fromBlock: { $gt: Number(cursor.orderKey) } },
114
+ { session }
115
+ );
116
+ await db.collection(filterCollectionName).updateMany(
117
+ { id: indexerName, toBlock: { $gt: Number(cursor.orderKey) } },
118
+ { $set: { toBlock: null } },
119
+ { session }
120
+ );
121
+ }
122
+ async function finalizeState(props) {
123
+ const { db, session, cursor, indexerName } = props;
124
+ await db.collection(filterCollectionName).deleteMany(
125
+ {
126
+ id: indexerName,
127
+ toBlock: { $lte: Number(cursor.orderKey) }
128
+ },
129
+ { session }
130
+ );
131
+ }
132
+
38
133
  class MongoStorage {
39
134
  constructor(db, session, endCursor) {
40
135
  this.db = db;
@@ -234,9 +329,67 @@ function mongoStorage({
234
329
  dbName,
235
330
  dbOptions,
236
331
  collections,
237
- persistState: enablePersistence = true
332
+ persistState: enablePersistence = true,
333
+ indexerName = "default"
238
334
  }) {
239
335
  return defineIndexerPlugin((indexer) => {
336
+ indexer.hooks.hook("run:before", async () => {
337
+ await withTransaction(client, async (session) => {
338
+ const db = client.db(dbName, dbOptions);
339
+ if (enablePersistence) {
340
+ await initializePersistentState(db, session);
341
+ }
342
+ });
343
+ });
344
+ indexer.hooks.hook("connect:before", async ({ request }) => {
345
+ if (!enablePersistence) {
346
+ return;
347
+ }
348
+ await withTransaction(client, async (session) => {
349
+ const db = client.db(dbName, dbOptions);
350
+ const { cursor, filter } = await getState({
351
+ db,
352
+ session,
353
+ indexerName
354
+ });
355
+ if (cursor) {
356
+ request.startingCursor = cursor;
357
+ }
358
+ if (filter) {
359
+ request.filter[1] = filter;
360
+ }
361
+ });
362
+ });
363
+ indexer.hooks.hook("connect:after", async ({ request }) => {
364
+ const cursor = request.startingCursor;
365
+ if (!cursor) {
366
+ return;
367
+ }
368
+ await withTransaction(client, async (session) => {
369
+ const db = client.db(dbName, dbOptions);
370
+ await invalidate(db, session, cursor, collections);
371
+ if (enablePersistence) {
372
+ await invalidateState({ db, session, cursor, indexerName });
373
+ }
374
+ });
375
+ });
376
+ indexer.hooks.hook("connect:factory", async ({ request, endCursor }) => {
377
+ if (!enablePersistence) {
378
+ return;
379
+ }
380
+ await withTransaction(client, async (session) => {
381
+ const db = client.db(dbName, dbOptions);
382
+ if (endCursor && request.filter[1]) {
383
+ await persistState({
384
+ db,
385
+ endCursor,
386
+ session,
387
+ filter: request.filter[1],
388
+ indexerName
389
+ });
390
+ }
391
+ });
392
+ });
240
393
  indexer.hooks.hook("message:finalize", async ({ message }) => {
241
394
  const { cursor } = message.finalize;
242
395
  if (!cursor) {
@@ -245,6 +398,9 @@ function mongoStorage({
245
398
  await withTransaction(client, async (session) => {
246
399
  const db = client.db(dbName, dbOptions);
247
400
  await finalize(db, session, cursor, collections);
401
+ if (enablePersistence) {
402
+ await finalizeState({ db, session, cursor, indexerName });
403
+ }
248
404
  });
249
405
  });
250
406
  indexer.hooks.hook("message:invalidate", async ({ message }) => {
@@ -255,16 +411,9 @@ function mongoStorage({
255
411
  await withTransaction(client, async (session) => {
256
412
  const db = client.db(dbName, dbOptions);
257
413
  await invalidate(db, session, cursor, collections);
258
- });
259
- });
260
- indexer.hooks.hook("connect:after", async ({ request }) => {
261
- const cursor = request.startingCursor;
262
- if (!cursor) {
263
- return;
264
- }
265
- await withTransaction(client, async (session) => {
266
- const db = client.db(dbName, dbOptions);
267
- await invalidate(db, session, cursor, collections);
414
+ if (enablePersistence) {
415
+ await invalidateState({ db, session, cursor, indexerName });
416
+ }
268
417
  });
269
418
  });
270
419
  indexer.hooks.hook("handler:middleware", async ({ use }) => {
@@ -278,6 +427,9 @@ function mongoStorage({
278
427
  context[MONGO_PROPERTY] = new MongoStorage(db, session, endCursor);
279
428
  await next();
280
429
  delete context[MONGO_PROPERTY];
430
+ if (enablePersistence) {
431
+ await persistState({ db, endCursor, session, indexerName });
432
+ }
281
433
  });
282
434
  });
283
435
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apibara/plugin-mongo",
3
- "version": "2.0.0-beta.28",
3
+ "version": "2.0.0-beta.30",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -35,7 +35,7 @@
35
35
  "mongodb": "^6.12.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,6 +3,13 @@ import { defineIndexerPlugin } from "@apibara/indexer/plugins";
3
3
  import type { DbOptions, MongoClient } from "mongodb";
4
4
 
5
5
  import { finalize, invalidate } from "./mongo";
6
+ import {
7
+ finalizeState,
8
+ getState,
9
+ initializePersistentState,
10
+ invalidateState,
11
+ persistState,
12
+ } from "./persistence";
6
13
  import { MongoStorage } from "./storage";
7
14
  import { MongoStorageError, withTransaction } from "./utils";
8
15
 
@@ -28,53 +35,127 @@ export interface MongoStorageOptions {
28
35
  dbOptions?: DbOptions;
29
36
  collections: string[];
30
37
  persistState?: boolean;
38
+ indexerName?: string;
31
39
  }
32
-
40
+ /**
41
+ * Creates a plugin that uses MongoDB as the storage layer.
42
+ *
43
+ * Supports storing the indexer's state and provides a simple Key-Value store.
44
+ * @param options.client - The MongoDB client instance.
45
+ * @param options.dbName - The name of the database.
46
+ * @param options.dbOptions - The database options.
47
+ * @param options.collections - The collections to use.
48
+ * @param options.persistState - Whether to persist the indexer's state. Defaults to true.
49
+ * @param options.indexerName - The name of the indexer. Defaults value is 'default'.
50
+ */
33
51
  export function mongoStorage<TFilter, TBlock>({
34
52
  client,
35
53
  dbName,
36
54
  dbOptions,
37
55
  collections,
38
56
  persistState: enablePersistence = true,
57
+ indexerName = "default",
39
58
  }: MongoStorageOptions) {
40
59
  return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
41
- indexer.hooks.hook("message:finalize", async ({ message }) => {
42
- const { cursor } = message.finalize;
60
+ indexer.hooks.hook("run:before", async () => {
61
+ await withTransaction(client, async (session) => {
62
+ const db = client.db(dbName, dbOptions);
63
+ if (enablePersistence) {
64
+ await initializePersistentState(db, session);
65
+ }
66
+ });
67
+ });
43
68
 
44
- if (!cursor) {
45
- throw new MongoStorageError("finalized cursor is undefined");
69
+ indexer.hooks.hook("connect:before", async ({ request }) => {
70
+ if (!enablePersistence) {
71
+ return;
46
72
  }
47
73
 
48
74
  await withTransaction(client, async (session) => {
49
75
  const db = client.db(dbName, dbOptions);
50
- await finalize(db, session, cursor, collections);
76
+ const { cursor, filter } = await getState<TFilter>({
77
+ db,
78
+ session,
79
+ indexerName,
80
+ });
81
+
82
+ if (cursor) {
83
+ request.startingCursor = cursor;
84
+ }
85
+
86
+ if (filter) {
87
+ request.filter[1] = filter;
88
+ }
51
89
  });
52
90
  });
53
91
 
54
- indexer.hooks.hook("message:invalidate", async ({ message }) => {
55
- const { cursor } = message.invalidate;
92
+ indexer.hooks.hook("connect:after", async ({ request }) => {
93
+ // On restart, we need to invalidate data for blocks that were processed but not persisted.
94
+ const cursor = request.startingCursor;
56
95
 
57
96
  if (!cursor) {
58
- throw new MongoStorageError("invalidate cursor is undefined");
97
+ return;
59
98
  }
60
99
 
61
100
  await withTransaction(client, async (session) => {
62
101
  const db = client.db(dbName, dbOptions);
63
102
  await invalidate(db, session, cursor, collections);
103
+
104
+ if (enablePersistence) {
105
+ await invalidateState({ db, session, cursor, indexerName });
106
+ }
64
107
  });
65
108
  });
66
109
 
67
- indexer.hooks.hook("connect:after", async ({ request }) => {
68
- // On restart, we need to invalidate data for blocks that were processed but not persisted.
69
- const cursor = request.startingCursor;
110
+ indexer.hooks.hook("connect:factory", async ({ request, endCursor }) => {
111
+ if (!enablePersistence) {
112
+ return;
113
+ }
114
+ await withTransaction(client, async (session) => {
115
+ const db = client.db(dbName, dbOptions);
116
+ if (endCursor && request.filter[1]) {
117
+ await persistState({
118
+ db,
119
+ endCursor,
120
+ session,
121
+ filter: request.filter[1],
122
+ indexerName,
123
+ });
124
+ }
125
+ });
126
+ });
127
+
128
+ indexer.hooks.hook("message:finalize", async ({ message }) => {
129
+ const { cursor } = message.finalize;
70
130
 
71
131
  if (!cursor) {
72
- return;
132
+ throw new MongoStorageError("finalized cursor is undefined");
133
+ }
134
+
135
+ await withTransaction(client, async (session) => {
136
+ const db = client.db(dbName, dbOptions);
137
+ await finalize(db, session, cursor, collections);
138
+
139
+ if (enablePersistence) {
140
+ await finalizeState({ db, session, cursor, indexerName });
141
+ }
142
+ });
143
+ });
144
+
145
+ indexer.hooks.hook("message:invalidate", async ({ message }) => {
146
+ const { cursor } = message.invalidate;
147
+
148
+ if (!cursor) {
149
+ throw new MongoStorageError("invalidate cursor is undefined");
73
150
  }
74
151
 
75
152
  await withTransaction(client, async (session) => {
76
153
  const db = client.db(dbName, dbOptions);
77
154
  await invalidate(db, session, cursor, collections);
155
+
156
+ if (enablePersistence) {
157
+ await invalidateState({ db, session, cursor, indexerName });
158
+ }
78
159
  });
79
160
  });
80
161
 
@@ -91,11 +172,10 @@ export function mongoStorage<TFilter, TBlock>({
91
172
  context[MONGO_PROPERTY] = new MongoStorage(db, session, endCursor);
92
173
 
93
174
  await next();
94
-
95
175
  delete context[MONGO_PROPERTY];
96
176
 
97
177
  if (enablePersistence) {
98
- // TODO: persist state
178
+ await persistState({ db, endCursor, session, indexerName });
99
179
  }
100
180
  });
101
181
  });
package/src/mongo.ts CHANGED
@@ -43,7 +43,7 @@ export async function finalize(
43
43
  // Delete documents where the upper bound of _cursor is less than the finalize cursor
44
44
  await db.collection(collection).deleteMany(
45
45
  {
46
- "_cursor.to": { $lt: orderKeyValue },
46
+ "_cursor.to": { $lte: orderKeyValue },
47
47
  },
48
48
  { session },
49
49
  );
@@ -0,0 +1,163 @@
1
+ import type { Cursor } from "@apibara/protocol";
2
+ import type { ClientSession, Db } from "mongodb";
3
+
4
+ export type CheckpointSchema = {
5
+ id: string;
6
+ orderKey: number;
7
+ uniqueKey?: `0x${string}`;
8
+ };
9
+
10
+ export type FilterSchema = {
11
+ id: string;
12
+ filter: Record<string, unknown>;
13
+ fromBlock: number;
14
+ toBlock: number | null;
15
+ };
16
+
17
+ export const checkpointCollectionName = "checkpoints";
18
+ export const filterCollectionName = "filters";
19
+
20
+ export async function initializePersistentState(
21
+ db: Db,
22
+ session: ClientSession,
23
+ ) {
24
+ const checkpoint = await db.createCollection<CheckpointSchema>(
25
+ checkpointCollectionName,
26
+ { session },
27
+ );
28
+ const filter = await db.createCollection<FilterSchema>(filterCollectionName, {
29
+ session,
30
+ });
31
+
32
+ await checkpoint.createIndex({ id: 1 }, { session });
33
+ await filter.createIndex({ id: 1, fromBlock: 1 }, { session });
34
+ }
35
+
36
+ export async function persistState<TFilter>(props: {
37
+ db: Db;
38
+ session: ClientSession;
39
+ endCursor: Cursor;
40
+ filter?: TFilter;
41
+ indexerName: string;
42
+ }) {
43
+ const { db, session, endCursor, filter, indexerName } = props;
44
+
45
+ if (endCursor) {
46
+ await db.collection<CheckpointSchema>(checkpointCollectionName).updateOne(
47
+ { id: indexerName },
48
+ {
49
+ $set: {
50
+ orderKey: Number(endCursor.orderKey),
51
+ uniqueKey: endCursor.uniqueKey,
52
+ },
53
+ },
54
+ { upsert: true, session },
55
+ );
56
+
57
+ if (filter) {
58
+ // Update existing filter's to_block
59
+ await db
60
+ .collection<FilterSchema>(filterCollectionName)
61
+ .updateMany(
62
+ { id: indexerName, toBlock: null },
63
+ { $set: { toBlock: Number(endCursor.orderKey) } },
64
+ { session },
65
+ );
66
+
67
+ // Insert new filter
68
+ await db.collection<FilterSchema>(filterCollectionName).updateOne(
69
+ {
70
+ id: indexerName,
71
+ fromBlock: Number(endCursor.orderKey),
72
+ },
73
+ {
74
+ $set: {
75
+ filter: filter as Record<string, unknown>,
76
+ fromBlock: Number(endCursor.orderKey),
77
+ toBlock: null,
78
+ },
79
+ },
80
+ { upsert: true, session },
81
+ );
82
+ }
83
+ }
84
+ }
85
+
86
+ export async function getState<TFilter>(props: {
87
+ db: Db;
88
+ session: ClientSession;
89
+ indexerName: string;
90
+ }): Promise<{ cursor?: Cursor; filter?: TFilter }> {
91
+ const { db, session, indexerName } = props;
92
+
93
+ let cursor: Cursor | undefined;
94
+ let filter: TFilter | undefined;
95
+
96
+ const checkpointRow = await db
97
+ .collection<CheckpointSchema>(checkpointCollectionName)
98
+ .findOne({ id: indexerName }, { session });
99
+
100
+ if (checkpointRow) {
101
+ cursor = {
102
+ orderKey: BigInt(checkpointRow.orderKey),
103
+ uniqueKey: checkpointRow.uniqueKey,
104
+ };
105
+ }
106
+
107
+ const filterRow = await db
108
+ .collection<FilterSchema>(filterCollectionName)
109
+ .findOne(
110
+ {
111
+ id: indexerName,
112
+ toBlock: null,
113
+ },
114
+ { session },
115
+ );
116
+
117
+ if (filterRow) {
118
+ filter = filterRow.filter as TFilter;
119
+ }
120
+
121
+ return { cursor, filter };
122
+ }
123
+
124
+ export async function invalidateState(props: {
125
+ db: Db;
126
+ session: ClientSession;
127
+ cursor: Cursor;
128
+ indexerName: string;
129
+ }) {
130
+ const { db, session, cursor, indexerName } = props;
131
+
132
+ await db
133
+ .collection<FilterSchema>(filterCollectionName)
134
+ .deleteMany(
135
+ { id: indexerName, fromBlock: { $gt: Number(cursor.orderKey) } },
136
+ { session },
137
+ );
138
+
139
+ await db
140
+ .collection<FilterSchema>(filterCollectionName)
141
+ .updateMany(
142
+ { id: indexerName, toBlock: { $gt: Number(cursor.orderKey) } },
143
+ { $set: { toBlock: null } },
144
+ { session },
145
+ );
146
+ }
147
+
148
+ export async function finalizeState(props: {
149
+ db: Db;
150
+ session: ClientSession;
151
+ cursor: Cursor;
152
+ indexerName: string;
153
+ }) {
154
+ const { db, session, cursor, indexerName } = props;
155
+
156
+ await db.collection<FilterSchema>(filterCollectionName).deleteMany(
157
+ {
158
+ id: indexerName,
159
+ toBlock: { $lte: Number(cursor.orderKey) },
160
+ },
161
+ { session },
162
+ );
163
+ }