envio 3.3.0-alpha.7 → 3.3.0-alpha.9

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 (95) hide show
  1. package/index.d.ts +23 -5
  2. package/package.json +6 -6
  3. package/src/Api.res +1 -1
  4. package/src/Api.res.mjs +1 -1
  5. package/src/ChainFetching.res +21 -10
  6. package/src/ChainFetching.res.mjs +11 -10
  7. package/src/ChainState.res +401 -171
  8. package/src/ChainState.res.mjs +270 -104
  9. package/src/ChainState.resi +19 -7
  10. package/src/Config.res +11 -27
  11. package/src/Config.res.mjs +8 -7
  12. package/src/Core.res +7 -0
  13. package/src/CrossChainState.res +82 -82
  14. package/src/CrossChainState.res.mjs +52 -59
  15. package/src/CrossChainState.resi +1 -1
  16. package/src/Ecosystem.res +3 -3
  17. package/src/Ecosystem.res.mjs +3 -3
  18. package/src/Envio.res +14 -9
  19. package/src/EnvioGlobal.res +53 -0
  20. package/src/EnvioGlobal.res.mjs +31 -0
  21. package/src/EventConfigBuilder.res +117 -69
  22. package/src/EventConfigBuilder.res.mjs +71 -34
  23. package/src/EventProcessing.res +19 -15
  24. package/src/EventProcessing.res.mjs +13 -12
  25. package/src/FetchState.res +410 -185
  26. package/src/FetchState.res.mjs +268 -253
  27. package/src/HandlerLoader.res +8 -113
  28. package/src/HandlerLoader.res.mjs +2 -88
  29. package/src/HandlerRegister.res +518 -144
  30. package/src/HandlerRegister.res.mjs +285 -133
  31. package/src/HandlerRegister.resi +24 -8
  32. package/src/IndexerState.res +6 -3
  33. package/src/IndexerState.res.mjs +3 -3
  34. package/src/IndexerState.resi +1 -1
  35. package/src/IndexingAddresses.res +10 -7
  36. package/src/IndexingAddresses.res.mjs +8 -7
  37. package/src/IndexingAddresses.resi +3 -1
  38. package/src/Internal.res +104 -39
  39. package/src/Internal.res.mjs +11 -1
  40. package/src/LogSelection.res +102 -165
  41. package/src/LogSelection.res.mjs +101 -116
  42. package/src/Main.res +49 -164
  43. package/src/Main.res.mjs +39 -104
  44. package/src/Metrics.res +43 -2
  45. package/src/Metrics.res.mjs +39 -3
  46. package/src/Prometheus.res +0 -35
  47. package/src/Prometheus.res.mjs +33 -68
  48. package/src/RawEvent.res +7 -2
  49. package/src/RawEvent.res.mjs +4 -4
  50. package/src/RollbackCommit.res +4 -1
  51. package/src/RollbackCommit.res.mjs +3 -2
  52. package/src/SimulateItems.res +20 -7
  53. package/src/SimulateItems.res.mjs +7 -11
  54. package/src/TestIndexer.res +1 -1
  55. package/src/TestIndexer.res.mjs +1 -1
  56. package/src/sources/BlockStore.res +46 -0
  57. package/src/sources/BlockStore.res.mjs +24 -0
  58. package/src/sources/EventRouter.res +19 -12
  59. package/src/sources/EventRouter.res.mjs +7 -5
  60. package/src/sources/Evm.res +58 -5
  61. package/src/sources/Evm.res.mjs +44 -5
  62. package/src/sources/EvmChain.res +14 -18
  63. package/src/sources/EvmChain.res.mjs +12 -13
  64. package/src/sources/FieldMask.res +39 -0
  65. package/src/sources/FieldMask.res.mjs +42 -0
  66. package/src/sources/Fuel.res +8 -3
  67. package/src/sources/Fuel.res.mjs +5 -4
  68. package/src/sources/HyperFuelSource.res +31 -40
  69. package/src/sources/HyperFuelSource.res.mjs +52 -44
  70. package/src/sources/HyperSync.res +31 -9
  71. package/src/sources/HyperSync.res.mjs +47 -31
  72. package/src/sources/HyperSync.resi +10 -3
  73. package/src/sources/HyperSyncClient.res +15 -4
  74. package/src/sources/HyperSyncHeightStream.res +1 -8
  75. package/src/sources/HyperSyncHeightStream.res.mjs +0 -2
  76. package/src/sources/HyperSyncSource.res +58 -90
  77. package/src/sources/HyperSyncSource.res.mjs +57 -66
  78. package/src/sources/RpcSource.res +118 -120
  79. package/src/sources/RpcSource.res.mjs +92 -74
  80. package/src/sources/RpcWebSocketHeightStream.res +0 -5
  81. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -2
  82. package/src/sources/SimulateSource.res +5 -3
  83. package/src/sources/SimulateSource.res.mjs +12 -4
  84. package/src/sources/Source.res +19 -5
  85. package/src/sources/SourceManager.res +60 -10
  86. package/src/sources/SourceManager.res.mjs +54 -8
  87. package/src/sources/SourceManager.resi +10 -0
  88. package/src/sources/Svm.res +14 -10
  89. package/src/sources/Svm.res.mjs +23 -6
  90. package/src/sources/SvmHyperSyncClient.res +5 -6
  91. package/src/sources/SvmHyperSyncSource.res +82 -41
  92. package/src/sources/SvmHyperSyncSource.res.mjs +86 -41
  93. package/src/sources/TransactionStore.res +6 -107
  94. package/src/sources/TransactionStore.res.mjs +5 -86
  95. package/svm.schema.json +19 -0
