envio 3.2.0 → 3.3.0-alpha.0

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.
Files changed (113) hide show
  1. package/package.json +6 -7
  2. package/src/Batch.res +12 -15
  3. package/src/Batch.res.mjs +4 -5
  4. package/src/BatchProcessing.res +199 -0
  5. package/src/BatchProcessing.res.mjs +125 -0
  6. package/src/ChainFetching.res +375 -0
  7. package/src/ChainFetching.res.mjs +206 -0
  8. package/src/ChainMetadata.res +27 -0
  9. package/src/ChainMetadata.res.mjs +27 -0
  10. package/src/ChainState.res +618 -0
  11. package/src/ChainState.res.mjs +461 -0
  12. package/src/ChainState.resi +85 -0
  13. package/src/Config.res +13 -4
  14. package/src/Config.res.mjs +8 -3
  15. package/src/ContractRegisterContext.res +106 -0
  16. package/src/ContractRegisterContext.res.mjs +76 -0
  17. package/src/Core.res +3 -0
  18. package/src/CrossChainState.res +269 -0
  19. package/src/CrossChainState.res.mjs +197 -0
  20. package/src/CrossChainState.resi +47 -0
  21. package/src/Ecosystem.res +58 -0
  22. package/src/Ecosystem.res.mjs +43 -1
  23. package/src/Env.res +7 -2
  24. package/src/Env.res.mjs +5 -2
  25. package/src/EventConfigBuilder.res +35 -0
  26. package/src/EventConfigBuilder.res.mjs +21 -0
  27. package/src/EventProcessing.res +60 -46
  28. package/src/EventProcessing.res.mjs +33 -32
  29. package/src/EventUtils.res +0 -4
  30. package/src/EventUtils.res.mjs +0 -4
  31. package/src/ExitOnCaughtUp.res +10 -0
  32. package/src/ExitOnCaughtUp.res.mjs +22 -0
  33. package/src/FetchState.res +162 -113
  34. package/src/FetchState.res.mjs +181 -119
  35. package/src/HandlerLoader.res +6 -1
  36. package/src/HandlerLoader.res.mjs +13 -36
  37. package/src/InMemoryStore.res +36 -451
  38. package/src/InMemoryStore.res.mjs +20 -369
  39. package/src/IndexerLoop.res +44 -0
  40. package/src/IndexerLoop.res.mjs +46 -0
  41. package/src/IndexerState.res +544 -0
  42. package/src/IndexerState.res.mjs +547 -0
  43. package/src/IndexerState.resi +132 -0
  44. package/src/Internal.res +44 -7
  45. package/src/LoadLayer.res +20 -18
  46. package/src/LoadLayer.res.mjs +14 -12
  47. package/src/LoadLayer.resi +6 -3
  48. package/src/LogSelection.res +86 -17
  49. package/src/LogSelection.res.mjs +67 -14
  50. package/src/Logging.res +11 -44
  51. package/src/Logging.res.mjs +10 -42
  52. package/src/Main.res +54 -79
  53. package/src/Main.res.mjs +44 -56
  54. package/src/PgStorage.res +8 -71
  55. package/src/PgStorage.res.mjs +3 -47
  56. package/src/Prometheus.res +7 -8
  57. package/src/Prometheus.res.mjs +7 -4
  58. package/src/PruneStaleHistory.res +45 -0
  59. package/src/PruneStaleHistory.res.mjs +46 -0
  60. package/src/RawEvent.res +73 -0
  61. package/src/RawEvent.res.mjs +51 -0
  62. package/src/Rollback.res +204 -0
  63. package/src/Rollback.res.mjs +118 -0
  64. package/src/SimulateItems.res +12 -10
  65. package/src/SimulateItems.res.mjs +1 -1
  66. package/src/TestIndexer.res +34 -27
  67. package/src/TestIndexer.res.mjs +4 -1
  68. package/src/UserContext.res +26 -114
  69. package/src/UserContext.res.mjs +15 -78
  70. package/src/Writing.res +242 -0
  71. package/src/Writing.res.mjs +215 -0
  72. package/src/db/InternalTable.res +14 -27
  73. package/src/sources/Evm.res +40 -1
  74. package/src/sources/Evm.res.mjs +94 -84
  75. package/src/sources/Fuel.res +40 -1
  76. package/src/sources/Fuel.res.mjs +36 -26
  77. package/src/sources/HyperFuel.res +41 -120
  78. package/src/sources/HyperFuel.res.mjs +42 -80
  79. package/src/sources/HyperFuel.resi +1 -24
  80. package/src/sources/HyperFuelClient.res +16 -297
  81. package/src/sources/HyperFuelClient.res.mjs +5 -4
  82. package/src/sources/HyperFuelSource.res +33 -8
  83. package/src/sources/HyperFuelSource.res.mjs +56 -10
  84. package/src/sources/HyperSyncSource.res +7 -5
  85. package/src/sources/HyperSyncSource.res.mjs +3 -3
  86. package/src/sources/RpcSource.res +2 -2
  87. package/src/sources/RpcSource.res.mjs +1 -1
  88. package/src/sources/SourceManager.res +8 -18
  89. package/src/sources/SourceManager.res.mjs +8 -5
  90. package/src/sources/SourceManager.resi +4 -1
  91. package/src/sources/Svm.res +18 -1
  92. package/src/sources/Svm.res.mjs +30 -22
  93. package/src/sources/SvmHyperSyncSource.res +1 -1
  94. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
  95. package/src/tui/Tui.res +38 -36
  96. package/src/tui/Tui.res.mjs +51 -51
  97. package/src/ChainFetcher.res +0 -519
  98. package/src/ChainFetcher.res.mjs +0 -314
  99. package/src/ChainManager.res +0 -358
  100. package/src/ChainManager.res.mjs +0 -291
  101. package/src/Ctx.res +0 -6
  102. package/src/Ctx.res.mjs +0 -2
  103. package/src/GlobalState.res +0 -1056
  104. package/src/GlobalState.res.mjs +0 -1086
  105. package/src/GlobalStateManager.res +0 -57
  106. package/src/GlobalStateManager.res.mjs +0 -68
  107. package/src/GlobalStateManager.resi +0 -7
  108. package/src/Ports.res +0 -5
  109. package/src/Ports.res.mjs +0 -9
  110. package/src/adapters/MarkBatchProcessedAdapter.res +0 -5
  111. package/src/adapters/MarkBatchProcessedAdapter.res.mjs +0 -14
  112. package/src/sources/EnvioApiClient.res +0 -15
  113. package/src/sources/EnvioApiClient.res.mjs +0 -24
