envio 3.2.1 → 3.3.0-alpha.1

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 (106) 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 +373 -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 +643 -0
  11. package/src/ChainState.res.mjs +476 -0
  12. package/src/ChainState.resi +87 -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 +294 -0
  19. package/src/CrossChainState.res.mjs +221 -0
  20. package/src/CrossChainState.resi +39 -0
  21. package/src/Ecosystem.res +58 -0
  22. package/src/Ecosystem.res.mjs +43 -1
  23. package/src/Env.res +0 -2
  24. package/src/Env.res.mjs +0 -3
  25. package/src/EventConfigBuilder.res +2 -2
  26. package/src/EventProcessing.res +60 -46
  27. package/src/EventProcessing.res.mjs +33 -32
  28. package/src/EventUtils.res +0 -4
  29. package/src/EventUtils.res.mjs +0 -4
  30. package/src/ExitOnCaughtUp.res +10 -0
  31. package/src/ExitOnCaughtUp.res.mjs +22 -0
  32. package/src/FetchState.res +205 -73
  33. package/src/FetchState.res.mjs +242 -103
  34. package/src/InMemoryStore.res +36 -451
  35. package/src/InMemoryStore.res.mjs +20 -369
  36. package/src/IndexerLoop.res +43 -0
  37. package/src/IndexerLoop.res.mjs +46 -0
  38. package/src/IndexerState.res +536 -0
  39. package/src/IndexerState.res.mjs +543 -0
  40. package/src/IndexerState.resi +128 -0
  41. package/src/Internal.res +29 -8
  42. package/src/LoadLayer.res +20 -18
  43. package/src/LoadLayer.res.mjs +14 -12
  44. package/src/LoadLayer.resi +6 -3
  45. package/src/Logging.res +11 -44
  46. package/src/Logging.res.mjs +10 -42
  47. package/src/Main.res +54 -79
  48. package/src/Main.res.mjs +44 -56
  49. package/src/PgStorage.res +8 -71
  50. package/src/PgStorage.res.mjs +3 -47
  51. package/src/Prometheus.res +1 -13
  52. package/src/Prometheus.res.mjs +84 -96
  53. package/src/PruneStaleHistory.res +45 -0
  54. package/src/PruneStaleHistory.res.mjs +46 -0
  55. package/src/RawEvent.res +73 -0
  56. package/src/RawEvent.res.mjs +51 -0
  57. package/src/Rollback.res +204 -0
  58. package/src/Rollback.res.mjs +118 -0
  59. package/src/SimulateItems.res +12 -10
  60. package/src/SimulateItems.res.mjs +1 -1
  61. package/src/UserContext.res +26 -114
  62. package/src/UserContext.res.mjs +15 -78
  63. package/src/Writing.res +242 -0
  64. package/src/Writing.res.mjs +215 -0
  65. package/src/db/InternalTable.res +14 -27
  66. package/src/sources/Evm.res +40 -1
  67. package/src/sources/Evm.res.mjs +94 -84
  68. package/src/sources/Fuel.res +40 -1
  69. package/src/sources/Fuel.res.mjs +36 -26
  70. package/src/sources/HyperFuel.res +41 -120
  71. package/src/sources/HyperFuel.res.mjs +42 -80
  72. package/src/sources/HyperFuel.resi +1 -24
  73. package/src/sources/HyperFuelClient.res +16 -297
  74. package/src/sources/HyperFuelClient.res.mjs +5 -4
  75. package/src/sources/HyperFuelSource.res +33 -8
  76. package/src/sources/HyperFuelSource.res.mjs +56 -10
  77. package/src/sources/HyperSyncSource.res +5 -3
  78. package/src/sources/HyperSyncSource.res.mjs +1 -1
  79. package/src/sources/RpcSource.res +2 -2
  80. package/src/sources/RpcSource.res.mjs +1 -1
  81. package/src/sources/SourceManager.res +14 -23
  82. package/src/sources/SourceManager.res.mjs +27 -29
  83. package/src/sources/SourceManager.resi +4 -2
  84. package/src/sources/Svm.res +18 -1
  85. package/src/sources/Svm.res.mjs +30 -22
  86. package/src/sources/SvmHyperSyncSource.res +1 -1
  87. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
  88. package/src/tui/Tui.res +38 -36
  89. package/src/tui/Tui.res.mjs +51 -51
  90. package/src/ChainFetcher.res +0 -519
  91. package/src/ChainFetcher.res.mjs +0 -314
  92. package/src/ChainManager.res +0 -358
  93. package/src/ChainManager.res.mjs +0 -291
  94. package/src/Ctx.res +0 -6
  95. package/src/Ctx.res.mjs +0 -2
  96. package/src/GlobalState.res +0 -1056
  97. package/src/GlobalState.res.mjs +0 -1086
  98. package/src/GlobalStateManager.res +0 -57
  99. package/src/GlobalStateManager.res.mjs +0 -68
  100. package/src/GlobalStateManager.resi +0 -7
  101. package/src/Ports.res +0 -5
  102. package/src/Ports.res.mjs +0 -9
  103. package/src/adapters/MarkBatchProcessedAdapter.res +0 -5
  104. package/src/adapters/MarkBatchProcessedAdapter.res.mjs +0 -14
  105. package/src/sources/EnvioApiClient.res +0 -15
  106. package/src/sources/EnvioApiClient.res.mjs +0 -24
