univo 0.2.17 → 0.2.19
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/cli.mjs +2 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +237 -182
- package/dist/realtime.d.mts +0 -1
- package/dist/realtime.d.ts +0 -1
- package/dist/realtime.mjs +9 -7
- package/dist/rpc.d.mts +249 -7
- package/dist/rpc.d.ts +249 -7
- package/dist/shared/{univo.DgQqsZEe.mjs → univo.D1x0MkrS.mjs} +61 -36
- package/dist/shared/{univo.BMmU53c6.mjs → univo.bwpFiVa7.mjs} +9 -4
- package/dist/transport.d.mts +4 -1
- package/dist/transport.d.ts +4 -1
- package/dist/transport.mjs +2 -2
- package/package.json +1 -1
- package/dist/shared/univo.DdHfpTWq.d.mts +0 -220
- package/dist/shared/univo.DdHfpTWq.d.ts +0 -220
package/dist/cli.mjs
CHANGED
|
@@ -4,8 +4,8 @@ import open from 'open';
|
|
|
4
4
|
import { config } from 'dotenv';
|
|
5
5
|
import { WebSocket } from 'partysocket';
|
|
6
6
|
import { defineCommand, runMain } from 'citty';
|
|
7
|
-
import { h as http } from './shared/univo.
|
|
8
|
-
import { c as createLogger, v as version, b as name } from './shared/univo.
|
|
7
|
+
import { h as http } from './shared/univo.D1x0MkrS.mjs';
|
|
8
|
+
import { c as createLogger, v as version, b as name } from './shared/univo.bwpFiVa7.mjs';
|
|
9
9
|
|
|
10
10
|
const log = createLogger({ quiet: false });
|
|
11
11
|
const dev = defineCommand({
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { B as Block, E as Event, a as Filter, H as Head, I as Indexer, M as Metadata, R as Result, i as indexer } from './shared/univo.DdHfpTWq.mjs';
|
|
2
1
|
import '@storagesdk/core';
|
|
2
|
+
export { B as Block, E as Event, F as Filter, H as Head, I as Indexer, M as Metadata, R as Result, i as indexer } from './rpc.mjs';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { B as Block, E as Event, a as Filter, H as Head, I as Indexer, M as Metadata, R as Result, i as indexer } from './shared/univo.DdHfpTWq.js';
|
|
2
1
|
import '@storagesdk/core';
|
|
2
|
+
export { B as Block, E as Event, F as Filter, H as Head, I as Indexer, M as Metadata, R as Result, i as indexer } from './rpc.js';
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { StorageError } from '@storagesdk/core';
|
|
2
|
-
import { g as getException, c as createException, a as catchException, l as local } from './shared/univo.
|
|
3
|
-
import { c as createLogger, d as decompress, e as decoder, r as retry, v as version, h as hexToNumber, a as isHexEqual, f as normalizeHex, g as compress } from './shared/univo.
|
|
2
|
+
import { g as getException, c as createException, a as catchException, l as local } from './shared/univo.D1x0MkrS.mjs';
|
|
3
|
+
import { c as createLogger, d as decompress, e as decoder, r as retry, v as version, h as hexToNumber, a as isHexEqual, n as numberToHex, f as normalizeHex, g as compress } from './shared/univo.bwpFiVa7.mjs';
|
|
4
4
|
import 'ws';
|
|
5
5
|
import 'partysocket';
|
|
6
6
|
|
|
@@ -136,36 +136,31 @@ function indexer(opts) {
|
|
|
136
136
|
const log = createLogger({ quiet: opts.quiet ?? false, prefix: "[indexer]" });
|
|
137
137
|
const all_events = [];
|
|
138
138
|
const events_grouped_by_storage_map = /* @__PURE__ */ new Map();
|
|
139
|
+
const all_actions = [];
|
|
139
140
|
const metadata = {
|
|
141
|
+
// The blocks store essentially operates as our write-ahead-log (WAL) for unfinalized blocks. Before we
|
|
142
|
+
// commit any events to storage we first record the block input in our metadata layer. This allows our
|
|
143
|
+
// finalization handler to always have access to the input data in the case we need to remove events
|
|
144
|
+
// because of chain reorganisations
|
|
140
145
|
blocks: {
|
|
141
|
-
async
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (key === void 0) {
|
|
146
|
-
return null;
|
|
146
|
+
async list(chain, number) {
|
|
147
|
+
let prefix = `blocks/v1/${normalizeHex(chain)}`;
|
|
148
|
+
if (typeof number === "string") {
|
|
149
|
+
prefix += `/${normalizeHex(number, 16)}`;
|
|
147
150
|
}
|
|
148
|
-
const [_, __, ___, number, hash] = key.path.split("/");
|
|
149
|
-
return { chain, number, hash };
|
|
150
|
-
},
|
|
151
|
-
async getBlocksByNumber(chain, number) {
|
|
152
|
-
const prefix = `blocks/v1/${normalizeHex(chain)}/${normalizeHex(number, 16)}`;
|
|
153
151
|
const keys = await opts.metadataStorage.list({ prefix });
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const blob = await opts.metadataStorage.download(item.path, { as: "blob" });
|
|
160
|
-
const block = await decompress(blob);
|
|
161
|
-
const parsed = JSON.parse(block);
|
|
162
|
-
return parsed;
|
|
163
|
-
})
|
|
164
|
-
);
|
|
165
|
-
return blocks;
|
|
152
|
+
const mapped = keys.items.map((key) => {
|
|
153
|
+
const [_, __, ___, number2, hash, parent_hash] = key.path.split("/");
|
|
154
|
+
return { chain, number: number2, hash, parent_hash };
|
|
155
|
+
});
|
|
156
|
+
return mapped;
|
|
166
157
|
},
|
|
167
|
-
async
|
|
168
|
-
const
|
|
158
|
+
async get(head) {
|
|
159
|
+
const chain = normalizeHex(head.chain);
|
|
160
|
+
const number = normalizeHex(head.number, 16);
|
|
161
|
+
const hash = normalizeHex(head.hash);
|
|
162
|
+
const parentHash = normalizeHex(head.parent_hash);
|
|
163
|
+
const prefix = `blocks/v1/${chain}/${number}/${hash}/${parentHash}`;
|
|
169
164
|
const blob = await opts.metadataStorage.download(prefix, { as: "blob" }).catch((error) => {
|
|
170
165
|
if (error instanceof StorageError) {
|
|
171
166
|
if (error.code === "NotFound") {
|
|
@@ -188,9 +183,10 @@ function indexer(opts) {
|
|
|
188
183
|
await Promise.all(
|
|
189
184
|
blocks.map(async (block) => {
|
|
190
185
|
const chain = normalizeHex(block.eth_chainId);
|
|
191
|
-
const hash = normalizeHex(block.eth_getBlockByNumber.hash);
|
|
192
186
|
const number = normalizeHex(block.eth_getBlockByNumber.number, 16);
|
|
193
|
-
const
|
|
187
|
+
const hash = normalizeHex(block.eth_getBlockByNumber.hash);
|
|
188
|
+
const parentHash = normalizeHex(block.eth_getBlockByNumber.parentHash);
|
|
189
|
+
const prefix = `blocks/v1/${chain}/${number}/${hash}/${parentHash}`;
|
|
194
190
|
const compressed = await compress(JSON.stringify(block));
|
|
195
191
|
await opts.metadataStorage.upload(prefix, compressed);
|
|
196
192
|
})
|
|
@@ -202,14 +198,75 @@ function indexer(opts) {
|
|
|
202
198
|
}
|
|
203
199
|
await Promise.all(
|
|
204
200
|
blocks.map(async (block) => {
|
|
205
|
-
const chain = normalizeHex(block.
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
const
|
|
201
|
+
const chain = normalizeHex(block.chain);
|
|
202
|
+
const number = normalizeHex(block.number, 16);
|
|
203
|
+
const hash = normalizeHex(block.hash);
|
|
204
|
+
const parentHash = normalizeHex(block.parent_hash);
|
|
205
|
+
const prefix = `blocks/v1/${chain}/${number}/${hash}/${parentHash}`;
|
|
206
|
+
await opts.metadataStorage.delete(prefix);
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
// When unfinalized events are upserted we commit a given block number and block hash to indicate
|
|
212
|
+
// that those events were successfully recorded. This enables an optimisation in our finalized
|
|
213
|
+
// handler that allows it to skip processing blocks that were successfully processed
|
|
214
|
+
commits: {
|
|
215
|
+
async list(chain, number) {
|
|
216
|
+
let prefix = `commits/v1/${normalizeHex(chain)}`;
|
|
217
|
+
if (typeof number === "string") {
|
|
218
|
+
prefix += `/${normalizeHex(number, 16)}`;
|
|
219
|
+
}
|
|
220
|
+
const keys = await opts.metadataStorage.list({ prefix });
|
|
221
|
+
const mapped = keys.items.map((key) => {
|
|
222
|
+
const [_, __, ___, number2, hash, parent_hash] = key.path.split("/");
|
|
223
|
+
return { chain, number: number2, hash, parent_hash };
|
|
224
|
+
});
|
|
225
|
+
return mapped;
|
|
226
|
+
},
|
|
227
|
+
async upsert(head) {
|
|
228
|
+
const chain = normalizeHex(head.chain);
|
|
229
|
+
const number = normalizeHex(head.number, 16);
|
|
230
|
+
const hash = normalizeHex(head.hash);
|
|
231
|
+
const parentHash = normalizeHex(head.parent_hash);
|
|
232
|
+
const prefix = `commits/v1/${chain}/${number}/${hash}/${parentHash}`;
|
|
233
|
+
const body = JSON.stringify({ hello: "world" });
|
|
234
|
+
await opts.metadataStorage.upload(prefix, body);
|
|
235
|
+
},
|
|
236
|
+
async delete(commits) {
|
|
237
|
+
await Promise.all(
|
|
238
|
+
commits.map(async (commit) => {
|
|
239
|
+
const chain = normalizeHex(commit.chain);
|
|
240
|
+
const number = normalizeHex(commit.number, 16);
|
|
241
|
+
const hash = normalizeHex(commit.hash);
|
|
242
|
+
const parentHash = normalizeHex(commit.parent_hash);
|
|
243
|
+
const prefix = `commits/v1/${chain}/${number}/${hash}/${parentHash}`;
|
|
209
244
|
await opts.metadataStorage.delete(prefix);
|
|
210
245
|
})
|
|
211
246
|
);
|
|
212
247
|
}
|
|
248
|
+
},
|
|
249
|
+
// Our indexer always lags behind the canonical chain. The heights table is used as a durable record
|
|
250
|
+
// to track its own finalized height. As the chain finalizes, we process newly finalized blocks and
|
|
251
|
+
// then update the heights here to reflect to that outside world what height the indexer has currently
|
|
252
|
+
// finalized up to. This information is stored in a separate table so that we can `list` keys and
|
|
253
|
+
// immediately know which blocks were finalized correctly without ever having to load the entire block
|
|
254
|
+
// values themselves reducing memory usage and increasing finalizing throughput
|
|
255
|
+
heights: {
|
|
256
|
+
async list(chain) {
|
|
257
|
+
const prefix = `heights/v1/${normalizeHex(chain)}`;
|
|
258
|
+
const keys = await opts.metadataStorage.list({ prefix });
|
|
259
|
+
const mapped = keys.items.map((key) => {
|
|
260
|
+
const [_, __, ___, number] = key.path.split("/");
|
|
261
|
+
return { chain, number };
|
|
262
|
+
});
|
|
263
|
+
return mapped;
|
|
264
|
+
},
|
|
265
|
+
async upsert(chain, number) {
|
|
266
|
+
const prefix = `heights/v1/${normalizeHex(chain)}/${normalizeHex(number, 16)}`;
|
|
267
|
+
const body = JSON.stringify({ hello: "world" });
|
|
268
|
+
await opts.metadataStorage.upload(prefix, body);
|
|
269
|
+
}
|
|
213
270
|
}
|
|
214
271
|
};
|
|
215
272
|
async function getBlock(head) {
|
|
@@ -237,21 +294,18 @@ function indexer(opts) {
|
|
|
237
294
|
}
|
|
238
295
|
const GetBlockError = createException("Failed to load block from the provided `getBlock` function");
|
|
239
296
|
const public_getFinalizedHeight = async (chain) => {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const finalizedHeight = hexToNumber(finalizedBlock.eth_getBlockByNumber.number);
|
|
250
|
-
if (stored === null) {
|
|
251
|
-
return finalizedHeight;
|
|
297
|
+
const heights = await metadata.heights.list(chain);
|
|
298
|
+
if (heights.length === 0) {
|
|
299
|
+
const block = await getBlock({ chain, number: "finalized" });
|
|
300
|
+
if (block === null) {
|
|
301
|
+
log.debug("Failed to fetch finalized block and unable to determine finalized height, aborting...");
|
|
302
|
+
throw new Error(GetBlockError);
|
|
303
|
+
}
|
|
304
|
+
await metadata.heights.upsert(chain, block.eth_getBlockByNumber.number);
|
|
305
|
+
return hexToNumber(block.eth_getBlockByNumber.number);
|
|
252
306
|
}
|
|
253
|
-
const
|
|
254
|
-
return
|
|
307
|
+
const height = Math.max(...heights.map((height2) => hexToNumber(height2.number)));
|
|
308
|
+
return height;
|
|
255
309
|
};
|
|
256
310
|
const public_writeUnfinalizedHead = async (head) => {
|
|
257
311
|
log.debug("Received unfinalized head...");
|
|
@@ -276,7 +330,7 @@ function indexer(opts) {
|
|
|
276
330
|
await metadata.blocks.upsert([block]);
|
|
277
331
|
log.debug(`Persisted block to metadata in ${Date.now() - metadata_start}ms`);
|
|
278
332
|
const events_start = Date.now();
|
|
279
|
-
const
|
|
333
|
+
const events = events_grouped_by_storage_map.entries().map(async ([storage, grouped_events]) => {
|
|
280
334
|
const batch = [];
|
|
281
335
|
for (const event2 of grouped_events) {
|
|
282
336
|
try {
|
|
@@ -284,8 +338,8 @@ function indexer(opts) {
|
|
|
284
338
|
log.debug(`Block matches no filters for event ${event2.id}`);
|
|
285
339
|
continue;
|
|
286
340
|
}
|
|
287
|
-
const
|
|
288
|
-
for (const event3 of
|
|
341
|
+
const events2 = event2.handler(block);
|
|
342
|
+
for (const event3 of events2) {
|
|
289
343
|
batch.push(event3);
|
|
290
344
|
}
|
|
291
345
|
} catch (error) {
|
|
@@ -306,94 +360,28 @@ function indexer(opts) {
|
|
|
306
360
|
}
|
|
307
361
|
}
|
|
308
362
|
});
|
|
309
|
-
await Promise.all(
|
|
310
|
-
log.debug(`Wrote
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
return log.debug("No heads received, returning...");
|
|
315
|
-
}
|
|
316
|
-
let chain = void 0;
|
|
317
|
-
for (const head of heads) {
|
|
318
|
-
if (chain === void 0) {
|
|
319
|
-
chain = head.chain;
|
|
320
|
-
}
|
|
321
|
-
if (!isHexEqual(chain, head.chain)) {
|
|
322
|
-
throw new Error("Received heads from separate chains");
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
if (chain === void 0) {
|
|
326
|
-
throw new Error("Internal error. Expected chain to be defined after checking heads length");
|
|
327
|
-
}
|
|
328
|
-
log.debug(`Received ${heads.length} unfinalized heads...`);
|
|
329
|
-
const blocks_start = Date.now();
|
|
330
|
-
const [finalizedBlock, ...blocks_nullable] = await Promise.all([
|
|
331
|
-
getBlock({ chain, number: "finalized" }),
|
|
332
|
-
...heads.map(async (head) => {
|
|
333
|
-
return await getBlock({ chain, number: head.number, hash: head.hash });
|
|
334
|
-
})
|
|
335
|
-
]);
|
|
336
|
-
if (finalizedBlock === null) {
|
|
337
|
-
log.debug("Failed to determine finalized height when processing unfinalized heads");
|
|
338
|
-
throw new Error(GetBlockError);
|
|
339
|
-
}
|
|
340
|
-
const finalizedHeight = hexToNumber(finalizedBlock.eth_getBlockByNumber.number);
|
|
341
|
-
const blocks = blocks_nullable.flatMap((block) => {
|
|
342
|
-
if (block === null) {
|
|
343
|
-
return [];
|
|
344
|
-
}
|
|
345
|
-
if (hexToNumber(block.eth_getBlockByNumber.number) <= finalizedHeight) {
|
|
363
|
+
await Promise.all(events);
|
|
364
|
+
log.debug(`Wrote events in ${Date.now() - events_start}ms`);
|
|
365
|
+
await metadata.commits.upsert(head);
|
|
366
|
+
const actions = all_actions.flatMap((action2) => {
|
|
367
|
+
if (typeof action2.handler.latest === "undefined") {
|
|
346
368
|
return [];
|
|
347
369
|
}
|
|
348
|
-
return block
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
return log.debug("All blocks failed to load, aborting early...");
|
|
352
|
-
}
|
|
353
|
-
log.debug(`Loaded ${blocks.length} block(s) in ${Date.now() - blocks_start}ms`);
|
|
354
|
-
await metadata.blocks.upsert(blocks);
|
|
355
|
-
log.debug(`Recorded ${blocks.length} block(s) to metadata storage`);
|
|
356
|
-
const events_start = Date.now();
|
|
357
|
-
const promises = events_grouped_by_storage_map.entries().map(async ([storage, grouped_events]) => {
|
|
358
|
-
const batch = [];
|
|
359
|
-
for (const block of blocks) {
|
|
360
|
-
for (const event2 of grouped_events) {
|
|
361
|
-
try {
|
|
362
|
-
if (!event2.filters.some((filter) => matchFilter(block, filter))) {
|
|
363
|
-
log.debug(`Block matches no filters for event ${event2.id}`);
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
const events = event2.handler(block);
|
|
367
|
-
for (const event3 of events) {
|
|
368
|
-
batch.push(event3);
|
|
369
|
-
}
|
|
370
|
-
} catch (error) {
|
|
371
|
-
log.error(`Failed to run your 'handler' for event ${event2.id}`);
|
|
372
|
-
throw error;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
if (batch.length > 0) {
|
|
377
|
-
const start = Date.now();
|
|
378
|
-
await retry(() => storage.upsert(batch), 2).catch((error) => {
|
|
379
|
-
for (const event2 of grouped_events) {
|
|
380
|
-
log.error(`Failed to run your 'upsert' handler for event ${event2.id}`);
|
|
381
|
-
}
|
|
370
|
+
return action2.event.handler(block).map(async (event2) => {
|
|
371
|
+
await retry(() => action2.handler.latest(event2), 2).catch((error) => {
|
|
372
|
+
log.error(`Failed to execute 'latest' action ${action2.id}`);
|
|
382
373
|
throw error;
|
|
383
374
|
});
|
|
384
|
-
|
|
385
|
-
log.debug(`Recorded ${batch.length} ${event2.id} in ${Date.now() - start}ms`);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
375
|
+
});
|
|
388
376
|
});
|
|
389
|
-
await Promise.
|
|
390
|
-
log.debug(`Wrote ${heads.length} heads in ${Date.now() - events_start}ms`);
|
|
377
|
+
await Promise.allSettled(actions);
|
|
391
378
|
};
|
|
392
379
|
const public_deleteReorganisedHead = async (head) => {
|
|
393
380
|
log.debug(`Received reorganised head ${hexToNumber(head.number)}`);
|
|
394
|
-
const [
|
|
395
|
-
|
|
396
|
-
|
|
381
|
+
const [stored_block, canonical_block] = await Promise.all([
|
|
382
|
+
metadata.blocks.get(head),
|
|
383
|
+
//
|
|
384
|
+
getBlock({ chain: head.chain, number: head.number })
|
|
397
385
|
]);
|
|
398
386
|
if (stored_block === null) {
|
|
399
387
|
return log.debug("Reorganised block never/already processed");
|
|
@@ -404,10 +392,7 @@ function indexer(opts) {
|
|
|
404
392
|
if (isHexEqual(head.hash, canonical_block.eth_getBlockByNumber.hash)) {
|
|
405
393
|
throw new Error("Attempted to delete canonical block");
|
|
406
394
|
}
|
|
407
|
-
const
|
|
408
|
-
if (event2.storage.delete === void 0) {
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
395
|
+
const deletes = all_events.map(async (event2) => {
|
|
411
396
|
const batch = [];
|
|
412
397
|
try {
|
|
413
398
|
const events = event2.handler(stored_block);
|
|
@@ -426,11 +411,37 @@ function indexer(opts) {
|
|
|
426
411
|
throw error;
|
|
427
412
|
});
|
|
428
413
|
});
|
|
429
|
-
await Promise.all(
|
|
414
|
+
await Promise.all(deletes);
|
|
415
|
+
const upserts = events_grouped_by_storage_map.entries().map(async ([storage, grouped_events]) => {
|
|
416
|
+
const batch = [];
|
|
417
|
+
for (const event2 of grouped_events) {
|
|
418
|
+
try {
|
|
419
|
+
if (!event2.filters.some((filter) => matchFilter(canonical_block, filter))) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const events = event2.handler(canonical_block);
|
|
423
|
+
for (const event3 of events) {
|
|
424
|
+
batch.push(event3);
|
|
425
|
+
}
|
|
426
|
+
} catch (error) {
|
|
427
|
+
log.error(`Failed to run your 'handler' for event ${event2.id}`);
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (batch.length > 0) {
|
|
432
|
+
await retry(() => storage.upsert(batch), 2).catch((error) => {
|
|
433
|
+
for (const event2 of grouped_events) {
|
|
434
|
+
log.error(`Failed to run your 'upsert' handler for event ${event2.id}`);
|
|
435
|
+
}
|
|
436
|
+
throw error;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
await Promise.all(upserts);
|
|
430
441
|
};
|
|
431
442
|
const public_writeFinalizedHeads = async (heads) => {
|
|
432
443
|
if (heads.length === 0) {
|
|
433
|
-
|
|
444
|
+
throw new Error("No heads received, aborting...");
|
|
434
445
|
}
|
|
435
446
|
log.debug(`Received ${heads.length} finalized heads...`);
|
|
436
447
|
let chain = void 0;
|
|
@@ -459,8 +470,8 @@ function indexer(opts) {
|
|
|
459
470
|
}
|
|
460
471
|
previousHead = head;
|
|
461
472
|
}
|
|
462
|
-
const [
|
|
463
|
-
metadata.
|
|
473
|
+
const [heights, finalizedBlock] = await Promise.all([
|
|
474
|
+
metadata.heights.list(chain),
|
|
464
475
|
//
|
|
465
476
|
getBlock({ chain, number: "finalized" })
|
|
466
477
|
]);
|
|
@@ -468,56 +479,82 @@ function indexer(opts) {
|
|
|
468
479
|
log.debug("Failed to load chain finalized block. Request should be retried");
|
|
469
480
|
throw new Error(GetBlockError);
|
|
470
481
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
throw new Error("Received head greater than chain finalized height");
|
|
475
|
-
}
|
|
482
|
+
if (heights.length === 0) {
|
|
483
|
+
await metadata.heights.upsert(chain, finalizedBlock.eth_getBlockByNumber.number);
|
|
484
|
+
heights.push({ chain, number: finalizedBlock.eth_getBlockByNumber.number });
|
|
476
485
|
}
|
|
477
|
-
|
|
478
|
-
|
|
486
|
+
const indexerHeight = Math.max(...heights.map((height) => hexToNumber(height.number)));
|
|
487
|
+
const finalizedHeight = hexToNumber(finalizedBlock.eth_getBlockByNumber.number);
|
|
488
|
+
const lastHead = heads[heads.length - 1];
|
|
489
|
+
if (lastHead === void 0 || hexToNumber(lastHead.number) !== finalizedHeight) {
|
|
490
|
+
throw new Error("Client has stalled. Chain has finalized greater than the heads received");
|
|
479
491
|
}
|
|
480
|
-
const indexerHeight = hexToNumber(unfinalizedMetadataBlock.number) - 1;
|
|
481
492
|
log.debug(`Indexer height ${indexerHeight}, finalized height ${finalizedHeight}`);
|
|
482
493
|
if (indexerHeight === finalizedHeight) {
|
|
483
|
-
|
|
494
|
+
log.debug("No heads to finalize, returning...");
|
|
495
|
+
return await cleanupFinalizedHeads(chain, numberToHex(finalizedHeight));
|
|
484
496
|
}
|
|
485
497
|
const newHeads = heads.filter((head) => {
|
|
486
498
|
return hexToNumber(head.number) > indexerHeight;
|
|
487
499
|
});
|
|
488
500
|
const [firstNewHead] = newHeads;
|
|
489
501
|
if (firstNewHead === void 0) {
|
|
490
|
-
|
|
502
|
+
throw new Error("Internal error. Expected at least one head greater than the indexer height.");
|
|
491
503
|
}
|
|
492
504
|
if (hexToNumber(firstNewHead.number) !== indexerHeight + 1) {
|
|
493
|
-
throw new Error(
|
|
494
|
-
`Client advanced too far (${hexToNumber(firstNewHead.number)}). Heads received are greater than the first new head ${indexerHeight + 1}`
|
|
495
|
-
);
|
|
505
|
+
throw new Error("Client advanced too far. Heads received are greater than the first new head");
|
|
496
506
|
}
|
|
497
507
|
for (const head of newHeads) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
508
|
+
await Promise.all([
|
|
509
|
+
processEventsForCanonicalHead(head),
|
|
510
|
+
processActionsForCanonicalHead(head)
|
|
511
|
+
//
|
|
512
|
+
]);
|
|
513
|
+
await metadata.heights.upsert(head.chain, head.number);
|
|
514
|
+
}
|
|
515
|
+
await cleanupFinalizedHeads(chain, numberToHex(finalizedHeight));
|
|
516
|
+
};
|
|
517
|
+
async function processActionsForCanonicalHead(head) {
|
|
518
|
+
if (all_actions.every((action2) => typeof action2.handler.finalized === "undefined")) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const block = await getBlock({ chain: head.chain, number: head.number, hash: head.hash });
|
|
522
|
+
const promises = all_actions.flatMap((action2) => {
|
|
523
|
+
if (typeof action2.handler.finalized === "undefined") {
|
|
524
|
+
return [];
|
|
502
525
|
}
|
|
503
|
-
|
|
504
|
-
|
|
526
|
+
return action2.event.handler(block).map(async (event2) => {
|
|
527
|
+
await retry(() => action2.handler.finalized(event2), 2).catch((error) => {
|
|
528
|
+
log.error(`Failed to execute 'finalized' action ${action2.id}`);
|
|
529
|
+
throw error;
|
|
530
|
+
});
|
|
505
531
|
});
|
|
506
|
-
|
|
507
|
-
|
|
532
|
+
});
|
|
533
|
+
await Promise.all(promises);
|
|
534
|
+
}
|
|
535
|
+
async function processEventsForCanonicalHead(head) {
|
|
536
|
+
const [processed, commits] = await Promise.all([
|
|
537
|
+
metadata.blocks.list(head.chain, head.number),
|
|
538
|
+
//
|
|
539
|
+
metadata.commits.list(head.chain, head.number)
|
|
540
|
+
]);
|
|
541
|
+
if (processed.length === 1) {
|
|
542
|
+
if (commits.some((commit) => isHexEqual(head.hash, commit.hash) && isHexEqual(head.parent_hash, commit.parent_hash))) {
|
|
543
|
+
return;
|
|
508
544
|
}
|
|
509
|
-
const block = processed.find((block2) => {
|
|
510
|
-
return isHexEqual(head.hash, block2.eth_getBlockByNumber.hash);
|
|
511
|
-
});
|
|
512
|
-
await writeFinalizedHead(head, block ?? null);
|
|
513
|
-
await metadata.blocks.delete(processed);
|
|
514
|
-
}
|
|
515
|
-
};
|
|
516
|
-
async function writeFinalizedHead(head, block_nullable) {
|
|
517
|
-
let block = block_nullable;
|
|
518
|
-
if (block === null) {
|
|
519
|
-
block = await getBlock({ chain: head.chain, number: head.number, hash: head.hash });
|
|
520
545
|
}
|
|
546
|
+
const reorganised = processed.filter((block) => {
|
|
547
|
+
return !isHexEqual(head.hash, block.hash);
|
|
548
|
+
});
|
|
549
|
+
await Promise.all(
|
|
550
|
+
reorganised.map(async (block) => {
|
|
551
|
+
await deleteReorganisedHead(block);
|
|
552
|
+
})
|
|
553
|
+
);
|
|
554
|
+
await writeFinalizedHead(head);
|
|
555
|
+
}
|
|
556
|
+
async function writeFinalizedHead(head) {
|
|
557
|
+
const block = await getBlock({ chain: head.chain, number: head.number, hash: head.hash });
|
|
521
558
|
if (block === null) {
|
|
522
559
|
throw new Error("Received unknown head that wasn't canonical");
|
|
523
560
|
}
|
|
@@ -553,21 +590,17 @@ function indexer(opts) {
|
|
|
553
590
|
});
|
|
554
591
|
await Promise.all(promises);
|
|
555
592
|
}
|
|
556
|
-
async function
|
|
557
|
-
|
|
558
|
-
|
|
593
|
+
async function deleteReorganisedHead(head) {
|
|
594
|
+
const block = await metadata.blocks.get(head);
|
|
595
|
+
if (block === null) {
|
|
596
|
+
throw new Error("Expected reorganised block to exist in metadata");
|
|
559
597
|
}
|
|
560
598
|
const promises = all_events.map(async (event2) => {
|
|
561
|
-
if (event2.storage.delete === void 0) {
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
599
|
const batch = [];
|
|
565
600
|
try {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
batch.push(event3);
|
|
570
|
-
}
|
|
601
|
+
const events = event2.handler(block);
|
|
602
|
+
for (const event3 of events) {
|
|
603
|
+
batch.push(event3);
|
|
571
604
|
}
|
|
572
605
|
} catch (error) {
|
|
573
606
|
log.error(`Failed to run your 'handler' for event ${event2.id}`);
|
|
@@ -583,6 +616,24 @@ function indexer(opts) {
|
|
|
583
616
|
});
|
|
584
617
|
await Promise.all(promises);
|
|
585
618
|
}
|
|
619
|
+
async function cleanupFinalizedHeads(chain, finalized) {
|
|
620
|
+
const [_blocks, _commits] = await Promise.all([
|
|
621
|
+
metadata.blocks.list(chain),
|
|
622
|
+
//
|
|
623
|
+
metadata.commits.list(chain)
|
|
624
|
+
]);
|
|
625
|
+
const blocks = _blocks.filter((block) => {
|
|
626
|
+
return hexToNumber(block.number) <= hexToNumber(finalized);
|
|
627
|
+
});
|
|
628
|
+
const commits = _commits.filter((commit) => {
|
|
629
|
+
return hexToNumber(commit.number) < hexToNumber(finalized);
|
|
630
|
+
});
|
|
631
|
+
await Promise.all([
|
|
632
|
+
metadata.blocks.delete(blocks),
|
|
633
|
+
//
|
|
634
|
+
metadata.commits.delete(commits)
|
|
635
|
+
]);
|
|
636
|
+
}
|
|
586
637
|
const private_getMetadata = async () => {
|
|
587
638
|
return {
|
|
588
639
|
version,
|
|
@@ -855,12 +906,15 @@ function indexer(opts) {
|
|
|
855
906
|
events_grouped_by_storage_map.set(event2.storage, group);
|
|
856
907
|
return event2;
|
|
857
908
|
};
|
|
909
|
+
const action = (action2) => {
|
|
910
|
+
all_actions.push(action2);
|
|
911
|
+
return action2;
|
|
912
|
+
};
|
|
858
913
|
const rpc = {
|
|
859
914
|
request: {
|
|
860
915
|
public_getFinalizedHeight,
|
|
861
916
|
public_writeFinalizedHeads,
|
|
862
917
|
public_writeUnfinalizedHead,
|
|
863
|
-
public_writeUnfinalizedHeads,
|
|
864
918
|
public_deleteReorganisedHead,
|
|
865
919
|
private_getEvents,
|
|
866
920
|
private_getMetadata,
|
|
@@ -877,6 +931,7 @@ function indexer(opts) {
|
|
|
877
931
|
const indexer2 = {
|
|
878
932
|
...rpc,
|
|
879
933
|
event,
|
|
934
|
+
action,
|
|
880
935
|
fetch: server.http
|
|
881
936
|
};
|
|
882
937
|
return indexer2;
|
package/dist/realtime.d.mts
CHANGED
package/dist/realtime.d.ts
CHANGED
package/dist/realtime.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as createLogger, i as iife, h as hexToNumber, n as numberToHex, m as mutex, r as retry, a as isHexEqual } from './shared/univo.
|
|
1
|
+
import { c as createLogger, i as iife, h as hexToNumber, n as numberToHex, m as mutex, r as retry, a as isHexEqual } from './shared/univo.bwpFiVa7.mjs';
|
|
2
2
|
|
|
3
3
|
function defineBlockchain(opts) {
|
|
4
4
|
const log = createLogger({ quiet: opts.quiet });
|
|
@@ -165,7 +165,7 @@ function realtime(opts) {
|
|
|
165
165
|
});
|
|
166
166
|
log.debug("Subscribed indexer chain to new heads");
|
|
167
167
|
let indexerHeight = initialIndexerHeight;
|
|
168
|
-
|
|
168
|
+
async function poll() {
|
|
169
169
|
try {
|
|
170
170
|
log.debug("Polling for finalized height...");
|
|
171
171
|
const finalizedBlock = await opts.node.request({
|
|
@@ -189,7 +189,11 @@ function realtime(opts) {
|
|
|
189
189
|
return log.debug("No new finalized heads to deliver, ignoring...");
|
|
190
190
|
}
|
|
191
191
|
log.debug(`Delivering ${newHeads.length} finalized head(s)...`);
|
|
192
|
-
await opts.indexer.request({
|
|
192
|
+
await opts.indexer.request({
|
|
193
|
+
params: [newHeads],
|
|
194
|
+
method: "public_writeFinalizedHeads",
|
|
195
|
+
signal: AbortSignal.timeout(Number.POSITIVE_INFINITY)
|
|
196
|
+
});
|
|
193
197
|
log.debug(`Delivered ${newHeads.length} finalized head(s)`);
|
|
194
198
|
indexerHeight = hexToNumber(finalizedBlock.number);
|
|
195
199
|
log.debug(`Updated indexer height to ${indexerHeight}`);
|
|
@@ -207,11 +211,9 @@ function realtime(opts) {
|
|
|
207
211
|
if (error instanceof Error) {
|
|
208
212
|
log.error(`Failed to reconcile finalized head: ${error.message}`);
|
|
209
213
|
}
|
|
210
|
-
} finally {
|
|
211
|
-
setTimeout(poll, POLLING_INTERVAL_MS);
|
|
212
214
|
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
+
}
|
|
216
|
+
setInterval(mutex(poll), POLLING_INTERVAL_MS);
|
|
215
217
|
log.debug("Started polling finalized height");
|
|
216
218
|
});
|
|
217
219
|
promise.catch((error) => {
|