@@ -1,9 +1,6 @@
1
- type contractConfig = {filterByAddresses: bool, startBlock: option<int>}
1
+ type contractConfig = {startBlock: option<int>}
2
2
 
3
- type indexingAddress = {
4
- ...Internal.indexingAddress,
5
- effectiveStartBlock: int,
6
- }
3
+ type indexingAddress = Internal.indexingContract
7
4
 
8
5
  let deriveEffectiveStartBlock = (~registrationBlock: int, ~contractStartBlock: option<int>) => {
9
6
  Pervasives.max(Pervasives.max(registrationBlock, 0), contractStartBlock->Option.getOr(0))
@@ -499,9 +496,10 @@ type t = {
499
496
  blockLag: int,
500
497
  // Buffer of items ordered from earliest to latest
501
498
  buffer: array<Internal.item>,
502
- // How many items we should aim to have in the buffer
503
- // ready for processing
504
- targetBufferSize: int,
499
+ // Caps how far ahead onBlock items are pre-generated (set to 2x the batch
500
+ // size). Fetch depth is bounded separately by getNextQuery's bufferLimit, the
501
+ // chain's per-tick slice of the indexer-wide pool.
502
+ maxOnBlockBufferSize: int,
505
503
  onBlockConfigs: array<Internal.onBlockConfig>,
506
504
  knownHeight: int,
507
505
  firstEventBlock: option<int>,
@@ -557,7 +555,7 @@ let numAddresses = fetchState => fetchState.indexingAddresses->Utils.Dict.size
557
555
 
558
556
  // Appends Block items produced by the onBlock handlers for every block in
559
557
  // (fromBlock, maxBlockNumber] into mutItems and returns the new
560
- // latestOnBlockBlockNumber pointer. targetBufferSize bounds how many items
558
+ // latestOnBlockBlockNumber pointer. maxOnBlockBufferSize bounds how many items
561
559
  // are generated at once to prevent OOM.
562
560
  let appendOnBlockItems = (
563
561
  ~mutItems: array<Internal.item>,
@@ -565,7 +563,7 @@ let appendOnBlockItems = (
565
563
  ~indexerStartBlock,
566
564
  ~fromBlock,
567
565
  ~maxBlockNumber,
568
- ~targetBufferSize,
566
+ ~maxOnBlockBufferSize,
569
567
  ) => {
570
568
  let newItemsCounter = ref(0)
571
569
  let latestOnBlockBlockNumber = ref(fromBlock)
@@ -576,7 +574,7 @@ let appendOnBlockItems = (
576
574
  while (
577
575
  latestOnBlockBlockNumber.contents < maxBlockNumber &&
578
576
  // Additional safeguard to prevent OOM
579
- newItemsCounter.contents <= targetBufferSize
577
+ newItemsCounter.contents <= maxOnBlockBufferSize
580
578
  ) {
581
579
  let blockNumber = latestOnBlockBlockNumber.contents + 1
582
580
  latestOnBlockBlockNumber := blockNumber
@@ -630,7 +628,7 @@ let updateInternal = (
630
628
  | [] => knownHeight
631
629
  | onBlockConfigs => {
632
630
  // Calculate the max block number we are going to create items for
633
- // Use targetBufferSize to get the last target item in the buffer
631
+ // Use maxOnBlockBufferSize to get the last target item in the buffer
634
632
  //
635
633
  // mutItems is not very reliable, since it might not be sorted,
636
634
  // but the chances for it happen are very low and not critical
@@ -639,7 +637,7 @@ let updateInternal = (
639
637
  let maxBlockNumber = switch switch mutItemsRef.contents {
640
638
  | Some(mutItems) => mutItems
641
639
  | None => fetchState.buffer
642
- }->Array.get(fetchState.targetBufferSize - 1) {
640
+ }->Array.get(fetchState.maxOnBlockBufferSize - 1) {
643
641
  | Some(item) => item->Internal.getItemBlockNumber
644
642
  | None =>
645
643
  switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
@@ -660,7 +658,7 @@ let updateInternal = (
660
658
  ~indexerStartBlock=fetchState.startBlock,
661
659
  ~fromBlock=fetchState.latestOnBlockBlockNumber,
662
660
  ~maxBlockNumber,
663
- ~targetBufferSize=fetchState.targetBufferSize,
661
+ ~maxOnBlockBufferSize=fetchState.maxOnBlockBufferSize,
664
662
  )
665
663
  }
666
664
  }
@@ -672,7 +670,7 @@ let updateInternal = (
672
670
  normalSelection: fetchState.normalSelection,
673
671
  chainId: fetchState.chainId,
674
672
  onBlockConfigs: fetchState.onBlockConfigs,
675
- targetBufferSize: fetchState.targetBufferSize,
673
+ maxOnBlockBufferSize: fetchState.maxOnBlockBufferSize,
676
674
  optimizedPartitions,
677
675
  latestOnBlockBlockNumber,
678
676
  indexingAddresses,
@@ -756,7 +754,6 @@ Returns OptimizedPartitions.t directly.
756
754
  */
757
755
  let createPartitionsFromIndexingAddresses = (
758
756
  ~registeringContractsByContract: dict<dict<indexingAddress>>,
759
- ~contractConfigs: dict<contractConfig>,
760
757
  ~dynamicContracts: Utils.Set.t<string>,
761
758
  ~normalSelection: selection,
762
759
  ~maxAddrInPartition: int,
@@ -778,9 +775,6 @@ OptimizedPartitions.t => {
778
775
  let addresses =
779
776
  registeringContracts->Dict.keysToArray->(Utils.magic: array<string> => array<Address.t>)
780
777
 
781
- // Can unsafely get it, because we already filtered out the contracts
782
- // that don't have any events to fetch
783
- let contractConfig = contractConfigs->Dict.getUnsafe(contractName)
784
778
  let isDynamic = dynamicContracts->Utils.Set.has(contractName)
785
779
  let partitions = isDynamic ? dynamicPartitions : nonDynamicPartitions
786
780
 
@@ -802,30 +796,23 @@ OptimizedPartitions.t => {
802
796
  let maybeNextStartBlockKey =
803
797
  ascKeys->Array.getUnsafe(idx + 1)->(Utils.magic: string => option<string>)
804
798
 
805
- // For this case we can't filter out events earlier than contract registration
806
- // on the client side, so we need to keep the old logic of creating
807
- // a partition for every block range, so there are no irrelevant events
808
- let shouldAllocateNewPartition = if contractConfig.filterByAddresses {
809
- true
810
- } else {
811
- switch maybeNextStartBlockKey {
812
- | None => true
813
- | Some(nextStartBlockKey) => {
814
- let nextStartBlock = nextStartBlockKey->Int.fromString->Option.getUnsafe
815
- let shouldJoinCurrentStartBlock =
816
- nextStartBlock - startBlockRef.contents < OptimizedPartitions.tooFarBlockRange
817
-
818
- // If dynamic contract registration are close to eachother
819
- // and it's possible to use dc.effectiveStartBlock to filter out events on client side
820
- // then we can optimize the number of partitions,
821
- // by putting dcs with different startBlocks in the same partition
822
- if shouldJoinCurrentStartBlock {
823
- addressesRef :=
824
- addressesRef.contents->Array.concat(byStartBlock->Dict.getUnsafe(nextStartBlockKey))
825
- false
826
- } else {
827
- true
828
- }
799
+ let shouldAllocateNewPartition = switch maybeNextStartBlockKey {
800
+ | None => true
801
+ | Some(nextStartBlockKey) => {
802
+ let nextStartBlock = nextStartBlockKey->Int.fromString->Option.getUnsafe
803
+ let shouldJoinCurrentStartBlock =
804
+ nextStartBlock - startBlockRef.contents < OptimizedPartitions.tooFarBlockRange
805
+
806
+ // Addresses with different start blocks within range share a partition;
807
+ // events before each address's effectiveStartBlock are dropped on the
808
+ // client side (EventRouter for the srcAddress, the event's
809
+ // clientAddressFilter for address-valued params).
810
+ if shouldJoinCurrentStartBlock {
811
+ addressesRef :=
812
+ addressesRef.contents->Array.concat(byStartBlock->Dict.getUnsafe(nextStartBlockKey))
813
+ false
814
+ } else {
815
+ true
829
816
  }
830
817
  }
831
818
  }
@@ -907,15 +894,10 @@ OptimizedPartitions.t => {
907
894
  }
908
895
  }
909
896
 
910
- let nextContractName =
911
- nextP.addressesByContractName->Dict.keysToArray->Utils.Array.firstUnsafe
912
- let hasFilterByAddresses = (
913
- contractConfigs->Dict.getUnsafe(nextContractName)
914
- ).filterByAddresses
915
897
  let isTooFar = currentPBlock + OptimizedPartitions.tooFarBlockRange < nextPBlock
916
898
 
917
- if isTooFar || hasFilterByAddresses {
918
- // Too far or address-filtered: mergeBlock on current, merge addresses into next
899
+ if isTooFar {
900
+ // Too far: mergeBlock on current, merge addresses into next
919
901
  mergedNonDynamic
920
902
  ->Array.push({
921
903
  ...currentP,
@@ -927,7 +909,7 @@ OptimizedPartitions.t => {
927
909
  addressesByContractName: mergedAddresses,
928
910
  }
929
911
  } else {
930
- // Close and not address-filtered: push next's addresses into current
912
+ // Close: push next's addresses into current
931
913
  currentPRef := {
932
914
  ...currentP,
933
915
  addressesByContractName: mergedAddresses,
@@ -968,7 +950,6 @@ let registerDynamicContracts = (
968
950
  let indexingAddresses = fetchState.indexingAddresses
969
951
  let registeringContractsByContract: dict<dict<indexingAddress>> = Dict.make()
970
952
  let earliestRegisteringEventBlockNumber = ref(%raw(`Infinity`))
971
- let hasDCWithFilterByAddresses = ref(false)
972
953
  // Addresses registered for contracts without matching events. These are not
973
954
  // added to partitions, but they are tracked on fetchState.indexingAddresses
974
955
  // so that later conflicting registrations are detected, and are persisted
@@ -991,7 +972,7 @@ let registerDynamicContracts = (
991
972
  let shouldRemove = ref(false)
992
973
 
993
974
  switch fetchState.contractConfigs->Utils.Dict.dangerouslyGetNonOption(dc.contractName) {
994
- | Some({filterByAddresses, startBlock: contractStartBlock}) =>
975
+ | Some({startBlock: contractStartBlock}) =>
995
976
  let dcWithStartBlock: indexingAddress = {
996
977
  address: dc.address,
997
978
  contractName: dc.contractName,
@@ -1038,9 +1019,7 @@ let registerDynamicContracts = (
1038
1019
  // FIXME: This unsafely relies on the asc order of the items
1039
1020
  // which is 99% true, but there were cases when the source ordering was wrong
1040
1021
  false
1041
- | None =>
1042
- hasDCWithFilterByAddresses := hasDCWithFilterByAddresses.contents || filterByAddresses
1043
- true
1022
+ | None => true
1044
1023
  }
1045
1024
  if shouldUpdate {
1046
1025
  earliestRegisteringEventBlockNumber :=
@@ -1218,7 +1197,6 @@ let registerDynamicContracts = (
1218
1197
 
1219
1198
  let optimizedPartitions = createPartitionsFromIndexingAddresses(
1220
1199
  ~registeringContractsByContract,
1221
- ~contractConfigs=fetchState.contractConfigs,
1222
1200
  ~dynamicContracts=dynamicContractsRef.contents,
1223
1201
  ~normalSelection=fetchState.normalSelection,
1224
1202
  ~maxAddrInPartition=fetchState.optimizedPartitions.maxAddrInPartition,
@@ -1245,6 +1223,20 @@ let handleQueryResult = (
1245
1223
  ~latestFetchedBlock: blockNumberAndTimestamp,
1246
1224
  ~newItems,
1247
1225
  ): t => {
1226
+ // Drop events an address-param filter rejects. A merged partition may
1227
+ // over-fetch a wildcard event whose indexed address param references an
1228
+ // address registered after the log's block; `clientAddressFilter` is the
1229
+ // param-level analogue of EventRouter's srcAddress effectiveStartBlock check.
1230
+ let newItems = newItems->Array.filter(item =>
1231
+ switch item {
1232
+ | Internal.Event({eventConfig, payload, blockNumber}) =>
1233
+ switch eventConfig.clientAddressFilter {
1234
+ | Some(filter) => filter(payload, blockNumber, fetchState.indexingAddresses)
1235
+ | None => true
1236
+ }
1237
+ | _ => true
1238
+ }
1239
+ )
1248
1240
  fetchState->updateInternal(
1249
1241
  ~optimizedPartitions=fetchState.optimizedPartitions->OptimizedPartitions.handleQueryResponse(
1250
1242
  ~query,
@@ -1328,26 +1320,30 @@ let pushQueriesForRange = (
1328
1320
  | None => maxQueryBlockNumber
1329
1321
  }
1330
1322
  let chunkSize = Js.Math.ceil_int(chunkRange->Int.toFloat *. 1.8)
1331
- if rangeFromBlock + 2 * chunkSize - 1 <= maxBlock {
1332
- // Create 2 chunks of ceil(1.8 * chunkRange) each
1333
- queries->Array.push({
1334
- partitionId,
1335
- fromBlock: rangeFromBlock,
1336
- toBlock: Some(rangeFromBlock + chunkSize - 1),
1337
- isChunk: true,
1338
- selection,
1339
- addressesByContractName,
1340
- indexingAddresses,
1341
- })
1342
- queries->Array.push({
1343
- partitionId,
1344
- fromBlock: rangeFromBlock + chunkSize,
1345
- toBlock: Some(rangeFromBlock + 2 * chunkSize - 1),
1346
- isChunk: true,
1347
- selection,
1348
- addressesByContractName,
1349
- indexingAddresses,
1350
- })
1323
+ // Probe with two smaller chunks first so their responses come back
1324
+ // quickly and refresh the chunking heuristic, then three full-size chunks.
1325
+ let probeSize = Js.Math.ceil_int(chunkRange->Int.toFloat *. 0.9)
1326
+ let getChunkSize = chunkIdx => chunkIdx < 2 ? probeSize : chunkSize
1327
+ if rangeFromBlock + getChunkSize(0) + getChunkSize(1) - 1 <= maxBlock {
1328
+ let chunkFromBlock = ref(rangeFromBlock)
1329
+ let chunkIdx = ref(0)
1330
+ while (
1331
+ chunkIdx.contents < 5 &&
1332
+ chunkFromBlock.contents + getChunkSize(chunkIdx.contents) - 1 <= maxBlock
1333
+ ) {
1334
+ let chunkToBlock = chunkFromBlock.contents + getChunkSize(chunkIdx.contents) - 1
1335
+ queries->Array.push({
1336
+ partitionId,
1337
+ fromBlock: chunkFromBlock.contents,
1338
+ toBlock: Some(chunkToBlock),
1339
+ isChunk: true,
1340
+ selection,
1341
+ addressesByContractName,
1342
+ indexingAddresses,
1343
+ })
1344
+ chunkFromBlock := chunkToBlock + 1
1345
+ chunkIdx := chunkIdx.contents + 1
1346
+ }
1351
1347
  } else {
1352
1348
  // Not enough room for 2 chunks, fall back to a single query
1353
1349
  queries->Array.push({
@@ -1365,23 +1361,30 @@ let pushQueriesForRange = (
1365
1361
  }
1366
1362
  }
1367
1363
 
1364
+ // Most parallel in-flight chunk queries a single partition may have at once.
1365
+ let maxPendingChunksPerPartition = 10
1366
+
1368
1367
  let getNextQuery = (
1369
1368
  {
1370
1369
  buffer,
1371
1370
  optimizedPartitions,
1372
- targetBufferSize,
1373
1371
  indexingAddresses,
1374
1372
  blockLag,
1375
1373
  latestOnBlockBlockNumber,
1376
1374
  knownHeight,
1377
1375
  } as fetchState: t,
1378
1376
  ~concurrencyLimit,
1377
+ ~bufferLimit,
1379
1378
  ) => {
1380
1379
  let headBlockNumber = knownHeight - blockLag
1381
1380
  if headBlockNumber <= 0 {
1382
1381
  WaitingForNewBlock
1383
1382
  } else if concurrencyLimit === 0 {
1384
1383
  ReachedMaxConcurrency
1384
+ } else if bufferLimit <= 0 {
1385
+ // No room left in the shared buffer pool for this chain; wait for processing
1386
+ // to drain before fetching more.
1387
+ NothingToQuery
1385
1388
  } else {
1386
1389
  let isOnBlockBehindTheHead = latestOnBlockBlockNumber < headBlockNumber
1387
1390
  let shouldWaitForNewBlock = ref(
@@ -1392,13 +1395,12 @@ let getNextQuery = (
1392
1395
  !isOnBlockBehindTheHead,
1393
1396
  )
1394
1397
 
1395
- // We want to limit the buffer size to targetBufferSize (usually 3 * batchSize)
1396
- // To make sure the processing always has some buffer
1397
- // and not increase the memory usage too much
1398
- // If a partition fetched further
1399
- // it should be skipped until the buffer is consumed
1398
+ // Limit how far ahead we fetch to bufferLimit items (this chain's share of
1399
+ // the indexer-wide buffer pool) so processing always has buffer without
1400
+ // ballooning memory. A partition that fetched further is skipped until the
1401
+ // buffer drains.
1400
1402
  let maxQueryBlockNumber = {
1401
- switch buffer->Array.get(targetBufferSize - 1) {
1403
+ switch buffer->Array.get(bufferLimit - 1) {
1402
1404
  | Some(item) =>
1403
1405
  // Just in case check that we don't query beyond the current block
1404
1406
  Pervasives.min(item->Internal.getItemBlockNumber, knownHeight)
@@ -1415,8 +1417,9 @@ let getNextQuery = (
1415
1417
  let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
1416
1418
  let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
1417
1419
 
1420
+ let pendingCount = p.mutPendingQueries->Array.length
1418
1421
  let isBehindTheHead = p.latestFetchedBlock.blockNumber < headBlockNumber
1419
- let hasPendingQueries = p.mutPendingQueries->Utils.Array.notEmpty
1422
+ let hasPendingQueries = pendingCount > 0
1420
1423
 
1421
1424
  if hasPendingQueries || isBehindTheHead {
1422
1425
  // Even if there are some partitions waiting for the new block
@@ -1427,6 +1430,8 @@ let getNextQuery = (
1427
1430
  shouldWaitForNewBlock := false
1428
1431
  }
1429
1432
 
1433
+ let partitionQueriesStart = queries->Array.length
1434
+
1430
1435
  // Compute queryEndBlock for this partition
1431
1436
  let queryEndBlock = Utils.Math.minOptInt(fetchState.endBlock, p.mergeBlock)
1432
1437
  let queryEndBlock = switch blockLag {
@@ -1494,6 +1499,22 @@ let getNextQuery = (
1494
1499
  )
1495
1500
  }
1496
1501
 
1502
+ // Cap parallel in-flight chunks per partition so a single partition can't
1503
+ // drain the whole concurrency budget (especially the high realtime one).
1504
+ // Keep the earliest new chunks; the furthest-ahead ones wait for the next
1505
+ // round once these resolve.
1506
+ let maxNewChunks = Pervasives.max(0, maxPendingChunksPerPartition - pendingCount)
1507
+ let generatedCount = queries->Array.length - partitionQueriesStart
1508
+ if generatedCount > maxNewChunks {
1509
+ queries
1510
+ ->Array.splice(
1511
+ ~start=partitionQueriesStart + maxNewChunks,
1512
+ ~remove=generatedCount - maxNewChunks,
1513
+ ~insert=[],
1514
+ )
1515
+ ->ignore
1516
+ }
1517
+
1497
1518
  idxRef := idxRef.contents + 1
1498
1519
  }
1499
1520
 
@@ -1556,7 +1577,7 @@ let make = (
1556
1577
  ~addresses: array<Internal.indexingAddress>,
1557
1578
  ~maxAddrInPartition,
1558
1579
  ~chainId,
1559
- ~targetBufferSize,
1580
+ ~maxOnBlockBufferSize,
1560
1581
  ~knownHeight,
1561
1582
  ~progressBlockNumber=startBlock - 1,
1562
1583
  ~onBlockConfigs=[],
@@ -1572,15 +1593,14 @@ let make = (
1572
1593
  let normalEventConfigs = []
1573
1594
  let contractNamesWithNormalEvents = Utils.Set.make()
1574
1595
  let indexingAddresses = Dict.make()
1575
- let contractConfigs = Dict.make()
1596
+ let contractConfigs: dict<contractConfig> = Dict.make()
1576
1597
 
1577
1598
  eventConfigs->Array.forEach(ec => {
1578
1599
  switch contractConfigs->Utils.Dict.dangerouslyGetNonOption(ec.contractName) {
1579
- | Some({filterByAddresses, startBlock}) =>
1600
+ | Some({startBlock}) =>
1580
1601
  contractConfigs->Dict.set(
1581
1602
  ec.contractName,
1582
1603
  {
1583
- filterByAddresses: filterByAddresses || ec.filterByAddresses,
1584
1604
  startBlock: switch (startBlock, ec.startBlock) {
1585
1605
  | (Some(a), Some(b)) => Some(Pervasives.min(a, b))
1586
1606
  | (Some(_) as s, None) | (None, Some(_) as s) => s
@@ -1592,7 +1612,6 @@ let make = (
1592
1612
  contractConfigs->Dict.set(
1593
1613
  ec.contractName,
1594
1614
  {
1595
- filterByAddresses: ec.filterByAddresses,
1596
1615
  startBlock: ec.startBlock,
1597
1616
  },
1598
1617
  )
@@ -1672,7 +1691,6 @@ let make = (
1672
1691
 
1673
1692
  let optimizedPartitions = createPartitionsFromIndexingAddresses(
1674
1693
  ~registeringContractsByContract,
1675
- ~contractConfigs,
1676
1694
  ~dynamicContracts,
1677
1695
  ~normalSelection,
1678
1696
  ~maxAddrInPartition,
@@ -1708,7 +1726,7 @@ let make = (
1708
1726
  ~indexerStartBlock=startBlock,
1709
1727
  ~fromBlock=progressBlockNumber,
1710
1728
  ~maxBlockNumber,
1711
- ~targetBufferSize,
1729
+ ~maxOnBlockBufferSize,
1712
1730
  )
1713
1731
  } else {
1714
1732
  progressBlockNumber
@@ -1725,7 +1743,7 @@ let make = (
1725
1743
  indexingAddresses,
1726
1744
  blockLag,
1727
1745
  onBlockConfigs,
1728
- targetBufferSize,
1746
+ maxOnBlockBufferSize,
1729
1747
  knownHeight,
1730
1748
  buffer,
1731
1749
  firstEventBlock,
@@ -1749,6 +1767,25 @@ let make = (
1749
1767
 
1750
1768
  let bufferSize = ({buffer}: t) => buffer->Array.length
1751
1769
 
1770
+ // Number of buffered items at or below the ready frontier (processable now,
1771
+ // i.e. not stuck behind a gap from a lagging partition or out-of-order chunk).
1772
+ // The buffer is kept sorted, so binary-search the frontier in O(log n).
1773
+ let bufferReadyCount = (fetchState: t) => {
1774
+ let frontier = fetchState->bufferBlockNumber
1775
+ let buffer = fetchState.buffer
1776
+ let lo = ref(0)
1777
+ let hi = ref(buffer->Array.length)
1778
+ while lo.contents < hi.contents {
1779
+ let mid = (lo.contents + hi.contents) / 2
1780
+ if buffer->Array.getUnsafe(mid)->Internal.getItemBlockNumber <= frontier {
1781
+ lo := mid + 1
1782
+ } else {
1783
+ hi := mid
1784
+ }
1785
+ }
1786
+ lo.contents
1787
+ }
1788
+
1752
1789
  let rollbackPendingQueries = (mutPendingQueries: array<pendingQuery>, ~targetBlockNumber) => {
1753
1790
  // - Remove queries where fromBlock > target
1754
1791
  // - Cap fetchedBlock at target where fetchedBlock > target
@@ -1875,7 +1912,6 @@ let rollback = (fetchState: t, ~targetBlockNumber) => {
1875
1912
  // Step 3: Recreate partitions from deleted partition addresses
1876
1913
  let optimizedPartitions = createPartitionsFromIndexingAddresses(
1877
1914
  ~registeringContractsByContract,
1878
- ~contractConfigs=fetchState.contractConfigs,
1879
1915
  ~dynamicContracts=fetchState.optimizedPartitions.dynamicContracts,
1880
1916
  ~normalSelection=fetchState.normalSelection,
1881
1917
  ~maxAddrInPartition=fetchState.optimizedPartitions.maxAddrInPartition,
@@ -1948,6 +1984,18 @@ let isActivelyIndexing = ({endBlock} as fetchState: t) => {
1948
1984
  }
1949
1985
  }
1950
1986
 
1987
+ // True once the fetch frontier has reached the (lagged) head or endBlock,
1988
+ // regardless of whether the buffer has been consumed yet. Unlike
1989
+ // isReadyToEnterReorgThreshold, fetched-but-unprocessed items still count.
1990
+ let isFetchingAtHead = ({endBlock, blockLag, knownHeight} as fetchState: t) => {
1991
+ let bufferBlockNumber = fetchState->bufferBlockNumber
1992
+ knownHeight !== 0 &&
1993
+ switch endBlock {
1994
+ | Some(endBlock) if bufferBlockNumber >= endBlock => true
1995
+ | _ => bufferBlockNumber >= knownHeight - blockLag
1996
+ }
1997
+ }
1998
+
1951
1999
  let isReadyToEnterReorgThreshold = ({endBlock, blockLag, buffer, knownHeight} as fetchState: t) => {
1952
2000
  let bufferBlockNumber = fetchState->bufferBlockNumber
1953
2001
  knownHeight !== 0 &&
@@ -1958,7 +2006,26 @@ let isReadyToEnterReorgThreshold = ({endBlock, blockLag, buffer, knownHeight} as
1958
2006
  buffer->Utils.Array.isEmpty
1959
2007
  }
1960
2008
 
1961
- let sortForUnorderedBatch = {
2009
+ // Lower progress percentage = further behind = higher priority. Shared by the
2010
+ // batch ordering and the cross-chain fetch priority.
2011
+ let getProgressPercentage = (fetchState: t) => {
2012
+ switch fetchState.firstEventBlock {
2013
+ | None => 0.
2014
+ | Some(firstEventBlock) =>
2015
+ let totalRange = fetchState.knownHeight - firstEventBlock
2016
+ if totalRange <= 0 {
2017
+ 0.
2018
+ } else {
2019
+ let progress = switch fetchState.buffer->Array.get(0) {
2020
+ | Some(item) => item->Internal.getItemBlockNumber - firstEventBlock
2021
+ | None => fetchState->bufferBlockNumber - firstEventBlock
2022
+ }
2023
+ progress->Int.toFloat /. totalRange->Int.toFloat
2024
+ }
2025
+ }
2026
+ }
2027
+
2028
+ let sortForBatch = {
1962
2029
  let hasFullBatch = ({buffer} as fetchState: t, ~batchSizeTarget) => {
1963
2030
  switch buffer->Array.get(batchSizeTarget - 1) {
1964
2031
  | Some(item) => item->Internal.getItemBlockNumber <= fetchState->bufferBlockNumber
@@ -1966,24 +2033,6 @@ let sortForUnorderedBatch = {
1966
2033
  }
1967
2034
  }
1968
2035
 
1969
- // Lower progress percentage = further behind = higher priority
1970
- let getProgressPercentage = (fetchState: t) => {
1971
- switch fetchState.firstEventBlock {
1972
- | None => 0.
1973
- | Some(firstEventBlock) =>
1974
- let totalRange = fetchState.knownHeight - firstEventBlock
1975
- if totalRange <= 0 {
1976
- 0.
1977
- } else {
1978
- let progress = switch fetchState.buffer->Array.get(0) {
1979
- | Some(item) => item->Internal.getItemBlockNumber - firstEventBlock
1980
- | None => fetchState->bufferBlockNumber - firstEventBlock
1981
- }
1982
- progress->Int.toFloat /. totalRange->Int.toFloat
1983
- }
1984
- }
1985
- }
1986
-
1987
2036
  (fetchStates: array<t>, ~batchSizeTarget: int) => {
1988
2037
  let copied = fetchStates->Array.copy
1989
2038
  copied->Array.sort((a: t, b: t) => {