@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 +164 -12
- package/dist/index.d.cts +13 -1
- package/dist/index.d.mts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.mjs +164 -12
- package/package.json +3 -3
- package/src/index.ts +95 -15
- package/src/mongo.ts +1 -1
- package/src/persistence.ts +163 -0
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": { $
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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": { $
|
|
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
|
-
|
|
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.
|
|
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.
|
|
39
|
-
"@apibara/protocol": "2.0.0-beta.
|
|
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("
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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("
|
|
55
|
-
|
|
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
|
-
|
|
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:
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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": { $
|
|
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
|
+
}
|