envio 3.3.0-alpha.5 → 3.3.0-alpha.7

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 (50) hide show
  1. package/package.json +6 -6
  2. package/src/Address.res +5 -2
  3. package/src/Address.res.mjs +3 -1
  4. package/src/BatchProcessing.res +5 -0
  5. package/src/BatchProcessing.res.mjs +6 -0
  6. package/src/ChainFetching.res +14 -25
  7. package/src/ChainFetching.res.mjs +19 -20
  8. package/src/ChainState.res +4 -4
  9. package/src/ChainState.res.mjs +2 -2
  10. package/src/ChainState.resi +1 -1
  11. package/src/Config.res +44 -0
  12. package/src/Config.res.mjs +38 -0
  13. package/src/ContractRegisterContext.res +2 -12
  14. package/src/ContractRegisterContext.res.mjs +3 -5
  15. package/src/CrossChainState.res +5 -3
  16. package/src/CrossChainState.res.mjs +5 -4
  17. package/src/ExitOnCaughtUp.res +10 -2
  18. package/src/ExitOnCaughtUp.res.mjs +10 -1
  19. package/src/FetchState.res +49 -52
  20. package/src/FetchState.res.mjs +45 -47
  21. package/src/IndexerState.res +4 -0
  22. package/src/IndexerState.res.mjs +8 -1
  23. package/src/IndexerState.resi +1 -0
  24. package/src/SimulateDeadInputTracker.res +85 -0
  25. package/src/SimulateDeadInputTracker.res.mjs +73 -0
  26. package/src/SimulateDeadInputTracker.resi +12 -0
  27. package/src/SimulateItems.res +29 -43
  28. package/src/SimulateItems.res.mjs +16 -33
  29. package/src/TestIndexer.res +3 -27
  30. package/src/TestIndexer.res.mjs +2 -9
  31. package/src/sources/EvmChain.res +1 -1
  32. package/src/sources/EvmChain.res.mjs +1 -1
  33. package/src/sources/HyperFuelSource.res +1 -0
  34. package/src/sources/HyperFuelSource.res.mjs +1 -1
  35. package/src/sources/HyperSync.res +4 -0
  36. package/src/sources/HyperSync.res.mjs +5 -4
  37. package/src/sources/HyperSync.resi +1 -0
  38. package/src/sources/HyperSyncSource.res +2 -0
  39. package/src/sources/HyperSyncSource.res.mjs +2 -2
  40. package/src/sources/RpcSource.res +287 -146
  41. package/src/sources/RpcSource.res.mjs +286 -247
  42. package/src/sources/SimulateSource.res +2 -0
  43. package/src/sources/SimulateSource.res.mjs +3 -2
  44. package/src/sources/Source.res +10 -0
  45. package/src/sources/SourceManager.res +7 -0
  46. package/src/sources/SourceManager.res.mjs +2 -1
  47. package/src/sources/Svm.res +1 -0
  48. package/src/sources/Svm.res.mjs +1 -1
  49. package/src/sources/SvmHyperSyncSource.res +2 -0
  50. package/src/sources/SvmHyperSyncSource.res.mjs +4 -2
@@ -90,23 +90,41 @@ let deriveContractNameByAddress: dict<array<Address.t>> => dict<
90
90
  result
91
91
  })
92
92
 
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
+
93
98
  // Default estimate for a query whose partition hasn't responded yet, so the
94
99
  // shared budget still accounts for unknown queries instead of treating them as
95
- // free.
96
- let defaultEstResponseSize = 10_000.
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) =>
105
+ Pervasives.max(
106
+ minEstResponseSize,
107
+ Pervasives.min(maxEstResponseSize, 20_000. /. partitionsCount->Int.toFloat),
108
+ )
97
109
 
98
110
  // Estimated items a query will return, from the partition's event density
99
111
  // (items/block derived from its last response) and the query's block range.
100
- // toBlock None is the open-ended tail, capped at maxQueryBlockNumber. A partition
112
+ // toBlock None is the open-ended tail, capped at knownHeight. A partition
101
113
  // that responded with no items has density 0, so its queries cost 0 — correct,
102
114
  // they don't fill the buffer. Only a partition that has never responded
103
- // (prevQueryRange 0) has no signal, so it falls back to defaultEstResponseSize.
104
- let calculateEstResponseSize = (p: partition, ~fromBlock, ~toBlock, ~maxQueryBlockNumber) =>
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
+ ) =>
105
123
  if p.prevQueryRange > 0 {
106
124
  let density = p.prevRangeSize->Int.toFloat /. p.prevQueryRange->Int.toFloat
107
- (toBlock->Option.getOr(maxQueryBlockNumber) - fromBlock + 1)->Int.toFloat *. density
125
+ (toBlock->Option.getOr(knownHeight) - fromBlock + 1)->Int.toFloat *. density
108
126
  } else {
109
- defaultEstResponseSize
127
+ calculateDefaultEstResponseSize(~partitionsCount)
110
128
  }