@@ -7,15 +7,19 @@ type blockNumberAndTimestamp = {
7
7
 
8
8
  type blockNumberAndLogIndex = {blockNumber: int, logIndex: int}
9
9
 
10
- type selection = {eventConfigs: array<Internal.eventConfig>, dependsOnAddresses: bool}
10
+ type selection = {
11
+ onEventRegistrations: array<Internal.onEventRegistration>,
12
+ dependsOnAddresses: bool,
13
+ }
11
14
 
12
15
  type pendingQuery = {
13
16
  fromBlock: int,
14
17
  toBlock: option<int>,
15
18
  isChunk: bool,
16
- // Estimated items this in-flight query will add, carried from the query so the
17
- // shared buffer budget can account for what's already being fetched.
18
- estResponseSize: float,
19
+ // Items this in-flight query is targeting (server maxNumLogs-style cap), carried
20
+ // from the query so the shared buffer budget can account for what's already
21
+ // being fetched.
22
+ itemsTarget: int,
19
23
  // Stores latestFetchedBlock when query completes. Only needed to persist
20
24
  // timestamp while earlier queries are still pending before updating
21
25
  // the partition's latestFetchedBlock.
@@ -59,15 +63,12 @@ type query = {
59
63
  fromBlock: int,
60
64
  toBlock: option<int>,
61
65
  isChunk: bool,
62
- // Estimated number of items the query will return, from the partition's
63
- // density and the query's block range. Used to admit queries against the
64
- // shared buffer budget.
65
- estResponseSize: float,
66
- // Owning chain and the chain progress % at the query's fromBlock. Set by the
67
- // cross-chain scheduler so candidate queries can be pooled and ordered
68
- // (furthest-behind first) without allocating a side tuple per query.
69
- mutable chainId: int,
70
- mutable progress: float,
66
+ // Items this query targets: the server-side maxNumLogs-style cap, and the
67
+ // unit the chain's per-tick budget is reserved/consumed in. For a partition
68
+ // with known density this is density × the query's block range; for a
69
+ // partition with no signal yet it's whatever budget share the query was
70
+ // sized against.
71
+ itemsTarget: int,
71
72
  selection: selection,
72
73
  addressesByContractName: dict<array<Address.t>>,
73
74
  }
@@ -90,42 +91,20 @@ let deriveContractNameByAddress: dict<array<Address.t>> => dict<
90
91
  result
91
92
  })
92
93
 
93
- // Bounds for the no-history default estimate below. Also used by SourceManager
94
- // as the floor for its own maxNumLogs-style safety net.
95
- let minEstResponseSize = 2_000.
96
- let maxEstResponseSize = 10_000.
97
-
98
- // Default estimate for a query whose partition hasn't responded yet, so the
99
- // shared budget still accounts for unknown queries instead of treating them as
100
- // free. Scaled down as partitionsCount grows: assuming every partition's first
101
- // query is "big" starves the rest of that tick's admission when many
102
- // partitions share the same buffer, so the per-partition default shrinks
103
- // accordingly — clamped to [minEstResponseSize, maxEstResponseSize] either way.
104
- let calculateDefaultEstResponseSize = (~partitionsCount) =>
94
+ // itemsTarget for a query over [fromBlock, toBlock] at the given event density
95
+ // (items/block). toBlock None is the open-ended tail, capped at
96
+ // chainTargetBlock the soft per-tick horizon the owning chain wants to reach
97
+ // (see getNextQuery).
98
+ let densityItemsTarget = (~density, ~fromBlock, ~toBlock, ~chainTargetBlock) => {
99
+ // Floor at 1: the reservation must equal the server-side cap SourceManager
100
+ // sends, and a 0 cap would ask the backend for nothing.
105
101
  Pervasives.max(
106
- minEstResponseSize,
107
- Pervasives.min(maxEstResponseSize, 20_000. /. partitionsCount->Int.toFloat),
102
+ 1,
103
+ ((toBlock->Option.getOr(chainTargetBlock) - fromBlock + 1)->Int.toFloat *. density)
104
+ ->Math.ceil
105
+ ->Float.toInt,
108
106
  )
109
-
110
- // Estimated items a query will return, from the partition's event density
111
- // (items/block derived from its last response) and the query's block range.
112
- // toBlock None is the open-ended tail, capped at knownHeight. A partition
113
- // that responded with no items has density 0, so its queries cost 0 — correct,
114
- // they don't fill the buffer. Only a partition that has never responded
115
- // (prevQueryRange 0) has no signal, so it falls back to calculateDefaultEstResponseSize.
116
- let calculateEstResponseSize = (
117
- p: partition,
118
- ~fromBlock,
119
- ~toBlock,
120
- ~knownHeight,
121
- ~partitionsCount,
122
- ) =>
123
- if p.prevQueryRange > 0 {
124
- let density = p.prevRangeSize->Int.toFloat /. p.prevQueryRange->Int.toFloat
125
- (toBlock->Option.getOr(knownHeight) - fromBlock + 1)->Int.toFloat *. density
126
- } else {
127
- calculateDefaultEstResponseSize(~partitionsCount)
128
- }
107
+ }
129
108
 
130
109
  // Calculate the chunk range from history using min-of-last-3-ranges heuristic
131
110
  let getMinHistoryRange = (p: partition) => {
@@ -135,6 +114,15 @@ let getMinHistoryRange = (p: partition) => {
135
114
  }
136
115
  }
137
116
 