@@ -19,6 +19,9 @@ type pendingQuery = {
19
19
  fromBlock: int,
20
20
  toBlock: option<int>,
21
21
  isChunk: bool,
22
+ // Estimated items this in-flight query will add, carried from the query so the
23
+ // shared buffer budget can account for what's already being fetched.
24
+ estResponseSize: float,
22
25
  // Stores latestFetchedBlock when query completes. Only needed to persist
23
26
  // timestamp while earlier queries are still pending before updating
24
27
  // the partition's latestFetchedBlock.
@@ -47,6 +50,10 @@ type partition = {
47
50
  // Track last 3 successful query ranges for chunking heuristic (0 means no data)
48
51
  prevQueryRange: int,
49
52
  prevPrevQueryRange: int,
53
+ // Item count of the response that set prevQueryRange. Paired with it to derive
54
+ // the partition's event density (prevRangeSize / prevQueryRange) for sizing
55
+ // queries against the buffer budget.
56
+ prevRangeSize: int,
50
57
  // Tracks the latestFetchedBlock.blockNumber of the most recent response
51
58
  // that updated prevQueryRange. Prevents degradation of the chunking heuristic
52
59
  // when parallel query responses arrive out of order.
@@ -58,11 +65,39 @@ type query = {
58
65
  fromBlock: int,
59
66
  toBlock: option<int>,
60
67
  isChunk: bool,
68
+ // Estimated number of items the query will return, from the partition's
69
+ // density and the query's block range. Used to admit queries against the
70
+ // shared buffer budget.
71
+ estResponseSize: float,
72
+ // Owning chain and the chain progress % at the query's fromBlock. Set by the
73
+ // cross-chain scheduler so candidate queries can be pooled and ordered
74
+ // (furthest-behind first) without allocating a side tuple per query.
75
+ mutable chainId: int,
76
+ mutable progress: float,
61
77
  selection: selection,
62
78
  addressesByContractName: dict<array<Address.t>>,
63
79
  indexingAddresses: dict<indexingAddress>,
64
80
  }
65
81
 
82
+ // Default estimate for a query whose partition hasn't responded yet, so the
83
+ // shared budget still accounts for unknown queries instead of treating them as
84
+ // free.
85
+ let defaultEstResponseSize = 10_000.
86
+
87
+ // Estimated items a query will return, from the partition's event density
88
+ // (items/block derived from its last response) and the query's block range.
89
+ // toBlock None is the open-ended tail, capped at maxQueryBlockNumber. A partition
90
+ // that responded with no items has density 0, so its queries cost 0 — correct,
91
+ // they don't fill the buffer. Only a partition that has never responded
92
+ // (prevQueryRange 0) has no signal, so it falls back to defaultEstResponseSize.
93
+ let calculateEstResponseSize = (p: partition, ~fromBlock, ~toBlock, ~maxQueryBlockNumber) =>
94
+ if p.prevQueryRange > 0 {
95
+ let density = p.prevRangeSize->Int.toFloat /. p.prevQueryRange->Int.toFloat
96
+ (toBlock->Option.getOr(maxQueryBlockNumber) - fromBlock + 1)->Int.toFloat *. density
97
+ } else {
98
+ defaultEstResponseSize
99
+ }
100
+
66
101
  // Calculate the chunk range from history using min-of-last-3-ranges heuristic
67
102
  let getMinHistoryRange = (p: partition) => {
68
103
  switch (p.prevQueryRange, p.prevPrevQueryRange) {
@@ -160,6 +195,7 @@ module OptimizedPartitions = {
160
195
  mutPendingQueries: [],
161
196
  prevQueryRange: minRange,
162
197
  prevPrevQueryRange: minRange,
198
+ prevRangeSize: 0,
163
199
  latestBlockRangeUpdateBlock: 0,
164
200
  }
165
201
  }
@@ -397,6 +433,7 @@ module OptimizedPartitions = {
397
433
  optimizedPartitions: t,
398
434
  ~query,
399
435
  ~knownHeight,
436
+ ~itemsCount,
400
437
  ~latestFetchedBlock: blockNumberAndTimestamp,
401
438
  ) => {
402
439
  let p = optimizedPartitions->getOrThrow(~partitionId=query.partitionId)
@@ -428,6 +465,7 @@ module OptimizedPartitions = {
428
465
  }
429
466
  let updatedPrevQueryRange = shouldUpdateBlockRange ? blockRange : p.prevQueryRange
430
467
  let updatedPrevPrevQueryRange = shouldUpdateBlockRange ? p.prevQueryRange : p.prevPrevQueryRange
468
+ let updatedPrevRangeSize = shouldUpdateBlockRange ? itemsCount : p.prevRangeSize
431
469
 
432
470
  // Process fetched queries from front of queue for main partition
433
471
  let updatedLatestFetchedBlock = consumeFetchedQueries(
@@ -449,6 +487,7 @@ module OptimizedPartitions = {
449
487
  latestFetchedBlock: updatedLatestFetchedBlock,
450
488
  prevQueryRange: updatedPrevQueryRange,
451
489
  prevPrevQueryRange: updatedPrevPrevQueryRange,
490
+ prevRangeSize: updatedPrevRangeSize,
452
491
  latestBlockRangeUpdateBlock: shouldUpdateBlockRange
453
492
  ? latestFetchedBlock.blockNumber
454
493
  : p.latestBlockRangeUpdateBlock,
@@ -496,9 +535,10 @@ type t = {
496
535
  blockLag: int,
497
536
  // Buffer of items ordered from earliest to latest
498
537
  buffer: array<Internal.item>,
499
- // How many items we should aim to have in the buffer
500
- // ready for processing
501
- targetBufferSize: int,
538
+ // Caps how far ahead onBlock items are pre-generated (set to 2x the batch
539
+ // size). Fetch depth is bounded separately by getNextQuery's itemBudget, the
540
+ // chain's per-tick slice of the indexer-wide pool.
541
+ maxOnBlockBufferSize: int,
502
542
  onBlockConfigs: array<Internal.onBlockConfig>,
503
543
  knownHeight: int,
504
544
  firstEventBlock: option<int>,
@@ -554,7 +594,7 @@ let numAddresses = fetchState => fetchState.indexingAddresses->Utils.Dict.size
554
594
 
555
595
  // Appends Block items produced by the onBlock handlers for every block in
556
596
  // (fromBlock, maxBlockNumber] into mutItems and returns the new
557
- // latestOnBlockBlockNumber pointer. targetBufferSize bounds how many items
597
+ // latestOnBlockBlockNumber pointer. maxOnBlockBufferSize bounds how many items
558
598
  // are generated at once to prevent OOM.
559
599
  let appendOnBlockItems = (
560
600
  ~mutItems: array<Internal.item>,
@@ -562,7 +602,7 @@ let appendOnBlockItems = (
562
602
  ~indexerStartBlock,
563
603
  ~fromBlock,
564
604
  ~maxBlockNumber,
565
- ~targetBufferSize,
605
+ ~maxOnBlockBufferSize,
566
606
  ) => {
567
607
  let newItemsCounter = ref(0)
568
608
  let latestOnBlockBlockNumber = ref(fromBlock)
@@ -573,7 +613,7 @@ let appendOnBlockItems = (
573
613
  while (
574
614
  latestOnBlockBlockNumber.contents < maxBlockNumber &&
575
615
  // Additional safeguard to prevent OOM
576
- newItemsCounter.contents <= targetBufferSize
616
+ newItemsCounter.contents <= maxOnBlockBufferSize
577
617
  ) {
578
618
  let blockNumber = latestOnBlockBlockNumber.contents + 1
579
619
  latestOnBlockBlockNumber := blockNumber
@@ -627,7 +667,7 @@ let updateInternal = (
627
667
  | [] => knownHeight
628
668
  | onBlockConfigs => {
629
669
  // Calculate the max block number we are going to create items for
630
- // Use targetBufferSize to get the last target item in the buffer
670
+ // Use maxOnBlockBufferSize to get the last target item in the buffer
631
671
  //
632
672
  // mutItems is not very reliable, since it might not be sorted,
633
673
  // but the chances for it happen are very low and not critical
@@ -636,7 +676,7 @@ let updateInternal = (
636
676
  let maxBlockNumber = switch switch mutItemsRef.contents {
637
677
  | Some(mutItems) => mutItems
638
678
  | None => fetchState.buffer
639
- }->Array.get(fetchState.targetBufferSize - 1) {
679
+ }->Array.get(fetchState.maxOnBlockBufferSize - 1) {
640
680
  | Some(item) => item->Internal.getItemBlockNumber
641
681
  | None =>
642
682
  switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
@@ -657,7 +697,7 @@ let updateInternal = (
657
697
  ~indexerStartBlock=fetchState.startBlock,
658
698
  ~fromBlock=fetchState.latestOnBlockBlockNumber,
659
699
  ~maxBlockNumber,
660
- ~targetBufferSize=fetchState.targetBufferSize,
700
+ ~maxOnBlockBufferSize=fetchState.maxOnBlockBufferSize,
661
701
  )
662
702
  }
663
703
  }
@@ -669,7 +709,7 @@ let updateInternal = (
669
709
  normalSelection: fetchState.normalSelection,
670
710
  chainId: fetchState.chainId,
671
711
  onBlockConfigs: fetchState.onBlockConfigs,
672
- targetBufferSize: fetchState.targetBufferSize,
712
+ maxOnBlockBufferSize: fetchState.maxOnBlockBufferSize,
673
713
  optimizedPartitions,
674
714
  latestOnBlockBlockNumber,
675
715
  indexingAddresses,
@@ -837,6 +877,7 @@ OptimizedPartitions.t => {
837
877
  mutPendingQueries: [],
838
878
  prevQueryRange: 0,
839
879
  prevPrevQueryRange: 0,
880
+ prevRangeSize: 0,
840
881
  latestBlockRangeUpdateBlock: 0,
841
882
  })
842
883
  nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
@@ -1178,6 +1219,7 @@ let registerDynamicContracts = (
1178
1219
  mutPendingQueries: p.mutPendingQueries,
1179
1220
  prevQueryRange: p.prevQueryRange,
1180
1221
  prevPrevQueryRange: p.prevPrevQueryRange,
1222
+ prevRangeSize: p.prevRangeSize,
1181
1223
  latestBlockRangeUpdateBlock: p.latestBlockRangeUpdateBlock,
1182
1224
  })
1183
1225
  }
@@ -1228,9 +1270,9 @@ let handleQueryResult = (
1228
1270
  // param-level analogue of EventRouter's srcAddress effectiveStartBlock check.
1229
1271
  let newItems = newItems->Array.filter(item =>
1230
1272
  switch item {
1231
- | Internal.Event({eventConfig, event, blockNumber}) =>
1273
+ | Internal.Event({eventConfig, payload, blockNumber}) =>
1232
1274
  switch eventConfig.clientAddressFilter {
1233
- | Some(filter) => filter(event, blockNumber, fetchState.indexingAddresses)
1275
+ | Some(filter) => filter(payload, blockNumber, fetchState.indexingAddresses)
1234
1276
  | None => true
1235
1277
  }
1236
1278
  | _ => true
@@ -1240,6 +1282,7 @@ let handleQueryResult = (
1240
1282
  ~optimizedPartitions=fetchState.optimizedPartitions->OptimizedPartitions.handleQueryResponse(
1241
1283
  ~query,
1242
1284
  ~knownHeight=fetchState.knownHeight,
1285
+ ~itemsCount=newItems->Array.length,
1243
1286
  ~latestFetchedBlock,
1244
1287
  ),
1245
1288
  ~mutItems=?{
@@ -1252,7 +1295,6 @@ let handleQueryResult = (
1252
1295
  }
1253
1296
 
1254
1297
  type nextQuery =
1255
- | ReachedMaxConcurrency
1256
1298
  | WaitingForNewBlock
1257
1299
  | NothingToQuery
1258
1300
  | Ready(array<query>)
@@ -1266,6 +1308,7 @@ let startFetchingQueries = ({optimizedPartitions}: t, ~queries: array<query>) =>
1266
1308
  fromBlock: q.fromBlock,
1267
1309
  toBlock: q.toBlock,
1268
1310
  isChunk: q.isChunk,
1311
+ estResponseSize: q.estResponseSize,
1269
1312
  fetchedBlock: None,
1270
1313
  }
1271
1314
 
@@ -1294,6 +1337,7 @@ let pushQueriesForRange = (
1294
1337
  ~rangeEndBlock: option<int>,
1295
1338
  ~maxQueryBlockNumber: int,
1296
1339
  ~maybeChunkRange: option<int>,
1340
+ ~partition: partition,
1297
1341
  ~selection: selection,
1298
1342
  ~addressesByContractName: dict<array<Address.t>>,
1299
1343
  ~indexingAddresses: dict<indexingAddress>,
@@ -1310,6 +1354,14 @@ let pushQueriesForRange = (
1310
1354
  toBlock: rangeEndBlock,
1311
1355
  selection,
1312
1356
  isChunk: false,
1357
+ estResponseSize: calculateEstResponseSize(
1358
+ partition,
1359
+ ~fromBlock=rangeFromBlock,
1360
+ ~toBlock=rangeEndBlock,
1361
+ ~maxQueryBlockNumber,
1362
+ ),
1363
+ chainId: 0,
1364
+ progress: 0.,
1313
1365
  addressesByContractName,
1314
1366
  indexingAddresses,
1315
1367
  })
@@ -1319,26 +1371,38 @@ let pushQueriesForRange = (
1319
1371
  | None => maxQueryBlockNumber
1320
1372
  }
1321
1373
  let chunkSize = Js.Math.ceil_int(chunkRange->Int.toFloat *. 1.8)
1322
- if rangeFromBlock + 2 * chunkSize - 1 <= maxBlock {
1323
- // Create 2 chunks of ceil(1.8 * chunkRange) each
1324
- queries->Array.push({
1325
- partitionId,
1326
- fromBlock: rangeFromBlock,
1327
- toBlock: Some(rangeFromBlock + chunkSize - 1),
1328
- isChunk: true,
1329
- selection,
1330
- addressesByContractName,
1331
- indexingAddresses,
1332
- })
1333
- queries->Array.push({
1334
- partitionId,
1335
- fromBlock: rangeFromBlock + chunkSize,
1336
- toBlock: Some(rangeFromBlock + 2 * chunkSize - 1),
1337
- isChunk: true,
1338
- selection,
1339
- addressesByContractName,
1340
- indexingAddresses,
1341
- })
1374
+ // Probe with two smaller chunks first so their responses come back
1375
+ // quickly and refresh the chunking heuristic, then three full-size chunks.
1376
+ let probeSize = Js.Math.ceil_int(chunkRange->Int.toFloat *. 0.9)
1377
+ let getChunkSize = chunkIdx => chunkIdx < 2 ? probeSize : chunkSize
1378
+ if rangeFromBlock + getChunkSize(0) + getChunkSize(1) - 1 <= maxBlock {
1379
+ let chunkFromBlock = ref(rangeFromBlock)
1380
+ let chunkIdx = ref(0)
1381
+ while (
1382
+ chunkIdx.contents < 5 &&
1383
+ chunkFromBlock.contents + getChunkSize(chunkIdx.contents) - 1 <= maxBlock
1384
+ ) {
1385
+ let chunkToBlock = chunkFromBlock.contents + getChunkSize(chunkIdx.contents) - 1
1386
+ queries->Array.push({
1387
+ partitionId,
1388
+ fromBlock: chunkFromBlock.contents,
1389
+ toBlock: Some(chunkToBlock),
1390
+ isChunk: true,
1391
+ selection,
1392
+ estResponseSize: calculateEstResponseSize(
1393
+ partition,
1394
+ ~fromBlock=chunkFromBlock.contents,
1395
+ ~toBlock=Some(chunkToBlock),
1396
+ ~maxQueryBlockNumber,
1397
+ ),
1398
+ chainId: 0,
1399
+ progress: 0.,
1400
+ addressesByContractName,
1401
+ indexingAddresses,
1402
+ })
1403
+ chunkFromBlock := chunkToBlock + 1
1404
+ chunkIdx := chunkIdx.contents + 1
1405
+ }
1342
1406
  } else {
1343
1407
  // Not enough room for 2 chunks, fall back to a single query
1344
1408
  queries->Array.push({
@@ -1347,6 +1411,14 @@ let pushQueriesForRange = (
1347
1411
  toBlock: rangeEndBlock,
1348
1412
  selection,
1349
1413
  isChunk: rangeEndBlock !== None,
1414
+ estResponseSize: calculateEstResponseSize(
1415
+ partition,
1416
+ ~fromBlock=rangeFromBlock,
1417
+ ~toBlock=rangeEndBlock,
1418
+ ~maxQueryBlockNumber,
1419
+ ),
1420
+ chainId: 0,
1421
+ progress: 0.,
1350
1422
  addressesByContractName,
1351
1423
  indexingAddresses,
1352
1424
  })
@@ -1356,23 +1428,28 @@ let pushQueriesForRange = (
1356
1428
  }
1357
1429
  }
1358
1430
 
1431
+ // Most parallel in-flight chunk queries a single partition may have at once.
1432
+ let maxPendingChunksPerPartition = 10
1433
+
1359
1434
  let getNextQuery = (
1360
1435
  {
1361
1436
  buffer,
1362
1437
  optimizedPartitions,
1363
- targetBufferSize,
1364
1438
  indexingAddresses,
1365
1439
  blockLag,
1366
1440
  latestOnBlockBlockNumber,
1367
1441
  knownHeight,
1368
1442
  } as fetchState: t,
1369
- ~concurrencyLimit,
1443
+ ~budget,
1444
+ ~chainPendingBudget,
1370
1445
  ) => {
1371
1446
  let headBlockNumber = knownHeight - blockLag
1372
1447
  if headBlockNumber <= 0 {
1373
1448
  WaitingForNewBlock
1374
- } else if concurrencyLimit === 0 {
1375
- ReachedMaxConcurrency
1449
+ } else if budget <= 0 {
1450
+ // No room left in the shared buffer pool for this chain; wait for processing
1451
+ // to drain before fetching more.
1452
+ NothingToQuery
1376
1453
  } else {
1377
1454
  let isOnBlockBehindTheHead = latestOnBlockBlockNumber < headBlockNumber
1378
1455
  let shouldWaitForNewBlock = ref(
@@ -1383,13 +1460,11 @@ let getNextQuery = (
1383
1460
  !isOnBlockBehindTheHead,
1384
1461
  )
1385
1462
 
1386
- // We want to limit the buffer size to targetBufferSize (usually 3 * batchSize)
1387
- // To make sure the processing always has some buffer
1388
- // and not increase the memory usage too much
1389
- // If a partition fetched further
1390
- // it should be skipped until the buffer is consumed
1463
+ // Limit how far ahead we fetch to budget items (plus what's already in
1464
+ // flight) so processing always has buffer without ballooning memory. A
1465
+ // partition that fetched further is skipped until the buffer drains.
1391
1466
  let maxQueryBlockNumber = {
1392
- switch buffer->Array.get(targetBufferSize - 1) {
1467
+ switch buffer->Array.get(budget + chainPendingBudget->Float.toInt - 1) {
1393
1468
  | Some(item) =>
1394
1469
  // Just in case check that we don't query beyond the current block
1395
1470
  Pervasives.min(item->Internal.getItemBlockNumber, knownHeight)
@@ -1406,8 +1481,9 @@ let getNextQuery = (
1406
1481
  let partitionId = optimizedPartitions.idsInAscOrder->Array.getUnsafe(idx)
1407
1482
  let p = optimizedPartitions.entities->Dict.getUnsafe(partitionId)
1408
1483
 
1484
+ let pendingCount = p.mutPendingQueries->Array.length
1409
1485
  let isBehindTheHead = p.latestFetchedBlock.blockNumber < headBlockNumber
1410
- let hasPendingQueries = p.mutPendingQueries->Utils.Array.notEmpty
1486
+ let hasPendingQueries = pendingCount > 0
1411
1487
 
1412
1488
  if hasPendingQueries || isBehindTheHead {
1413
1489
  // Even if there are some partitions waiting for the new block
@@ -1418,6 +1494,8 @@ let getNextQuery = (
1418
1494
  shouldWaitForNewBlock := false
1419
1495
  }
1420
1496
 
1497
+ let partitionQueriesStart = queries->Array.length
1498
+
1421
1499
  // Compute queryEndBlock for this partition
1422
1500
  let queryEndBlock = Utils.Math.minOptInt(fetchState.endBlock, p.mergeBlock)
1423
1501
  let queryEndBlock = switch blockLag {
@@ -1455,6 +1533,7 @@ let getNextQuery = (
1455
1533
  ~rangeEndBlock=Utils.Math.minOptInt(Some(pq.fromBlock - 1), queryEndBlock),
1456
1534
  ~maxQueryBlockNumber,
1457
1535
  ~maybeChunkRange,
1536
+ ~partition=p,
1458
1537
  ~selection=p.selection,
1459
1538
  ~addressesByContractName=p.addressesByContractName,
1460
1539
  ~indexingAddresses,
@@ -1479,12 +1558,28 @@ let getNextQuery = (
1479
1558
  ~rangeEndBlock=queryEndBlock,
1480
1559
  ~maxQueryBlockNumber,
1481
1560
  ~maybeChunkRange,
1561
+ ~partition=p,
1482
1562
  ~selection=p.selection,
1483
1563
  ~addressesByContractName=p.addressesByContractName,
1484
1564
  ~indexingAddresses,
1485
1565
  )
1486
1566
  }
1487
1567
 
1568
+ // Cap parallel in-flight chunks per partition so a single partition can't
1569
+ // monopolize fetching. Keep the earliest new chunks; the furthest-ahead
1570
+ // ones wait for the next round once these resolve.
1571
+ let maxNewChunks = Pervasives.max(0, maxPendingChunksPerPartition - pendingCount)
1572
+ let generatedCount = queries->Array.length - partitionQueriesStart
1573
+ if generatedCount > maxNewChunks {
1574
+ queries
1575
+ ->Array.splice(
1576
+ ~start=partitionQueriesStart + maxNewChunks,
1577
+ ~remove=generatedCount - maxNewChunks,
1578
+ ~insert=[],
1579
+ )
1580
+ ->ignore
1581
+ }
1582
+
1488
1583
  idxRef := idxRef.contents + 1
1489
1584
  }
1490
1585
 
@@ -1495,13 +1590,6 @@ let getNextQuery = (
1495
1590
  NothingToQuery
1496
1591
  }
1497
1592
  } else {
1498
- // Enforce concurrency limit: sort by fromBlock and take the first concurrencyLimit
1499
- let queries = if queries->Array.length > concurrencyLimit {
1500
- queries->Array.sort((a, b) => Int.compare(a.fromBlock, b.fromBlock))
1501
- queries->Array.slice(~start=0, ~end=concurrencyLimit)
1502
- } else {
1503
- queries
1504
- }
1505
1593
  Ready(queries)
1506
1594
  }
1507
1595
  }
@@ -1547,7 +1635,7 @@ let make = (
1547
1635
  ~addresses: array<Internal.indexingAddress>,
1548
1636
  ~maxAddrInPartition,
1549
1637
  ~chainId,
1550
- ~targetBufferSize,
1638
+ ~maxOnBlockBufferSize,
1551
1639
  ~knownHeight,
1552
1640
  ~progressBlockNumber=startBlock - 1,
1553
1641
  ~onBlockConfigs=[],
@@ -1611,6 +1699,7 @@ let make = (
1611
1699
  mutPendingQueries: [],
1612
1700
  prevQueryRange: 0,
1613
1701
  prevPrevQueryRange: 0,
1702
+ prevRangeSize: 0,
1614
1703
  latestBlockRangeUpdateBlock: 0,
1615
1704
  })
1616
1705
  }
@@ -1696,7 +1785,7 @@ let make = (
1696
1785
  ~indexerStartBlock=startBlock,
1697
1786
  ~fromBlock=progressBlockNumber,
1698
1787
  ~maxBlockNumber,
1699
- ~targetBufferSize,
1788
+ ~maxOnBlockBufferSize,
1700
1789
  )
1701
1790
  } else {
1702
1791
  progressBlockNumber
@@ -1713,7 +1802,7 @@ let make = (
1713
1802
  indexingAddresses,
1714
1803
  blockLag,
1715
1804
  onBlockConfigs,
1716
- targetBufferSize,
1805
+ maxOnBlockBufferSize,
1717
1806
  knownHeight,
1718
1807
  buffer,
1719
1808
  firstEventBlock,
@@ -1737,6 +1826,25 @@ let make = (
1737
1826
 
1738
1827
  let bufferSize = ({buffer}: t) => buffer->Array.length
1739
1828
 
1829
+ // Number of buffered items at or below the ready frontier (processable now,
1830
+ // i.e. not stuck behind a gap from a lagging partition or out-of-order chunk).
1831
+ // The buffer is kept sorted, so binary-search the frontier in O(log n).
1832
+ let bufferReadyCount = (fetchState: t) => {
1833
+ let frontier = fetchState->bufferBlockNumber
1834
+ let buffer = fetchState.buffer
1835
+ let lo = ref(0)
1836
+ let hi = ref(buffer->Array.length)
1837
+ while lo.contents < hi.contents {
1838
+ let mid = (lo.contents + hi.contents) / 2
1839
+ if buffer->Array.getUnsafe(mid)->Internal.getItemBlockNumber <= frontier {
1840
+ lo := mid + 1
1841
+ } else {
1842
+ hi := mid
1843
+ }
1844
+ }
1845
+ lo.contents
1846
+ }
1847
+
1740
1848
  let rollbackPendingQueries = (mutPendingQueries: array<pendingQuery>, ~targetBlockNumber) => {
1741
1849
  // - Remove queries where fromBlock > target
1742
1850
  // - Cap fetchedBlock at target where fetchedBlock > target
@@ -1935,6 +2043,18 @@ let isActivelyIndexing = ({endBlock} as fetchState: t) => {
1935
2043
  }
1936
2044
  }
1937
2045
 
2046
+ // True once the fetch frontier has reached the (lagged) head or endBlock,
2047
+ // regardless of whether the buffer has been consumed yet. Unlike
2048
+ // isReadyToEnterReorgThreshold, fetched-but-unprocessed items still count.
2049
+ let isFetchingAtHead = ({endBlock, blockLag, knownHeight} as fetchState: t) => {
2050
+ let bufferBlockNumber = fetchState->bufferBlockNumber
2051
+ knownHeight !== 0 &&
2052
+ switch endBlock {
2053
+ | Some(endBlock) if bufferBlockNumber >= endBlock => true
2054
+ | _ => bufferBlockNumber >= knownHeight - blockLag
2055
+ }
2056
+ }
2057
+
1938
2058
  let isReadyToEnterReorgThreshold = ({endBlock, blockLag, buffer, knownHeight} as fetchState: t) => {
1939
2059
  let bufferBlockNumber = fetchState->bufferBlockNumber
1940
2060
  knownHeight !== 0 &&
@@ -1945,7 +2065,37 @@ let isReadyToEnterReorgThreshold = ({endBlock, blockLag, buffer, knownHeight} as
1945
2065
  buffer->Utils.Array.isEmpty
1946
2066
  }
1947
2067
 
1948
- let sortForUnorderedBatch = {
2068
+ // Lower progress percentage = further behind = higher priority. Shared by the
2069
+ // batch ordering and the cross-chain fetch priority.
2070
+ let getProgressPercentage = (fetchState: t) => {
2071
+ switch fetchState.firstEventBlock {
2072
+ | None => 0.
2073
+ | Some(firstEventBlock) =>
2074
+ let totalRange = fetchState.knownHeight - firstEventBlock
2075
+ if totalRange <= 0 {
2076
+ 0.
2077
+ } else {
2078
+ let progress = switch fetchState.buffer->Array.get(0) {
2079
+ | Some(item) => item->Internal.getItemBlockNumber - firstEventBlock
2080
+ | None => fetchState->bufferBlockNumber - firstEventBlock
2081
+ }
2082
+ progress->Int.toFloat /. totalRange->Int.toFloat
2083
+ }
2084
+ }
2085
+ }
2086
+
2087
+ // Progress a specific block sits at along the chain, used to order queries from
2088
+ // different chains: a query starting further back (lower %) is fetched first.
2089
+ let getProgressPercentageAt = (fetchState: t, ~blockNumber) => {
2090
+ switch fetchState.firstEventBlock {
2091
+ | None => 0.
2092
+ | Some(firstEventBlock) =>
2093
+ let totalRange = fetchState.knownHeight - firstEventBlock
2094
+ totalRange <= 0 ? 0. : (blockNumber - firstEventBlock)->Int.toFloat /. totalRange->Int.toFloat
2095
+ }
2096
+ }
2097
+
2098
+ let sortForBatch = {
1949
2099
  let hasFullBatch = ({buffer} as fetchState: t, ~batchSizeTarget) => {
1950
2100
  switch buffer->Array.get(batchSizeTarget - 1) {
1951
2101
  | Some(item) => item->Internal.getItemBlockNumber <= fetchState->bufferBlockNumber
@@ -1953,24 +2103,6 @@ let sortForUnorderedBatch = {
1953
2103
  }
1954
2104
  }
1955
2105
 
1956
- // Lower progress percentage = further behind = higher priority
1957
- let getProgressPercentage = (fetchState: t) => {
1958
- switch fetchState.firstEventBlock {
1959
- | None => 0.
1960
- | Some(firstEventBlock) =>
1961
- let totalRange = fetchState.knownHeight - firstEventBlock
1962
- if totalRange <= 0 {
1963
- 0.
1964
- } else {
1965
- let progress = switch fetchState.buffer->Array.get(0) {
1966
- | Some(item) => item->Internal.getItemBlockNumber - firstEventBlock
1967
- | None => fetchState->bufferBlockNumber - firstEventBlock
1968
- }
1969
- progress->Int.toFloat /. totalRange->Int.toFloat
1970
- }
1971
- }
1972
- }
1973
-
1974
2106
  (fetchStates: array<t>, ~batchSizeTarget: int) => {
1975
2107
  let copied = fetchStates->Array.copy
1976
2108
  copied->Array.sort((a: t, b: t) => {