111
129
 
112
130
  // Calculate the chunk range from history using min-of-last-3-ranges heuristic
@@ -545,8 +563,8 @@ type t = {
545
563
  // Buffer of items ordered from earliest to latest
546
564
  buffer: array<Internal.item>,
547
565
  // Caps how far ahead onBlock items are pre-generated (set to 2x the batch
548
- // size). Fetch depth is bounded separately by getNextQuery's itemBudget, the
549
- // chain's per-tick slice of the indexer-wide pool.
566
+ // size). Event fetch depth is bounded separately, by CrossChainState's
567
+ // cross-chain admission against the indexer-wide buffer pool.
550
568
  maxOnBlockBufferSize: int,
551
569
  onBlockConfigs: array<Internal.onBlockConfig>,
552
570
  knownHeight: int,
@@ -1352,14 +1370,15 @@ let pushQueriesForRange = (
1352
1370
  ~partitionId: string,
1353
1371
  ~rangeFromBlock: int,
1354
1372
  ~rangeEndBlock: option<int>,
1355
- ~maxQueryBlockNumber: int,
1373
+ ~knownHeight: int,
1356
1374
  ~maybeChunkRange: option<int>,
1357
1375
  ~maxChunks: int,
1358
1376
  ~partition: partition,
1359
1377
  ~selection: selection,
1360
1378
  ~addressesByContractName: dict<array<Address.t>>,
1379
+ ~partitionsCount: int,
1361
1380
  ) => {
1362
- if rangeFromBlock <= maxQueryBlockNumber && maxChunks > 0 {
1381
+ if rangeFromBlock <= knownHeight && maxChunks > 0 {
1363
1382
  switch rangeEndBlock {
1364
1383
  | Some(endBlock) if rangeFromBlock > endBlock => ()
1365
1384
  | _ =>
@@ -1375,7 +1394,8 @@ let pushQueriesForRange = (
1375
1394
  partition,
1376
1395
  ~fromBlock=rangeFromBlock,
1377
1396
  ~toBlock=rangeEndBlock,
1378
- ~maxQueryBlockNumber,
1397
+ ~knownHeight,
1398
+ ~partitionsCount,
1379
1399
  ),
1380
1400
  chainId: 0,
1381
1401
  progress: 0.,
@@ -1384,7 +1404,7 @@ let pushQueriesForRange = (
1384
1404
  | Some(chunkRange) =>
1385
1405
  let maxBlock = switch rangeEndBlock {
1386
1406
  | Some(eb) => eb
1387
- | None => maxQueryBlockNumber
1407
+ | None => knownHeight
1388
1408
  }
1389
1409
  let chunkSize = Js.Math.ceil_int(chunkRange->Int.toFloat *. 1.8)
1390
1410
  if rangeFromBlock + chunkSize * 2 - 1 <= maxBlock {
@@ -1404,7 +1424,8 @@ let pushQueriesForRange = (
1404
1424
  partition,
1405
1425
  ~fromBlock=chunkFromBlock.contents,
1406
1426
  ~toBlock=Some(chunkToBlock),
1407
- ~maxQueryBlockNumber,
1427
+ ~knownHeight,
1428
+ ~partitionsCount,
1408
1429
  ),
1409
1430
  chainId: 0,
1410
1431
  progress: 0.,
@@ -1425,7 +1446,8 @@ let pushQueriesForRange = (
1425
1446
  partition,
1426
1447
  ~fromBlock=rangeFromBlock,
1427
1448
  ~toBlock=rangeEndBlock,
1428
- ~maxQueryBlockNumber,
1449
+ ~knownHeight,
1450
+ ~partitionsCount,
1429
1451
  ),
1430
1452
  chainId: 0,
1431
1453
  progress: 0.,
@@ -1437,45 +1459,27 @@ let pushQueriesForRange = (
1437
1459
  }
1438
1460
  }
1439
1461
 
1462
+ // Candidate queries are sized against the natural ceiling (head/endBlock/mergeBlock),
1463
+ // not a share of the shared buffer — CrossChainState.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).
1440
1467
  let getNextQuery = (
1441
- {buffer, optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight} as fetchState: t,
1442
- ~budget,
1443
- ~chainPendingBudget,
1468
+ {optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight, endBlock}: t,
1444
1469
  ) => {
1445
1470
  let headBlockNumber = knownHeight - blockLag
1446
1471
  if headBlockNumber <= 0 {
1447
1472
  WaitingForNewBlock
1448
- } else if budget <= 0 {
1449
- // No room left in the shared buffer pool for this chain; wait for processing
1450
- // to drain before fetching more.
1451
- NothingToQuery
1452
1473
  } else {
1453
1474
  let isOnBlockBehindTheHead = latestOnBlockBlockNumber < headBlockNumber
1454
1475
  let shouldWaitForNewBlock = ref(
1455
- switch fetchState.endBlock {
1476
+ switch endBlock {
1456
1477
  | Some(endBlock) => headBlockNumber < endBlock
1457
1478
  | None => true
1458
1479
  } &&
1459
1480
  !isOnBlockBehindTheHead,
1460
1481
  )
1461
1482
 
1462
- // Fetch at most `budget` items past the ready frontier (plus what's already
1463
- // in flight) so processing always has buffer without ballooning memory.
1464
- // budget already excludes items at/below the frontier (they're in the shared
1465
- // totalReadyCount), so offset the index by bufferReadyCount — otherwise the
1466
- // ready prefix is subtracted twice and the buffer caps at a fraction of its
1467
- // target. A partition that fetched further is skipped until the buffer drains.
1468
- let maxQueryBlockNumber = {
1469
- switch buffer->Array.get(
1470
- fetchState->bufferReadyCount + budget + chainPendingBudget->Float.toInt - 1,
1471
- ) {
1472
- | Some(item) =>
1473
- // Just in case check that we don't query beyond the current block
1474
- Pervasives.min(item->Internal.getItemBlockNumber, knownHeight)
1475
- | None => knownHeight
1476
- }
1477
- }
1478
-
1479
1483
  let queries = []
1480
1484
 
1481
1485
  let partitionsCount = optimizedPartitions.idsInAscOrder->Array.length
@@ -1501,7 +1505,7 @@ let getNextQuery = (
1501
1505
  let partitionQueriesStart = queries->Array.length
1502
1506
 
1503
1507
  // Compute queryEndBlock for this partition
1504
- let queryEndBlock = Utils.Math.minOptInt(fetchState.endBlock, p.mergeBlock)
1508
+ let queryEndBlock = Utils.Math.minOptInt(endBlock, p.mergeBlock)
1505
1509
  let queryEndBlock = switch blockLag {
1506
1510
  | 0 => queryEndBlock
1507
1511
  | _ =>
@@ -1509,15 +1513,6 @@ let getNextQuery = (
1509
1513
  // because otherwise HyperSync might return bigger range
1510
1514
  Utils.Math.minOptInt(Some(headBlockNumber), queryEndBlock)
1511
1515
  }
1512
- // Enforce the response range up until target block
1513
- // Otherwise for indexers with 100+ partitions
1514
- // we might blow up the buffer size to more than 600k events
1515
- // simply because of HyperSync returning extra blocks
1516
- let queryEndBlock = switch (queryEndBlock, maxQueryBlockNumber < knownHeight) {
1517
- | (Some(endBlock), true) => Some(Pervasives.min(maxQueryBlockNumber, endBlock))
1518
- | (None, true) => Some(maxQueryBlockNumber)
1519
- | (_, false) => queryEndBlock
1520
- }
1521
1516
 
1522
1517
  let maybeChunkRange = getMinHistoryRange(p)
1523
1518
 
@@ -1535,7 +1530,7 @@ let getNextQuery = (
1535
1530
  ~partitionId,
1536
1531
  ~rangeFromBlock=cursor.contents,
1537
1532
  ~rangeEndBlock=Utils.Math.minOptInt(Some(pq.fromBlock - 1), queryEndBlock),
1538
- ~maxQueryBlockNumber,
1533
+ ~knownHeight,
1539
1534
  ~maybeChunkRange,
1540
1535
  ~maxChunks=maxPendingChunksPerPartition -
1541
1536
  pendingCount -
@@ -1544,6 +1539,7 @@ let getNextQuery = (
1544
1539
  ~partition=p,
1545
1540
  ~selection=p.selection,
1546
1541
  ~addressesByContractName=p.addressesByContractName,
1542
+ ~partitionsCount,
1547
1543
  )
1548
1544
  }
1549
1545
  switch pq {
@@ -1563,7 +1559,7 @@ let getNextQuery = (
1563
1559
  ~partitionId,
1564
1560
  ~rangeFromBlock=cursor.contents,
1565
1561
  ~rangeEndBlock=queryEndBlock,
1566
- ~maxQueryBlockNumber,
1562
+ ~knownHeight,
1567
1563
  ~maybeChunkRange,
1568
1564
  ~maxChunks=maxPendingChunksPerPartition -
1569
1565
  pendingCount -
@@ -1572,6 +1568,7 @@ let getNextQuery = (
1572
1568
  ~partition=p,
1573
1569
  ~selection=p.selection,
1574
1570
  ~addressesByContractName=p.addressesByContractName,
1571
+ ~partitionsCount,
1575
1572
  )
1576
1573
  }
1577
1574
 
@@ -8,6 +8,7 @@ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
8
8
  import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
9
9
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
10
10
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
11
+ import * as Primitive_float from "@rescript/runtime/lib/es6/Primitive_float.js";
11
12
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
12
13
  import * as IndexingAddresses from "./IndexingAddresses.res.mjs";
13
14
 
@@ -21,12 +22,16 @@ let deriveContractNameByAddress = Utils.$$WeakMap.memoize(addressesByContractNam
21
22
  return result;
22
23
  });
23
24
 
24
- function calculateEstResponseSize(p, fromBlock, toBlock, maxQueryBlockNumber) {
25
+ function calculateDefaultEstResponseSize(partitionsCount) {
26
+ return Primitive_float.max(2000, Primitive_float.min(10000, 20000 / partitionsCount));
27
+ }
28
+
29
+ function calculateEstResponseSize(p, fromBlock, toBlock, knownHeight, partitionsCount) {
25
30
  if (p.prevQueryRange <= 0) {
26
- return 10000;
31
+ return calculateDefaultEstResponseSize(partitionsCount);
27
32
  }
28
33
  let density = p.prevRangeSize / p.prevQueryRange;
29
- return ((Stdlib_Option.getOr(toBlock, maxQueryBlockNumber) - fromBlock | 0) + 1 | 0) * density;
34
+ return ((Stdlib_Option.getOr(toBlock, knownHeight) - fromBlock | 0) + 1 | 0) * density;
30
35
  }
31
36
 
32
37
  function getMinHistoryRange(p) {
@@ -977,15 +982,15 @@ function startFetchingQueries(param, queries) {
977
982
  }
978
983
  }
979
984
 
980
- function pushQueriesForRange(queries, partitionId, rangeFromBlock, rangeEndBlock, maxQueryBlockNumber, maybeChunkRange, maxChunks, partition, selection, addressesByContractName) {
981
- if (!(rangeFromBlock <= maxQueryBlockNumber && maxChunks > 0)) {
985
+ function pushQueriesForRange(queries, partitionId, rangeFromBlock, rangeEndBlock, knownHeight, maybeChunkRange, maxChunks, partition, selection, addressesByContractName, partitionsCount) {
986
+ if (!(rangeFromBlock <= knownHeight && maxChunks > 0)) {
982
987
  return;
983
988
  }
984
989
  if (rangeEndBlock !== undefined && rangeFromBlock > rangeEndBlock) {
985
990
  return;
986
991
  }
987
992
  if (maybeChunkRange !== undefined) {
988
- let maxBlock = rangeEndBlock !== undefined ? rangeEndBlock : maxQueryBlockNumber;
993
+ let maxBlock = rangeEndBlock !== undefined ? rangeEndBlock : knownHeight;
989
994
  let chunkSize = Js_math.ceil_int(maybeChunkRange * 1.8);
990
995
  if (((rangeFromBlock + (chunkSize << 1) | 0) - 1 | 0) <= maxBlock) {
991
996
  let chunkFromBlock = rangeFromBlock;
@@ -997,7 +1002,7 @@ function pushQueriesForRange(queries, partitionId, rangeFromBlock, rangeEndBlock
997
1002
  fromBlock: chunkFromBlock,
998
1003
  toBlock: chunkToBlock,
999
1004
  isChunk: true,
1000
- estResponseSize: calculateEstResponseSize(partition, chunkFromBlock, chunkToBlock, maxQueryBlockNumber),
1005
+ estResponseSize: calculateEstResponseSize(partition, chunkFromBlock, chunkToBlock, knownHeight, partitionsCount),
1001
1006
  chainId: 0,
1002
1007
  progress: 0,
1003
1008
  selection: selection,
@@ -1013,7 +1018,7 @@ function pushQueriesForRange(queries, partitionId, rangeFromBlock, rangeEndBlock
1013
1018
  fromBlock: rangeFromBlock,
1014
1019
  toBlock: rangeEndBlock,
1015
1020
  isChunk: rangeEndBlock !== undefined,
1016
- estResponseSize: calculateEstResponseSize(partition, rangeFromBlock, rangeEndBlock, maxQueryBlockNumber),
1021
+ estResponseSize: calculateEstResponseSize(partition, rangeFromBlock, rangeEndBlock, knownHeight, partitionsCount),
1017
1022
  chainId: 0,
1018
1023
  progress: 0,
1019
1024
  selection: selection,
@@ -1026,7 +1031,7 @@ function pushQueriesForRange(queries, partitionId, rangeFromBlock, rangeEndBlock
1026
1031
  fromBlock: rangeFromBlock,
1027
1032
  toBlock: rangeEndBlock,
1028
1033
  isChunk: false,
1029
- estResponseSize: calculateEstResponseSize(partition, rangeFromBlock, rangeEndBlock, maxQueryBlockNumber),
1034
+ estResponseSize: calculateEstResponseSize(partition, rangeFromBlock, rangeEndBlock, knownHeight, partitionsCount),
1030
1035
  chainId: 0,
1031
1036
  progress: 0,
1032
1037
  selection: selection,
@@ -1034,24 +1039,19 @@ function pushQueriesForRange(queries, partitionId, rangeFromBlock, rangeEndBlock
1034
1039
  });
1035
1040
  }
1036
1041
 
1037
- function getNextQuery(fetchState, budget, chainPendingBudget) {
1038
- let knownHeight = fetchState.knownHeight;
1039
- let blockLag = fetchState.blockLag;
1040
- let optimizedPartitions = fetchState.optimizedPartitions;
1042
+ function getNextQuery(param) {
1043
+ let knownHeight = param.knownHeight;
1044
+ let blockLag = param.blockLag;
1045
+ let endBlock = param.endBlock;
1046
+ let optimizedPartitions = param.optimizedPartitions;
1041
1047
  let headBlockNumber = knownHeight - blockLag | 0;
1042
1048
  if (headBlockNumber <= 0) {
1043
1049
  return "WaitingForNewBlock";
1044
1050
  }
1045
- if (budget <= 0) {
1046
- return "NothingToQuery";
1047
- }
1048
- let isOnBlockBehindTheHead = fetchState.latestOnBlockBlockNumber < headBlockNumber;
1049
- let endBlock = fetchState.endBlock;
1051
+ let isOnBlockBehindTheHead = param.latestOnBlockBlockNumber < headBlockNumber;
1050
1052
  let shouldWaitForNewBlock = (
1051
1053
  endBlock !== undefined ? headBlockNumber < endBlock : true
1052
1054
  ) && !isOnBlockBehindTheHead;
1053
- let item = fetchState.buffer[((bufferReadyCount(fetchState) + budget | 0) + (chainPendingBudget | 0) | 0) - 1 | 0];
1054
- let maxQueryBlockNumber = item !== undefined ? Primitive_int.min(item.blockNumber, knownHeight) : knownHeight;
1055
1055
  let queries = [];
1056
1056
  let partitionsCount = optimizedPartitions.idsInAscOrder.length;
1057
1057
  let idxRef = 0;
@@ -1066,14 +1066,8 @@ function getNextQuery(fetchState, budget, chainPendingBudget) {
1066
1066
  shouldWaitForNewBlock = false;
1067
1067
  }
1068
1068
  let partitionQueriesStart = queries.length;
1069
- let queryEndBlock = Utils.$$Math.minOptInt(fetchState.endBlock, p.mergeBlock);
1069
+ let queryEndBlock = Utils.$$Math.minOptInt(endBlock, p.mergeBlock);
1070
1070
  let queryEndBlock$1 = blockLag !== 0 ? Utils.$$Math.minOptInt(headBlockNumber, queryEndBlock) : queryEndBlock;
1071
- let match = maxQueryBlockNumber < knownHeight;
1072
- let queryEndBlock$2 = queryEndBlock$1 !== undefined ? (
1073
- match ? Primitive_int.min(maxQueryBlockNumber, queryEndBlock$1) : queryEndBlock$1
1074
- ) : (
1075
- match ? maxQueryBlockNumber : queryEndBlock$1
1076
- );
1077
1071
  let maybeChunkRange = getMinHistoryRange(p);
1078
1072
  let cursor = p.latestFetchedBlock.blockNumber + 1 | 0;
1079
1073
  let canContinue = true;
@@ -1084,16 +1078,16 @@ function getNextQuery(fetchState, budget, chainPendingBudget) {
1084
1078
  let addressesByContractName = p.addressesByContractName;
1085
1079
  let selection = p.selection;
1086
1080
  let maxChunks = (10 - pendingCount | 0) - (queries.length - partitionQueriesStart | 0) | 0;
1087
- let rangeEndBlock = Utils.$$Math.minOptInt(pq.fromBlock - 1 | 0, queryEndBlock$2);
1081
+ let rangeEndBlock = Utils.$$Math.minOptInt(pq.fromBlock - 1 | 0, queryEndBlock$1);
1088
1082
  let rangeFromBlock = cursor;
1089
- if (rangeFromBlock <= maxQueryBlockNumber && maxChunks > 0) {
1083
+ if (rangeFromBlock <= knownHeight && maxChunks > 0) {
1090
1084
  let exit = 0;
1091
1085
  if (!(rangeEndBlock !== undefined && rangeFromBlock > rangeEndBlock)) {
1092
1086
  exit = 1;
1093
1087
  }
1094
1088
  if (exit === 1) {
1095
1089
  if (maybeChunkRange !== undefined) {
1096
- let maxBlock = rangeEndBlock !== undefined ? rangeEndBlock : maxQueryBlockNumber;
1090
+ let maxBlock = rangeEndBlock !== undefined ? rangeEndBlock : knownHeight;
1097
1091
  let chunkSize = Js_math.ceil_int(maybeChunkRange * 1.8);
1098
1092
  if (((rangeFromBlock + (chunkSize << 1) | 0) - 1 | 0) <= maxBlock) {
1099
1093
  let chunkFromBlock = rangeFromBlock;
@@ -1105,7 +1099,7 @@ function getNextQuery(fetchState, budget, chainPendingBudget) {
1105
1099
  fromBlock: chunkFromBlock,
1106
1100
  toBlock: chunkToBlock,
1107
1101
  isChunk: true,
1108
- estResponseSize: calculateEstResponseSize(p, chunkFromBlock, chunkToBlock, maxQueryBlockNumber),
1102
+ estResponseSize: calculateEstResponseSize(p, chunkFromBlock, chunkToBlock, knownHeight, partitionsCount),
1109
1103
  chainId: 0,
1110
1104
  progress: 0,
1111
1105
  selection: selection,
@@ -1120,7 +1114,7 @@ function getNextQuery(fetchState, budget, chainPendingBudget) {
1120
1114
  fromBlock: rangeFromBlock,
1121
1115
  toBlock: rangeEndBlock,
1122
1116
  isChunk: rangeEndBlock !== undefined,
1123
- estResponseSize: calculateEstResponseSize(p, rangeFromBlock, rangeEndBlock, maxQueryBlockNumber),
1117
+ estResponseSize: calculateEstResponseSize(p, rangeFromBlock, rangeEndBlock, knownHeight, partitionsCount),
1124
1118
  chainId: 0,
1125
1119
  progress: 0,
1126
1120
  selection: selection,
@@ -1133,7 +1127,7 @@ function getNextQuery(fetchState, budget, chainPendingBudget) {
1133
1127
  fromBlock: rangeFromBlock,
1134
1128
  toBlock: rangeEndBlock,
1135
1129
  isChunk: false,
1136
- estResponseSize: calculateEstResponseSize(p, rangeFromBlock, rangeEndBlock, maxQueryBlockNumber),
1130
+ estResponseSize: calculateEstResponseSize(p, rangeFromBlock, rangeEndBlock, knownHeight, partitionsCount),
1137
1131
  chainId: 0,
1138
1132
  progress: 0,
1139
1133
  selection: selection,
@@ -1145,9 +1139,9 @@ function getNextQuery(fetchState, budget, chainPendingBudget) {
1145
1139
  }
1146
1140
  let toBlock = pq.toBlock;
1147
1141
  if (toBlock !== undefined && pq.isChunk) {
1148
- let match$1 = pq.fetchedBlock;
1149
- if (match$1 !== undefined) {
1150
- let blockNumber = match$1.blockNumber;
1142
+ let match = pq.fetchedBlock;
1143
+ if (match !== undefined) {
1144
+ let blockNumber = match.blockNumber;
1151
1145
  cursor = blockNumber < toBlock ? blockNumber + 1 | 0 : toBlock + 1 | 0;
1152
1146
  } else {
1153
1147
  cursor = toBlock + 1 | 0;
@@ -1162,14 +1156,14 @@ function getNextQuery(fetchState, budget, chainPendingBudget) {
1162
1156
  let selection$1 = p.selection;
1163
1157
  let maxChunks$1 = (10 - pendingCount | 0) - (queries.length - partitionQueriesStart | 0) | 0;
1164
1158
  let rangeFromBlock$1 = cursor;
1165
- if (rangeFromBlock$1 <= maxQueryBlockNumber && maxChunks$1 > 0) {
1159
+ if (rangeFromBlock$1 <= knownHeight && maxChunks$1 > 0) {
1166
1160
  let exit$1 = 0;
1167
- if (!(queryEndBlock$2 !== undefined && rangeFromBlock$1 > queryEndBlock$2)) {
1161
+ if (!(queryEndBlock$1 !== undefined && rangeFromBlock$1 > queryEndBlock$1)) {
1168
1162
  exit$1 = 1;
1169
1163
  }
1170
1164
  if (exit$1 === 1) {
1171
1165
  if (maybeChunkRange !== undefined) {
1172
- let maxBlock$1 = queryEndBlock$2 !== undefined ? queryEndBlock$2 : maxQueryBlockNumber;
1166
+ let maxBlock$1 = queryEndBlock$1 !== undefined ? queryEndBlock$1 : knownHeight;
1173
1167
  let chunkSize$1 = Js_math.ceil_int(maybeChunkRange * 1.8);
1174
1168
  if (((rangeFromBlock$1 + (chunkSize$1 << 1) | 0) - 1 | 0) <= maxBlock$1) {
1175
1169
  let chunkFromBlock$1 = rangeFromBlock$1;
@@ -1181,7 +1175,7 @@ function getNextQuery(fetchState, budget, chainPendingBudget) {
1181
1175
  fromBlock: chunkFromBlock$1,
1182
1176
  toBlock: chunkToBlock$1,
1183
1177
  isChunk: true,
1184
- estResponseSize: calculateEstResponseSize(p, chunkFromBlock$1, chunkToBlock$1, maxQueryBlockNumber),
1178
+ estResponseSize: calculateEstResponseSize(p, chunkFromBlock$1, chunkToBlock$1, knownHeight, partitionsCount),
1185
1179
  chainId: 0,
1186
1180
  progress: 0,
1187
1181
  selection: selection$1,
@@ -1194,9 +1188,9 @@ function getNextQuery(fetchState, budget, chainPendingBudget) {
1194
1188
  queries.push({
1195
1189
  partitionId: partitionId,
1196
1190
  fromBlock: rangeFromBlock$1,
1197
- toBlock: queryEndBlock$2,
1198
- isChunk: queryEndBlock$2 !== undefined,
1199
- estResponseSize: calculateEstResponseSize(p, rangeFromBlock$1, queryEndBlock$2, maxQueryBlockNumber),
1191
+ toBlock: queryEndBlock$1,
1192
+ isChunk: queryEndBlock$1 !== undefined,
1193
+ estResponseSize: calculateEstResponseSize(p, rangeFromBlock$1, queryEndBlock$1, knownHeight, partitionsCount),
1200
1194
  chainId: 0,
1201
1195
  progress: 0,
1202
1196
  selection: selection$1,
@@ -1207,9 +1201,9 @@ function getNextQuery(fetchState, budget, chainPendingBudget) {
1207
1201
  queries.push({
1208
1202
  partitionId: partitionId,
1209
1203
  fromBlock: rangeFromBlock$1,
1210
- toBlock: queryEndBlock$2,
1204
+ toBlock: queryEndBlock$1,
1211
1205
  isChunk: false,
1212
- estResponseSize: calculateEstResponseSize(p, rangeFromBlock$1, queryEndBlock$2, maxQueryBlockNumber),
1206
+ estResponseSize: calculateEstResponseSize(p, rangeFromBlock$1, queryEndBlock$1, knownHeight, partitionsCount),
1213
1207
  chainId: 0,
1214
1208
  progress: 0,
1215
1209
  selection: selection$1,
@@ -1656,7 +1650,9 @@ function updateKnownHeight(fetchState, knownHeight) {
1656
1650
  }
1657
1651
  }
1658
1652
 
1659
- let defaultEstResponseSize = 10000;
1653
+ let minEstResponseSize = 2000;
1654
+
1655
+ let maxEstResponseSize = 10000;
1660
1656
 
1661
1657
  let blockItemLogIndex = 16777216;
1662
1658
 
@@ -1664,7 +1660,9 @@ let maxPendingChunksPerPartition = 10;
1664
1660
 
1665
1661
  export {
1666
1662
  deriveContractNameByAddress,
1667
- defaultEstResponseSize,
1663
+ minEstResponseSize,
1664
+ maxEstResponseSize,
1665
+ calculateDefaultEstResponseSize,
1668
1666
  calculateEstResponseSize,
1669
1667
  getMinHistoryRange,
1670
1668
  getMinQueryRange,
@@ -108,6 +108,8 @@ type t = {
108
108
  // waitForNewBlock waiter is bound to the old, pre-realtime source). A fetch
109
109
  // response or waiter carrying an older epoch than this is discarded.
110
110
  mutable epoch: int,
111
+ // None off the simulate path.
112
+ simulateDeadInputTracker: option<SimulateDeadInputTracker.t>,
111
113
  }
112
114
 
113
115
  let make = (
@@ -169,6 +171,7 @@ let make = (
169
171
  onError,
170
172
  isStopped: false,
171
173
  epoch: 0,
174
+ simulateDeadInputTracker: SimulateDeadInputTracker.makeFromConfig(config),
172
175
  }
173
176
  }
174
177
 
@@ -400,6 +403,7 @@ let exitAfterFirstEventBlock = (state: t) => state.exitAfterFirstEventBlock
400
403
  let isStopped = (state: t) => state.isStopped
401
404
  let epoch = (state: t) => state.epoch
402
405
  let pruneStaleEntityHistoryThrottler = (state: t) => state.writeThrottlers.pruneStaleEntityHistory
406
+ let simulateDeadInputTracker = (state: t) => state.simulateDeadInputTracker
403
407
 
404
408
  // --- Store domain operations. ---
405
409
 
@@ -18,6 +18,7 @@ import * as CrossChainState from "./CrossChainState.res.mjs";
18
18
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
19
19
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
20
20
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
21
+ import * as SimulateDeadInputTracker from "./SimulateDeadInputTracker.res.mjs";
21
22
 
22
23
  function make() {
23
24
  let intervalMillis = Env.ThrottleWrites.pruneStaleDataIntervalMillis;
@@ -91,7 +92,8 @@ function make$2(config, persistence, chainStates, isInReorgThreshold, isRealtime
91
92
  exitAfterFirstEventBlock: exitAfterFirstEventBlock,
92
93
  onError: onError,
93
94
  isStopped: false,
94
- epoch: 0
95
+ epoch: 0,
96
+ simulateDeadInputTracker: SimulateDeadInputTracker.makeFromConfig(config)
95
97
  };
96
98
  }
97
99
 
@@ -348,6 +350,10 @@ function pruneStaleEntityHistoryThrottler(state) {
348
350
  return state.writeThrottlers.pruneStaleEntityHistory;
349
351
  }
350
352
 
353
+ function simulateDeadInputTracker(state) {
354
+ return state.simulateDeadInputTracker;
355
+ }
356
+
351
357
  function queueProcessedBatch(state, batch) {
352
358
  state.processedBatches.push(batch);
353
359
  let checkpointId = Utils.$$Array.last(batch.checkpointIds);
@@ -527,6 +533,7 @@ export {
527
533
  isStopped,
528
534
  epoch,
529
535
  pruneStaleEntityHistoryThrottler,
536
+ simulateDeadInputTracker,
530
537
  queueProcessedBatch,
531
538
  drainBatchRun,
532
539
  takeRollback,
@@ -107,6 +107,7 @@ let exitAfterFirstEventBlock: t => bool
107
107
  let isStopped: t => bool
108
108
  let epoch: t => int
109
109
  let pruneStaleEntityHistoryThrottler: t => Throttler.t
110
+ let simulateDeadInputTracker: t => option<SimulateDeadInputTracker.t>
110
111
 
111
112
  // Store domain operations.
112
113
  let queueProcessedBatch: (t, ~batch: Batch.t) => unit
@@ -0,0 +1,85 @@
1
+ // Lives on IndexerState and is fed each processed batch, so ChainState and the
2
+ // fetch loop carry no simulate-specific state.
3
+
4
+ // Match a provided item to a processed one by its (chain, block, logIndex)
5
+ // coordinate rather than object identity, so matching survives any copy or
6
+ // transform of the item between the source and the batch.
7
+ let itemKey = (item: Internal.item): string =>
8
+ switch item {
9
+ | Internal.Event({chain, blockNumber, logIndex}) =>
10
+ `${chain
11
+ ->ChainMap.Chain.toChainId
12
+ ->Int.toString}:${blockNumber->Int.toString}:${logIndex->Int.toString}`
13
+ | _ => "" // non-event items are never a provided simulate input
14
+ }
15
+
16
+ // `index` is the item's position in its chain's `simulate` array, reported back
17
+ // so a user finds it without echoing its fields.
18
+ type entry = {chainId: int, index: int, key: string}
19
+
20
+ type t = {mutable unprocessed: array<entry>}
21
+
22
+ let makeFromConfig = (config: Config.t): option<t> => {
23
+ let entries =
24
+ config.chainMap
25
+ ->ChainMap.values
26
+ ->Array.flatMap(chainConfig =>
27
+ switch chainConfig.sourceConfig {
28
+ | Config.CustomSources(sources) =>
29
+ sources->Array.flatMap(source =>
30
+ source.simulateItems
31
+ ->Option.getOr([])
32
+ ->Array.mapWithIndex(
33
+ (item, index) => {
34
+ chainId: chainConfig.id,
35
+ index,
36
+ key: itemKey(item),
37
+ },
38
+ )
39
+ )
40
+ | _ => []
41
+ }
42
+ )
43
+ switch entries {
44
+ | [] => None
45
+ | _ => Some({unprocessed: entries})
46
+ }
47
+ }
48
+
49
+ let recordProcessed = (t: t, ~batch: Batch.t) => {
50
+ let processedKeys = batch.items->Array.map(itemKey)->Utils.Set.fromArray
51
+ t.unprocessed = t.unprocessed->Array.filter(entry => !(processedKeys->Utils.Set.has(entry.key)))
52
+ }
53
+
54
+ // Unrouted item indices grouped by chain, in the order chains were first seen.
55
+ let unroutedByChain = (t: t): array<(int, array<int>)> => {
56
+ let indicesByChain = Dict.make()
57
+ let chainOrder = []
58
+ t.unprocessed->Array.forEach(entry => {
59
+ let key = entry.chainId->Int.toString
60
+ if indicesByChain->Dict.get(key)->Option.isNone {
61
+ chainOrder->Array.push(entry.chainId)->ignore
62
+ }
63
+ indicesByChain->Utils.Dict.push(key, entry.index)
64
+ })
65
+ chainOrder->Array.map(chainId => (chainId, indicesByChain->Dict.getUnsafe(chainId->Int.toString)))
66
+ }
67
+
68
+ let failureMessage = (t: t): option<string> =>
69
+ switch t->unroutedByChain {
70
+ | [] => None
71
+ | byChain =>
72
+ let count = byChain->Array.reduce(0, (acc, (_chainId, indices)) => acc + indices->Array.length)
73
+ let itemWord = count === 1 ? "item" : "items"
74
+ let lines =
75
+ byChain
76
+ ->Array.map(((chainId, indices)) =>
77
+ ` - chain ${chainId->Int.toString}: ${indices
78
+ ->Array.map(index => index->Int.toString)
79
+ ->Array.join(", ")}`
80
+ )
81
+ ->Array.join("\n")
82
+ Some(
83
+ `simulate: ${count->Int.toString} ${itemWord} you passed to simulate never reached a handler, so nothing ran for them. Each was filtered out before the handler — usually a non-wildcard srcAddress that isn't indexed for the contract, or a where/block filter that excluded the event. Unrouted items, by index in each chain's simulate array:\n${lines}`,
84
+ )
85
+ }