117
+ // Density (items/block) from the last response, trusted only after two
118
+ // responses — a single sample is too noisy to size the next query by.
119
+ let getTrustedDensity = (p: partition) => {
120
+ switch (p.prevQueryRange, p.prevPrevQueryRange) {
121
+ | (0, _) | (_, 0) => None
122
+ | (prevQueryRange, _) => Some(p.prevRangeSize->Int.toFloat /. prevQueryRange->Int.toFloat)
123
+ }
124
+ }
125
+
138
126
  let getMinQueryRange = (partitions: array<partition>) => {
139
127
  let min = ref(0)
140
128
  for i in 0 to partitions->Array.length - 1 {
@@ -214,6 +202,12 @@ module OptimizedPartitions = {
214
202
  let newId = nextPartitionIndexRef.contents->Int.toString
215
203
  nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
216
204
  let minRange = getMinQueryRange([p1, p2])
205
+ // The merged partition indexes both parents' addresses, so its expected
206
+ // event rate is the sum of their densities. Parents without a trusted
207
+ // density contribute 0; if none has one, prevRangeSize stays 0 and the
208
+ // partition probes for a fresh signal instead of chunking.
209
+ let inheritedDensity =
210
+ p1->getTrustedDensity->Option.getOr(0.) +. p2->getTrustedDensity->Option.getOr(0.)
217
211
  {
218
212
  id: newId,
219
213
  dynamicContract: Some(contractName),
@@ -224,7 +218,7 @@ module OptimizedPartitions = {
224
218
  mutPendingQueries: [],
225
219
  prevQueryRange: minRange,
226
220
  prevPrevQueryRange: minRange,
227
- prevRangeSize: 0,
221
+ prevRangeSize: (inheritedDensity *. minRange->Int.toFloat)->Math.ceil->Float.toInt,
228
222
  latestBlockRangeUpdateBlock: 0,
229
223
  }
230
224
  }
@@ -566,7 +560,7 @@ type t = {
566
560
  // size). Event fetch depth is bounded separately, by CrossChainState's
567
561
  // cross-chain admission against the indexer-wide buffer pool.
568
562
  maxOnBlockBufferSize: int,
569
- onBlockConfigs: array<Internal.onBlockConfig>,
563
+ onBlockRegistrations: array<Internal.onBlockRegistration>,
570
564
  knownHeight: int,
571
565
  firstEventBlock: option<int>,
572
566
  }
@@ -642,7 +636,7 @@ let blockItemLogIndex = 16777216
642
636
  // are generated at once to prevent OOM.
643
637
  let appendOnBlockItems = (
644
638
  ~mutItems: array<Internal.item>,
645
- ~onBlockConfigs: array<Internal.onBlockConfig>,
639
+ ~onBlockRegistrations: array<Internal.onBlockRegistration>,
646
640
  ~indexerStartBlock,
647
641
  ~fromBlock,
648
642
  ~maxBlockNumber,
@@ -662,27 +656,27 @@ let appendOnBlockItems = (
662
656
  let blockNumber = latestOnBlockBlockNumber.contents + 1
663
657
  latestOnBlockBlockNumber := blockNumber
664
658
 
665
- for configIdx in 0 to onBlockConfigs->Array.length - 1 {
666
- let onBlockConfig = onBlockConfigs->Array.getUnsafe(configIdx)
659
+ for configIdx in 0 to onBlockRegistrations->Array.length - 1 {
660
+ let onBlockRegistration = onBlockRegistrations->Array.getUnsafe(configIdx)
667
661
 
668
- let handlerStartBlock = switch onBlockConfig.startBlock {
662
+ let handlerStartBlock = switch onBlockRegistration.startBlock {
669
663
  | Some(startBlock) => startBlock
670
664
  | None => indexerStartBlock
671
665
  }
672
666
 
673
667
  if (
674
668
  blockNumber >= handlerStartBlock &&
675
- switch onBlockConfig.endBlock {
669
+ switch onBlockRegistration.endBlock {
676
670
  | Some(endBlock) => blockNumber <= endBlock
677
671
  | None => true
678
672
  } &&
679
- (blockNumber - handlerStartBlock)->Pervasives.mod(onBlockConfig.interval) === 0
673
+ (blockNumber - handlerStartBlock)->Pervasives.mod(onBlockRegistration.interval) === 0
680
674
  ) {
681
675
  mutItems->Array.push(
682
676
  Block({
683
- onBlockConfig,
677
+ onBlockRegistration,
684
678
  blockNumber,
685
- logIndex: blockItemLogIndex + onBlockConfig.index,
679
+ logIndex: blockItemLogIndex + onBlockRegistration.index,
686
680
  }),
687
681
  )
688
682
  newItemsCounter := newItemsCounter.contents + 1
@@ -706,9 +700,9 @@ let updateInternal = (
706
700
  ): t => {
707
701
  let mutItemsRef = ref(mutItems)
708
702
 
709
- let latestOnBlockBlockNumber = switch fetchState.onBlockConfigs {
703
+ let latestOnBlockBlockNumber = switch fetchState.onBlockRegistrations {
710
704
  | [] => knownHeight
711
- | onBlockConfigs => {
705
+ | onBlockRegistrations => {
712
706
  // Calculate the max block number we are going to create items for
713
707
  // Use maxOnBlockBufferSize to get the last target item in the buffer
714
708
  //
@@ -736,7 +730,7 @@ let updateInternal = (
736
730
 
737
731
  appendOnBlockItems(
738
732
  ~mutItems,
739
- ~onBlockConfigs,
733
+ ~onBlockRegistrations,
740
734
  ~indexerStartBlock=fetchState.startBlock,
741
735
  ~fromBlock=fetchState.latestOnBlockBlockNumber,
742
736
  ~maxBlockNumber,
@@ -751,7 +745,7 @@ let updateInternal = (
751
745
  contractConfigs: fetchState.contractConfigs,
752
746
  normalSelection: fetchState.normalSelection,
753
747
  chainId: fetchState.chainId,
754
- onBlockConfigs: fetchState.onBlockConfigs,
748
+ onBlockRegistrations: fetchState.onBlockRegistrations,
755
749
  maxOnBlockBufferSize: fetchState.maxOnBlockBufferSize,
756
750
  optimizedPartitions,
757
751
  latestOnBlockBlockNumber,
@@ -1017,7 +1011,7 @@ let registerDynamicContracts = (
1017
1011
  // Might contain duplicates which we should filter out
1018
1012
  items: array<Internal.item>,
1019
1013
  ) => {
1020
- if fetchState.normalSelection.eventConfigs->Utils.Array.isEmpty {
1014
+ if fetchState.normalSelection.onEventRegistrations->Utils.Array.isEmpty {
1021
1015
  // Can the normalSelection be empty?
1022
1016
  JsError.throwWithMessage(
1023
1017
  "Invalid configuration. No events to fetch for the dynamic contract registration.",
@@ -1301,8 +1295,8 @@ let handleQueryResult = (
1301
1295
  // param-level analogue of EventRouter's srcAddress effectiveStartBlock check.
1302
1296
  let newItems = newItems->Array.filter(item =>
1303
1297
  switch item {
1304
- | Internal.Event({eventConfig, payload, blockNumber}) =>
1305
- switch eventConfig.clientAddressFilter {
1298
+ | Internal.Event({onEventRegistration, payload, blockNumber}) =>
1299
+ switch onEventRegistration.clientAddressFilter {
1306
1300
  | Some(filter) =>
1307
1301
  filter(payload, blockNumber, indexingAddresses->IndexingAddresses.rawForFilter)
1308
1302
  | None => true
@@ -1340,7 +1334,7 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
1340
1334
  fromBlock: q.fromBlock,
1341
1335
  toBlock: q.toBlock,
1342
1336
  isChunk: q.isChunk,
1343
- estResponseSize: q.estResponseSize,
1337
+ itemsTarget: q.itemsTarget,
1344
1338
  fetchedBlock: None,
1345
1339
  }
1346
1340
 
@@ -1364,48 +1358,58 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
1364
1358
  // Most parallel in-flight chunk queries a single partition may have at once.
1365
1359
  let maxPendingChunksPerPartition = 10
1366
1360
 
1367
- @inline
1368
- let pushQueriesForRange = (
1361
+ // Fills a gap range (a hole left between completed/pending chunks, e.g. from an
1362
+ // out-of-order partial response) unconditionally — gaps are already-committed
1363
+ // range, not subject to this tick's water-fill budget. Priced by the
1364
+ // partition's trusted density when it has one; otherwise by the "available
1365
+ // density" — the partition's equal-divide budget spread over its remaining
1366
+ // range this tick — so a small gap reserves proportionally little instead of a
1367
+ // noisy one-sample estimate. Chunks only on a trusted POSITIVE density, same
1368
+ // rule as the water-fill. Returns the created queries' total itemsTarget.
1369
+ let pushGapFillQueries = (
1369
1370
  queries: array<query>,
1370
1371
  ~partitionId: string,
1371
1372
  ~rangeFromBlock: int,
1372
1373
  ~rangeEndBlock: option<int>,
1373
1374
  ~knownHeight: int,
1375
+ ~chainTargetBlock: int,
1374
1376
  ~maybeChunkRange: option<int>,
1375
1377
  ~maxChunks: int,
1376
1378
  ~partition: partition,
1379
+ ~partitionBudget: float,
1377
1380
  ~selection: selection,
1378
1381
  ~addressesByContractName: dict<array<Address.t>>,
1379
- ~partitionsCount: int,
1380
1382
  ) => {
1383
+ let cost = ref(0.)
1381
1384
  if rangeFromBlock <= knownHeight && maxChunks > 0 {
1382
1385
  switch rangeEndBlock {
1383
1386
  | Some(endBlock) if rangeFromBlock > endBlock => ()
1384
1387
  | _ =>
1385
- switch maybeChunkRange {
1386
- | None =>
1388
+ let trustedDensity = partition->getTrustedDensity
1389
+ let maxBlock = switch rangeEndBlock {
1390
+ | Some(eb) => eb
1391
+ | None => chainTargetBlock
1392
+ }
1393
+ let pushSingleQuery = (~density, ~isChunk) => {
1394
+ let itemsTarget = densityItemsTarget(
1395
+ ~density,
1396
+ ~fromBlock=rangeFromBlock,
1397
+ ~toBlock=rangeEndBlock,
1398
+ ~chainTargetBlock,
1399
+ )
1387
1400
  queries->Array.push({
1388
1401
  partitionId,
1389
1402
  fromBlock: rangeFromBlock,
1390
1403
  toBlock: rangeEndBlock,
1391
1404
  selection,
1392
- isChunk: false,
1393
- estResponseSize: calculateEstResponseSize(
1394
- partition,
1395
- ~fromBlock=rangeFromBlock,
1396
- ~toBlock=rangeEndBlock,
1397
- ~knownHeight,
1398
- ~partitionsCount,
1399
- ),
1400
- chainId: 0,
1401
- progress: 0.,
1405
+ isChunk,
1406
+ itemsTarget,
1402
1407
  addressesByContractName,
1403
1408
  })
1404
- | Some(chunkRange) =>
1405
- let maxBlock = switch rangeEndBlock {
1406
- | Some(eb) => eb
1407
- | None => knownHeight
1408
- }
1409
+ cost := cost.contents +. itemsTarget->Int.toFloat
1410
+ }
1411
+ switch (trustedDensity, maybeChunkRange) {
1412
+ | (Some(density), Some(chunkRange)) if density > 0. =>
1409
1413
  let chunkSize = Js.Math.ceil_int(chunkRange->Int.toFloat *. 1.8)
1410
1414
  if rangeFromBlock + chunkSize * 2 - 1 <= maxBlock {
1411
1415
  let chunkFromBlock = ref(rangeFromBlock)
@@ -1414,58 +1418,111 @@ let pushQueriesForRange = (
1414
1418
  chunkIdx.contents < maxChunks && chunkFromBlock.contents + chunkSize - 1 <= maxBlock
1415
1419
  ) {
1416
1420
  let chunkToBlock = chunkFromBlock.contents + chunkSize - 1
1421
+ let itemsTarget = densityItemsTarget(
1422
+ ~density,
1423
+ ~fromBlock=chunkFromBlock.contents,
1424
+ ~toBlock=Some(chunkToBlock),
1425
+ ~chainTargetBlock,
1426
+ )
1417
1427
  queries->Array.push({
1418
1428
  partitionId,
1419
1429
  fromBlock: chunkFromBlock.contents,
1420
1430
  toBlock: Some(chunkToBlock),
1421
1431
  isChunk: true,
1422
1432
  selection,
1423
- estResponseSize: calculateEstResponseSize(
1424
- partition,
1425
- ~fromBlock=chunkFromBlock.contents,
1426
- ~toBlock=Some(chunkToBlock),
1427
- ~knownHeight,
1428
- ~partitionsCount,
1429
- ),
1430
- chainId: 0,
1431
- progress: 0.,
1433
+ itemsTarget,
1432
1434
  addressesByContractName,
1433
1435
  })
1436
+ cost := cost.contents +. itemsTarget->Int.toFloat
1434
1437
  chunkFromBlock := chunkToBlock + 1
1435
1438
  chunkIdx := chunkIdx.contents + 1
1436
1439
  }
1437
1440
  } else {
1438
1441
  // Not enough room for 2 chunks, fall back to a single query
1439
- queries->Array.push({
1440
- partitionId,
1441
- fromBlock: rangeFromBlock,
1442
- toBlock: rangeEndBlock,
1443
- selection,
1444
- isChunk: rangeEndBlock !== None,
1445
- estResponseSize: calculateEstResponseSize(
1446
- partition,
1447
- ~fromBlock=rangeFromBlock,
1448
- ~toBlock=rangeEndBlock,
1449
- ~knownHeight,
1450
- ~partitionsCount,
1451
- ),
1452
- chainId: 0,
1453
- progress: 0.,
1454
- addressesByContractName,
1455
- })
1442
+ pushSingleQuery(~density, ~isChunk=rangeEndBlock !== None)
1456
1443
  }
1444
+ | (Some(density), _) => pushSingleQuery(~density, ~isChunk=false)
1445
+ | (None, _) =>
1446
+ let remainingRange = Pervasives.max(1, chainTargetBlock - rangeFromBlock + 1)
1447
+ pushSingleQuery(~density=partitionBudget /. remainingRange->Int.toFloat, ~isChunk=false)
1457
1448
  }
1458
1449
  }
1459
1450
  }
1451
+ cost.contents
1452
+ }
1453
+
1454
+ // The level every partition ends at when `budget` is poured across partitions
1455
+ // already holding `footprints`: the unique L with Σ max(0, L - fᵢ) = budget.
1456
+ // Partitions above L get nothing (their head start is their share); the rest
1457
+ // are topped up exactly to L, so the pour equals the budget no matter how
1458
+ // uneven the footprints are.
1459
+ let waterLevel = (~budget: float, ~footprints: array<float>) => {
1460
+ let sorted = footprints->Array.toSorted(Float.compare)
1461
+ let n = sorted->Array.length
1462
+ let prefix = ref(0.)
1463
+ let level = ref(None)
1464
+ let idx = ref(0)
1465
+ while level.contents == None && idx.contents < n {
1466
+ let i = idx.contents
1467
+ prefix := prefix.contents +. sorted->Array.getUnsafe(i)
1468
+ // The level if only the i+1 lowest footprints receive water — correct once
1469
+ // it doesn't reach the next footprint up.
1470
+ let candidate = (budget +. prefix.contents) /. (i + 1)->Int.toFloat
1471
+ if i == n - 1 || candidate <= sorted->Array.getUnsafe(i + 1) {
1472
+ level := Some(candidate)
1473
+ }
1474
+ idx := idx.contents + 1
1475
+ }
1476
+ level.contents->Option.getOr(0.)
1477
+ }
1478
+
1479
+ // Per-partition mutable state threaded through the water-fill rounds below.
1480
+ type waterFillState = {
1481
+ partitionId: string,
1482
+ p: partition,
1483
+ mutable cursor: int,
1484
+ mutable chunksUsedThisCall: int,
1485
+ // Existing mutPendingQueries count before this call — fixed for the call,
1486
+ // used with chunksUsedThisCall against maxPendingChunksPerPartition.
1487
+ pendingCount: int,
1488
+ queryEndBlock: option<int>,
1489
+ maybeChunkRange: option<int>,
1490
+ // This partition's own slot in queriesByPartitionIndex — gap-fill and every
1491
+ // water-fill round push here directly, so query order falls out of
1492
+ // idsInAscOrder for free (see getNextQuery) instead of a final sort pass.
1493
+ bucket: array<query>,
1460
1494
  }
1461
1495
 
1462
- // Candidate queries are sized against the natural ceiling (head/endBlock/mergeBlock),
1463
- // not a share of the shared bufferCrossChainState.checkAndFetch's admission loop
1464
- // is what actually bounds how much gets fetched per tick, by estResponseSize (which
1465
- // the HyperSync/SVM sources also enforce server-side via a maxNumLogs-style cap, so
1466
- // a wrong estimate truncates the response instead of overshooting the buffer).
1496
+ // Candidate queries are sized against chainTargetBlock, the soft querying
1497
+ // horizon the owning chain wants to reach this tick derived by ChainState
1498
+ // from its share of the indexer-wide buffer budget and its chain-level event
1499
+ // density. chainTargetBlock is never used as a hard query end, only to (a)
1500
+ // select which partitions are "in range" this tick and (b) size an open-ended
1501
+ // query with no other ceiling: the true hard bounds stay
1502
+ // endBlock/mergeBlock/the lagged head.
1503
+ //
1504
+ // In-range partitions share chainTargetItems (this chain's target total
1505
+ // footprint — in-flight plus new) by water-fill: each round pours exactly the
1506
+ // remaining fresh budget at the level computed by waterLevel, so a partition
1507
+ // already holding more than the level (from earlier ticks' in-flight queries)
1508
+ // gets nothing and its implicit share flows to the others, while totals stay
1509
+ // even and the pour never exceeds the budget. Rounds repeat only because
1510
+ // emits are quantized: a partition may consume less than its allotment (range
1511
+ // or chunk-cap runs out) or slightly more (min one chunk), and the leftover
1512
+ // re-pours over whoever still has range left.
1513
+ //
1514
+ // A partition with a trusted positive density (two or more responses — see
1515
+ // getMinHistoryRange) always emits at least one full-size chunk/query once
1516
+ // given an allotment, sized by its real density — may overshoot the
1517
+ // allotment by at most one chunk (the server also enforces itemsTarget via a
1518
+ // maxNumLogs-style cap, so an overshoot truncates the response rather than
1519
+ // the buffer). Any other partition (no signal, one noisy sample, or a
1520
+ // density-0 estimate) emits one open-ended probe sized exactly at its
1521
+ // allotment instead.
1467
1522
  let getNextQuery = (
1468
1523
  {optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight, endBlock}: t,
1524
+ ~chainTargetBlock: int,
1525
+ ~chainTargetItems: float,
1469
1526
  ) => {
1470
1527
  let headBlockNumber = knownHeight - blockLag
1471
1528
  if headBlockNumber <= 0 {
@@ -1480,20 +1537,17 @@ let getNextQuery = (
1480
1537
  !isOnBlockBehindTheHead,
1481
1538
  )
1482
1539
 
1483
- let queries = []
1484
-
1485
1540
  let partitionsCount = optimizedPartitions.idsInAscOrder->Array.length
1486
- let idxRef = ref(0)
1487
- while idxRef.contents < partitionsCount {
1488
- let idx = idxRef.contents
1541
+
1542
+ // Every partition is visited once here regardless of whether it gets a
1543
+ // query pushed below — waiting-for-new-block bookkeeping shouldn't depend
1544
+ // on this tick's budget.
1545
+ for idx in 0 to partitionsCount - 1 {
1489
1546
  let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
1490
1547
  let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
1491
-
1492
- let pendingCount = p.mutPendingQueries->Array.length
1493
- let isBehindTheHead = p.latestFetchedBlock.blockNumber < headBlockNumber
1494
- let hasPendingQueries = pendingCount > 0
1495
-
1496
- if hasPendingQueries || isBehindTheHead {
1548
+ if (
1549
+ p.mutPendingQueries->Array.length > 0 || p.latestFetchedBlock.blockNumber < headBlockNumber
1550
+ ) {
1497
1551
  // Even if there are some partitions waiting for the new block
1498
1552
  // We still want to wait for all partitions reaching the head
1499
1553
  // because they might update knownHeight in their response
@@ -1501,46 +1555,86 @@ let getNextQuery = (
1501
1555
  // and we don't want to poll the head for a few small partitions
1502
1556
  shouldWaitForNewBlock := false
1503
1557
  }
1558
+ }
1504
1559
 
1505
- let partitionQueriesStart = queries->Array.length
1560
+ // One bucket per partition, in idsInAscOrder order — gap-fill and
1561
+ // water-fill both push into a partition's own bucket, so flattening at
1562
+ // the end (see below) reproduces idsInAscOrder without a sort.
1563
+ let queriesByPartitionIndex: array<
1564
+ array<query>,
1565
+ > = Array.fromInitializer(~length=partitionsCount, _ => [])
1506
1566
 
1507
- // Compute queryEndBlock for this partition
1567
+ // Compute queryEndBlock for this partition
1568
+ let computeQueryEndBlock = (p: partition) => {
1508
1569
  let queryEndBlock = Utils.Math.minOptInt(endBlock, p.mergeBlock)
1509
- let queryEndBlock = switch blockLag {
1570
+ switch blockLag {
1510
1571
  | 0 => queryEndBlock
1511
1572
  | _ =>
1512
1573
  // Force head block as an endBlock when blockLag is set
1513
1574
  // because otherwise HyperSync might return bigger range
1514
1575
  Utils.Math.minOptInt(Some(headBlockNumber), queryEndBlock)
1515
1576
  }
1577
+ }
1578
+
1579
+ // Each partition's existing in-flight itemsTarget, summed once and reused
1580
+ // both for chainReserved (the call-wide fresh-budget ceiling below) and to
1581
+ // seed each partition's water-fill footprint — so a partition already
1582
+ // holding in-flight queries is counted toward its even share and doesn't
1583
+ // get topped up past the others.
1584
+ let existingReservedByPartition = Dict.make()
1585
+ let chainReserved = ref(0.)
1586
+ for idx in 0 to partitionsCount - 1 {
1587
+ let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
1588
+ let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
1589
+ let cost = ref(0.)
1590
+ for pqIdx in 0 to p.mutPendingQueries->Array.length - 1 {
1591
+ cost :=
1592
+ cost.contents +. (p.mutPendingQueries->Array.getUnsafe(pqIdx)).itemsTarget->Int.toFloat
1593
+ }
1594
+ existingReservedByPartition->Dict.set(partitionId, cost.contents)
1595
+ chainReserved := chainReserved.contents +. cost.contents
1596
+ }
1516
1597
 
1598
+ // Phase A: gap-fill. Walk each partition's pending queries once,
1599
+ // unconditionally filling any hole (e.g. from an out-of-order partial
1600
+ // chunk response) — uncapped, same as before this tick's water-fill was
1601
+ // introduced. This also determines each partition's post-gap cursor and
1602
+ // whether it's blocked on an unresolved single-shot query.
1603
+ let fillStates = []
1604
+ let gapFillCost = ref(0.)
1605
+ for idx in 0 to partitionsCount - 1 {
1606
+ let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
1607
+ let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
1608
+ let bucket = queriesByPartitionIndex->Array.getUnsafe(idx)
1609
+ let pendingCount = p.mutPendingQueries->Array.length
1610
+ let queryEndBlock = computeQueryEndBlock(p)
1517
1611
  let maybeChunkRange = getMinHistoryRange(p)
1518
1612
 
1519
- // Walk pending queries to find open ranges and create queries for each
1520
1613
  let cursor = ref(p.latestFetchedBlock.blockNumber + 1)
1521
1614
  let canContinue = ref(true)
1615
+ let chunksUsedThisCall = ref(0)
1522
1616
  let pqIdx = ref(0)
1523
- while pqIdx.contents < p.mutPendingQueries->Array.length && canContinue.contents {
1617
+ while pqIdx.contents < pendingCount && canContinue.contents {
1524
1618
  let pq = p.mutPendingQueries->Array.getUnsafe(pqIdx.contents)
1525
1619
 
1526
- // Gap before this pending query → create queries for the gap range
1527
1620
  if pq.fromBlock > cursor.contents {
1528
- pushQueriesForRange(
1529
- queries,
1621
+ let beforeLen = bucket->Array.length
1622
+ let cost = pushGapFillQueries(
1623
+ bucket,
1530
1624
  ~partitionId,
1531
1625
  ~rangeFromBlock=cursor.contents,
1532
1626
  ~rangeEndBlock=Utils.Math.minOptInt(Some(pq.fromBlock - 1), queryEndBlock),
1533
1627
  ~knownHeight,
1628
+ ~chainTargetBlock,
1534
1629
  ~maybeChunkRange,
1535
- ~maxChunks=maxPendingChunksPerPartition -
1536
- pendingCount -
1537
- (queries->Array.length -
1538
- partitionQueriesStart),
1630
+ ~maxChunks=maxPendingChunksPerPartition - pendingCount - chunksUsedThisCall.contents,
1539
1631
  ~partition=p,
1632
+ ~partitionBudget=chainTargetItems /. partitionsCount->Int.toFloat,
1540
1633
  ~selection=p.selection,
1541
1634
  ~addressesByContractName=p.addressesByContractName,
1542
- ~partitionsCount,
1543
1635
  )
1636
+ chunksUsedThisCall := chunksUsedThisCall.contents + (bucket->Array.length - beforeLen)
1637
+ gapFillCost := gapFillCost.contents +. cost
1544
1638
  }
1545
1639
  switch pq {
1546
1640
  | {isChunk: true, toBlock: Some(toBlock), fetchedBlock: Some({blockNumber})}
@@ -1552,29 +1646,168 @@ let getNextQuery = (
1552
1646
  pqIdx := pqIdx.contents + 1
1553
1647
  }
1554
1648
 
1555
- // Tail range after all pending queries
1556
1649
  if canContinue.contents {
1557
- pushQueriesForRange(
1558
- queries,
1559
- ~partitionId,
1560
- ~rangeFromBlock=cursor.contents,
1561
- ~rangeEndBlock=queryEndBlock,
1562
- ~knownHeight,
1563
- ~maybeChunkRange,
1564
- ~maxChunks=maxPendingChunksPerPartition -
1565
- pendingCount -
1566
- (queries->Array.length -
1567
- partitionQueriesStart),
1568
- ~partition=p,
1569
- ~selection=p.selection,
1570
- ~addressesByContractName=p.addressesByContractName,
1571
- ~partitionsCount,
1572
- )
1650
+ fillStates
1651
+ ->Array.push({
1652
+ partitionId,
1653
+ p,
1654
+ cursor: cursor.contents,
1655
+ chunksUsedThisCall: chunksUsedThisCall.contents,
1656
+ pendingCount,
1657
+ queryEndBlock,
1658
+ maybeChunkRange,
1659
+ bucket,
1660
+ })
1661
+ ->ignore
1573
1662
  }
1663
+ }
1574
1664
 
1575
- idxRef := idxRef.contents + 1
1665
+ // Fresh budget for this call — what's left of chainTargetItems after
1666
+ // existing in-flight queries and this tick's gap-fill. The water-fill
1667
+ // rounds below hand it out; a partition already holding a large share
1668
+ // (seeded below) gets proportionally less so totals stay even.
1669
+ let rangeItemsTarget = Pervasives.max(
1670
+ 0.,
1671
+ chainTargetItems -. chainReserved.contents -. gapFillCost.contents,
1672
+ )
1673
+
1674
+ // Each in-range partition's running footprint: existing in-flight plus any
1675
+ // gap-fill just pushed into its bucket. The water-fill levels every
1676
+ // partition up toward a shared line, so this seed is what a partition
1677
+ // starts the fill already holding.
1678
+ let reservedByPartition = Dict.make()
1679
+ fillStates->Array.forEach(fs => {
1680
+ let cost = ref(existingReservedByPartition->Dict.getUnsafe(fs.partitionId))
1681
+ for i in 0 to fs.bucket->Array.length - 1 {
1682
+ cost := cost.contents +. (fs.bucket->Array.getUnsafe(i)).itemsTarget->Int.toFloat
1683
+ }
1684
+ reservedByPartition->Dict.set(fs.partitionId, cost.contents)
1685
+ })
1686
+
1687
+ let isInRange = (fs: waterFillState) =>
1688
+ fs.cursor <= chainTargetBlock &&
1689
+ switch fs.queryEndBlock {
1690
+ | Some(eb) => fs.cursor <= eb
1691
+ | None => true
1692
+ } &&
1693
+ fs.pendingCount + fs.chunksUsedThisCall < maxPendingChunksPerPartition
1694
+
1695
+ let inRangeStates = fillStates->Array.filter(isInRange)
1696
+
1697
+ // Emits this round's queries for one partition, given its water-fill
1698
+ // allotment. Mutates fs.cursor/chunksUsedThisCall and returns the
1699
+ // itemsTarget consumed.
1700
+ //
1701
+ // Chunks require a POSITIVE trusted density: density 0 prices every chunk
1702
+ // at ~nothing, letting a partition flood its full chunk pipeline with
1703
+ // hard-bounded queries that crawl 1.8× per two responses — an open-ended
1704
+ // probe instead gets the server's full scan range in one response.
1705
+ let emitQueries = (fs: waterFillState, ~budget: float) => {
1706
+ let p = fs.p
1707
+ let maxBlock = switch fs.queryEndBlock {
1708
+ | Some(eb) => eb
1709
+ | None => chainTargetBlock
1710
+ }
1711
+ switch (fs.maybeChunkRange, p->getTrustedDensity) {
1712
+ | (Some(minHistoryRange), Some(density)) if density > 0. =>
1713
+ // Chunking active: strict chunks with a hard endBlock, uncapped real
1714
+ // density — at least one full chunk this round even if budget falls
1715
+ // short.
1716
+ let chunkSize = Js.Math.ceil_int(minHistoryRange->Int.toFloat *. 1.8)
1717
+ let chunkCost = density *. chunkSize->Int.toFloat
1718
+ let maxChunksRemaining =
1719
+ maxPendingChunksPerPartition - fs.pendingCount - fs.chunksUsedThisCall
1720
+ let affordable = Math.floor(budget /. chunkCost)->Float.toInt
1721
+ let numChunks = Pervasives.max(1, Pervasives.min(affordable, maxChunksRemaining))
1722
+ let consumed = ref(0.)
1723
+ let created = ref(0)
1724
+ let chunkFromBlock = ref(fs.cursor)
1725
+ while created.contents < numChunks && chunkFromBlock.contents <= maxBlock {
1726
+ let chunkToBlock = Pervasives.min(chunkFromBlock.contents + chunkSize - 1, maxBlock)
1727
+ let itemsTarget = Pervasives.max(
1728
+ 1,
1729
+ (density *. (chunkToBlock - chunkFromBlock.contents + 1)->Int.toFloat)
1730
+ ->Math.ceil
1731
+ ->Float.toInt,
1732
+ )
1733
+ fs.bucket->Array.push({
1734
+ partitionId: fs.partitionId,
1735
+ fromBlock: chunkFromBlock.contents,
1736
+ toBlock: Some(chunkToBlock),
1737
+ isChunk: true,
1738
+ selection: p.selection,
1739
+ itemsTarget,
1740
+ addressesByContractName: p.addressesByContractName,
1741
+ })
1742
+ consumed := consumed.contents +. itemsTarget->Int.toFloat
1743
+ chunkFromBlock := chunkToBlock + 1
1744
+ created := created.contents + 1
1745
+ }
1746
+ fs.cursor = chunkFromBlock.contents
1747
+ fs.chunksUsedThisCall = fs.chunksUsedThisCall + created.contents
1748
+ consumed.contents
1749
+ | _ =>
1750
+ let itemsTarget = Pervasives.max(1, Math.round(budget)->Float.toInt)
1751
+ fs.bucket->Array.push({
1752
+ partitionId: fs.partitionId,
1753
+ fromBlock: fs.cursor,
1754
+ toBlock: fs.queryEndBlock,
1755
+ isChunk: false,
1756
+ selection: p.selection,
1757
+ itemsTarget,
1758
+ addressesByContractName: p.addressesByContractName,
1759
+ })
1760
+ fs.cursor = maxBlock + 1
1761
+ fs.chunksUsedThisCall = fs.chunksUsedThisCall + 1
1762
+ itemsTarget->Int.toFloat
1763
+ }
1576
1764
  }
1577
1765
 
1766
+ // Water-fill. Range membership is fixed by chainTargetBlock/queryEndBlock,
1767
+ // so the in-range partitions are filtered once up front; each round pours
1768
+ // the remaining fresh budget at the waterLevel of the still-not-filled
1769
+ // partitions' footprints and keeps only those still in range for the next
1770
+ // round. Allotments sum to exactly the poured budget, so the outcome is
1771
+ // order-independent and never exceeds it — the only overshoot left is the
1772
+ // min-one-chunk quantization in emitQueries, bounded by one chunk per
1773
+ // partition per round.
1774
+ //
1775
+ // No explicit round cap: a partition survives a round only by advancing
1776
+ // chunksUsedThisCall (capped at maxPendingChunksPerPartition) or by
1777
+ // consuming fresh budget (every emit consumes at least 1), so the
1778
+ // not-filled set drains on its own and the loop also stops once the whole
1779
+ // budget is poured.
1780
+ let notFilledPartitions = ref(inRangeStates)
1781
+ let remainingBudget = ref(rangeItemsTarget)
1782
+ while notFilledPartitions.contents->Array.length > 0 && remainingBudget.contents > 0. {
1783
+ let level = waterLevel(
1784
+ ~budget=remainingBudget.contents,
1785
+ ~footprints=notFilledPartitions.contents->Array.map(fs =>
1786
+ reservedByPartition->Dict.getUnsafe(fs.partitionId)
1787
+ ),
1788
+ )
1789
+ let next = []
1790
+ notFilledPartitions.contents->Array.forEach(fs => {
1791
+ let reserved = reservedByPartition->Dict.getUnsafe(fs.partitionId)
1792
+ let budget = level -. reserved
1793
+ if budget > 0. {
1794
+ let consumed = emitQueries(fs, ~budget)
1795
+ reservedByPartition->Dict.set(fs.partitionId, reserved +. consumed)
1796
+ remainingBudget := remainingBudget.contents -. consumed
1797
+ if fs->isInRange {
1798
+ next->Array.push(fs)->ignore
1799
+ }
1800
+ }
1801
+ })
1802
+ notFilledPartitions := next
1803
+ }
1804
+
1805
+ // Each partition pushed only into its own bucket (indexed by its
1806
+ // idsInAscOrder position), so flattening reproduces idsInAscOrder order
1807
+ // directly — no sort needed even though water-fill rounds interleave
1808
+ // across partitions.
1809
+ let queries = queriesByPartitionIndex->Array.flat
1810
+
1578
1811
  if queries->Utils.Array.isEmpty {
1579
1812
  if shouldWaitForNewBlock.contents {
1580
1813
  WaitingForNewBlock
@@ -1623,7 +1856,7 @@ Instantiates a fetch state with partitions for initial addresses
1623
1856
  let make = (
1624
1857
  ~startBlock,
1625
1858
  ~endBlock,
1626
- ~eventConfigs: array<Internal.eventConfig>,
1859
+ ~onEventRegistrations: array<Internal.onEventRegistration>,
1627
1860
  ~contractConfigs: dict<IndexingAddresses.contractConfig>,
1628
1861
  ~addresses: array<Internal.indexingAddress>,
1629
1862
  ~maxAddrInPartition,
@@ -1631,7 +1864,7 @@ let make = (
1631
1864
  ~maxOnBlockBufferSize,
1632
1865
  ~knownHeight,
1633
1866
  ~progressBlockNumber=startBlock - 1,
1634
- ~onBlockConfigs=[],
1867
+ ~onBlockRegistrations=[],
1635
1868
  ~blockLag=0,
1636
1869
  ~firstEventBlock=None,
1637
1870
  ): t => {
@@ -1641,15 +1874,15 @@ let make = (
1641
1874
  }
1642
1875
 
1643
1876
  let notDependingOnAddresses = []
1644
- let normalEventConfigs = []
1877
+ let normalRegistrations = []
1645
1878
  let contractNamesWithNormalEvents = Utils.Set.make()
1646
1879
 
1647
- eventConfigs->Array.forEach(ec => {
1648
- if ec.dependsOnAddresses {
1649
- normalEventConfigs->Array.push(ec)
1650
- contractNamesWithNormalEvents->Utils.Set.add(ec.contractName)->ignore
1880
+ onEventRegistrations->Array.forEach(reg => {
1881
+ if reg.dependsOnAddresses {
1882
+ normalRegistrations->Array.push(reg)
1883
+ contractNamesWithNormalEvents->Utils.Set.add(reg.eventConfig.contractName)->ignore
1651
1884
  } else {
1652
- notDependingOnAddresses->Array.push(ec)
1885
+ notDependingOnAddresses->Array.push(reg)
1653
1886
  }
1654
1887
  })
1655
1888
 
@@ -1661,7 +1894,7 @@ let make = (
1661
1894
  latestFetchedBlock,
1662
1895
  selection: {
1663
1896
  dependsOnAddresses: false,
1664
- eventConfigs: notDependingOnAddresses,
1897
+ onEventRegistrations: notDependingOnAddresses,
1665
1898
  },
1666
1899
  addressesByContractName: Dict.make(),
1667
1900
  mergeBlock: None,
@@ -1676,7 +1909,7 @@ let make = (
1676
1909
 
1677
1910
  let normalSelection = {
1678
1911
  dependsOnAddresses: true,
1679
- eventConfigs: normalEventConfigs,
1912
+ onEventRegistrations: normalRegistrations,
1680
1913
  }
1681
1914
 
1682
1915
  let registeringContractsByContract: dict<dict<indexingAddress>> = Dict.make()
@@ -1712,12 +1945,15 @@ let make = (
1712
1945
  ~progressBlockNumber,
1713
1946
  )
1714
1947
 
1715
- if optimizedPartitions->OptimizedPartitions.count === 0 && onBlockConfigs->Utils.Array.isEmpty {
1948
+ if (
1949
+ optimizedPartitions->OptimizedPartitions.count === 0 &&
1950
+ onBlockRegistrations->Utils.Array.isEmpty
1951
+ ) {
1716
1952
  JsError.throwWithMessage(
1717
1953
  `Invalid configuration: Nothing to fetch on chain ${chainId->Int.toString}. ` ++
1718
1954
  `addresses=${addresses->Array.length->Int.toString}, ` ++
1719
- `eventConfigs=${eventConfigs->Array.length->Int.toString}, ` ++
1720
- `normalEventConfigs=${normalEventConfigs
1955
+ `onEventRegistrations=${onEventRegistrations->Array.length->Int.toString}, ` ++
1956
+ `normalRegistrations=${normalRegistrations
1721
1957
  ->Array.length
1722
1958
  ->Int.toString}. ` ++ `Make sure that you provided at least one contract address to index, or have events with Wildcard mode enabled, or have onBlock handlers.`,
1723
1959
  )
@@ -1728,14 +1964,14 @@ let make = (
1728
1964
  // fetching, so without seeding the buffer here getNextQuery would return
1729
1965
  // NothingToQuery and the indexer would get stuck.
1730
1966
  let buffer = []
1731
- let latestOnBlockBlockNumber = if knownHeight > 0 && onBlockConfigs->Utils.Array.notEmpty {
1967
+ let latestOnBlockBlockNumber = if knownHeight > 0 && onBlockRegistrations->Utils.Array.notEmpty {
1732
1968
  let maxBlockNumber = switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
1733
1969
  | None => knownHeight
1734
1970
  | Some(latestFullyFetchedBlock) => latestFullyFetchedBlock.blockNumber
1735
1971
  }
1736
1972
  appendOnBlockItems(
1737
1973
  ~mutItems=buffer,
1738
- ~onBlockConfigs,
1974
+ ~onBlockRegistrations,
1739
1975
  ~indexerStartBlock=startBlock,
1740
1976
  ~fromBlock=progressBlockNumber,
1741
1977
  ~maxBlockNumber,
@@ -1754,7 +1990,7 @@ let make = (
1754
1990
  latestOnBlockBlockNumber,
1755
1991
  normalSelection,
1756
1992
  blockLag,
1757
- onBlockConfigs,
1993
+ onBlockRegistrations,
1758
1994
  maxOnBlockBufferSize,
1759
1995
  knownHeight,
1760
1996
  buffer,
@@ -2004,17 +2240,6 @@ let getProgressPercentage = (fetchState: t) => {
2004
2240
  }
2005
2241
  }
2006
2242
 
2007
- // Progress a specific block sits at along the chain, used to order queries from
2008
- // different chains: a query starting further back (lower %) is fetched first.
2009
- let getProgressPercentageAt = (fetchState: t, ~blockNumber) => {
2010
- switch fetchState.firstEventBlock {
2011
- | None => 0.
2012
- | Some(firstEventBlock) =>
2013
- let totalRange = fetchState.knownHeight - firstEventBlock
2014
- totalRange <= 0 ? 0. : (blockNumber - firstEventBlock)->Int.toFloat /. totalRange->Int.toFloat
2015
- }
2016
- }
2017
-
2018
2243
  let sortForBatch = {
2019
2244
  let hasFullBatch = ({buffer} as fetchState: t, ~batchSizeTarget) => {
2020
2245
  switch buffer->Array.get(batchSizeTarget - 1) {