univo 0.2.18 → 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 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.GuW0GiSl.mjs';
8
- import { c as createLogger, v as version, b as name } from './shared/univo.TYA11b8i.mjs';
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.BNqa7KED.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.BNqa7KED.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.GuW0GiSl.mjs';
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.TYA11b8i.mjs';
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 getNextUnfinalizedBlock(chain) {
142
- const prefix = `blocks/v1/${normalizeHex(chain)}`;
143
- const keys = await opts.metadataStorage.list({ prefix, limit: 1 });
144
- const [key] = keys.items;
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
- if (keys.items.length === 0) {
155
- return [];
156
- }
157
- const blocks = await Promise.all(
158
- keys.items.map(async (item) => {
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 getBlockByNumberAndHash(chain, number, hash) {
168
- const prefix = `blocks/v1/${normalizeHex(chain)}/${normalizeHex(number, 16)}/${normalizeHex(hash)}`;
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 prefix = `blocks/v1/${chain}/${number}/${hash}`;
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.eth_chainId);
206
- const hash = normalizeHex(block.eth_getBlockByNumber.hash);
207
- const number = normalizeHex(block.eth_getBlockByNumber.number, 16);
208
- const prefix = `blocks/v1/${chain}/${number}/${hash}`;
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 [stored, finalizedBlock] = await Promise.all([
241
- metadata.blocks.getNextUnfinalizedBlock(chain),
242
- //
243
- getBlock({ chain, number: "finalized" })
244
- ]);
245
- if (finalizedBlock === null) {
246
- log.debug("Failed to fetch finalized block and unable to determine finalized height, aborting...");
247
- throw new Error(GetBlockError);
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 indexerHeight = hexToNumber(stored.number) - 1;
254
- return Math.min(indexerHeight, finalizedHeight);
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 promises = events_grouped_by_storage_map.entries().map(async ([storage, grouped_events]) => {
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 events = event2.handler(block);
288
- for (const event3 of events) {
341
+ const events2 = event2.handler(block);
342
+ for (const event3 of events2) {
289
343
  batch.push(event3);
290
344
  }
291
345
  } catch (error) {
@@ -306,14 +360,28 @@ function indexer(opts) {
306
360
  }
307
361
  }
308
362
  });
309
- await Promise.all(promises);
310
- log.debug(`Wrote head in ${Date.now() - events_start}ms`);
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") {
368
+ return [];
369
+ }
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}`);
373
+ throw error;
374
+ });
375
+ });
376
+ });
377
+ await Promise.allSettled(actions);
311
378
  };
312
379
  const public_deleteReorganisedHead = async (head) => {
313
380
  log.debug(`Received reorganised head ${hexToNumber(head.number)}`);
314
- const [canonical_block, stored_block] = await Promise.all([
315
- getBlock({ chain: head.chain, number: head.number }),
316
- metadata.blocks.getBlockByNumberAndHash(head.chain, head.number, head.hash)
381
+ const [stored_block, canonical_block] = await Promise.all([
382
+ metadata.blocks.get(head),
383
+ //
384
+ getBlock({ chain: head.chain, number: head.number })
317
385
  ]);
318
386
  if (stored_block === null) {
319
387
  return log.debug("Reorganised block never/already processed");
@@ -373,7 +441,7 @@ function indexer(opts) {
373
441
  };
374
442
  const public_writeFinalizedHeads = async (heads) => {
375
443
  if (heads.length === 0) {
376
- return log.debug("No heads received, returning...");
444
+ throw new Error("No heads received, aborting...");
377
445
  }
378
446
  log.debug(`Received ${heads.length} finalized heads...`);
379
447
  let chain = void 0;
@@ -402,8 +470,8 @@ function indexer(opts) {
402
470
  }
403
471
  previousHead = head;
404
472
  }
405
- const [unfinalizedMetadataBlock, finalizedBlock] = await Promise.all([
406
- metadata.blocks.getNextUnfinalizedBlock(chain),
473
+ const [heights, finalizedBlock] = await Promise.all([
474
+ metadata.heights.list(chain),
407
475
  //
408
476
  getBlock({ chain, number: "finalized" })
409
477
  ]);
@@ -411,56 +479,82 @@ function indexer(opts) {
411
479
  log.debug("Failed to load chain finalized block. Request should be retried");
412
480
  throw new Error(GetBlockError);
413
481
  }
414
- const finalizedHeight = hexToNumber(finalizedBlock.eth_getBlockByNumber.number);
415
- for (const head of heads) {
416
- if (hexToNumber(head.number) > finalizedHeight) {
417
- throw new Error("Received head greater than chain finalized height");
418
- }
482
+ if (heights.length === 0) {
483
+ await metadata.heights.upsert(chain, finalizedBlock.eth_getBlockByNumber.number);
484
+ heights.push({ chain, number: finalizedBlock.eth_getBlockByNumber.number });
419
485
  }
420
- if (unfinalizedMetadataBlock === null) {
421
- return log.debug("No heads to finalize, returning...");
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");
422
491
  }
423
- const indexerHeight = hexToNumber(unfinalizedMetadataBlock.number) - 1;
424
492
  log.debug(`Indexer height ${indexerHeight}, finalized height ${finalizedHeight}`);
425
493
  if (indexerHeight === finalizedHeight) {
426
- return log.debug("No heads to finalize, returning...");
494
+ log.debug("No heads to finalize, returning...");
495
+ return await cleanupFinalizedHeads(chain, numberToHex(finalizedHeight));
427
496
  }
428
497
  const newHeads = heads.filter((head) => {
429
498
  return hexToNumber(head.number) > indexerHeight;
430
499
  });
431
500
  const [firstNewHead] = newHeads;
432
501
  if (firstNewHead === void 0) {
433
- return log.debug("No new heads received, returning...");
502
+ throw new Error("Internal error. Expected at least one head greater than the indexer height.");
434
503
  }
435
504
  if (hexToNumber(firstNewHead.number) !== indexerHeight + 1) {
436
- throw new Error(
437
- `Client advanced too far (${hexToNumber(firstNewHead.number)}). Heads received are greater than the first new head ${indexerHeight + 1}`
438
- );
505
+ throw new Error("Client advanced too far. Heads received are greater than the first new head");
439
506
  }
440
507
  for (const head of newHeads) {
441
- const processed = await metadata.blocks.getBlocksByNumber(head.chain, head.number);
442
- if (processed.length === 0) {
443
- await writeFinalizedHead(head, null);
444
- continue;
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 [];
445
525
  }
446
- const reorganised = processed.filter((block2) => {
447
- return !isHexEqual(head.hash, block2.eth_getBlockByNumber.hash);
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
+ });
448
531
  });
449
- if (reorganised.length > 0) {
450
- await deleteReorganisedBlocks(reorganised);
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;
451
544
  }
452
- const block = processed.find((block2) => {
453
- return isHexEqual(head.hash, block2.eth_getBlockByNumber.hash);
454
- });
455
- await writeFinalizedHead(head, block ?? null);
456
- await metadata.blocks.delete(processed);
457
- }
458
- };
459
- async function writeFinalizedHead(head, block_nullable) {
460
- let block = block_nullable;
461
- if (block === null) {
462
- block = await getBlock({ chain: head.chain, number: head.number, hash: head.hash });
463
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 });
464
558
  if (block === null) {
465
559
  throw new Error("Received unknown head that wasn't canonical");
466
560
  }
@@ -496,18 +590,17 @@ function indexer(opts) {
496
590
  });
497
591
  await Promise.all(promises);
498
592
  }
499
- async function deleteReorganisedBlocks(blocks) {
500
- if (blocks.length === 0) {
501
- return;
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");
502
597
  }
503
598
  const promises = all_events.map(async (event2) => {
504
599
  const batch = [];
505
600
  try {
506
- for (const block of blocks) {
507
- const events = event2.handler(block);
508
- for (const event3 of events) {
509
- batch.push(event3);
510
- }
601
+ const events = event2.handler(block);
602
+ for (const event3 of events) {
603
+ batch.push(event3);
511
604
  }
512
605
  } catch (error) {
513
606
  log.error(`Failed to run your 'handler' for event ${event2.id}`);
@@ -523,6 +616,24 @@ function indexer(opts) {
523
616
  });
524
617
  await Promise.all(promises);
525
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
+ }
526
637
  const private_getMetadata = async () => {
527
638
  return {
528
639
  version,
@@ -795,6 +906,10 @@ function indexer(opts) {
795
906
  events_grouped_by_storage_map.set(event2.storage, group);
796
907
  return event2;
797
908
  };
909
+ const action = (action2) => {
910
+ all_actions.push(action2);
911
+ return action2;
912
+ };
798
913
  const rpc = {
799
914
  request: {
800
915
  public_getFinalizedHeight,
@@ -816,6 +931,7 @@ function indexer(opts) {
816
931
  const indexer2 = {
817
932
  ...rpc,
818
933
  event,
934
+ action,
819
935
  fetch: server.http
820
936
  };
821
937
  return indexer2;
@@ -1,6 +1,5 @@
1
1
  import { NodeRpc, IndexerRpc } from './rpc.mjs';
2
2
  import { Transport } from './transport.mjs';
3
- import './shared/univo.BNqa7KED.mjs';
4
3
  import '@storagesdk/core';
5
4
 
6
5
  type RealtimeOptions = {
@@ -1,6 +1,5 @@
1
1
  import { NodeRpc, IndexerRpc } from './rpc.js';
2
2
  import { Transport } from './transport.js';
3
- import './shared/univo.BNqa7KED.js';
4
3
  import '@storagesdk/core';
5
4
 
6
5
  type RealtimeOptions = {
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.TYA11b8i.mjs';
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
- const poll = async () => {
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({ method: "public_writeFinalizedHeads", params: [newHeads] });
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
- await poll();
215
+ }
216
+ setInterval(mutex(poll), POLLING_INTERVAL_MS);
215
217
  log.debug("Started polling finalized height");
216
218
  });
217
219
  promise.catch((error) => {