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
@@ -74,6 +74,55 @@ let getKnownRawBlockWithBackoff = async (
74
74
  }
75
75
  result.contents->Option.getOrThrow
76
76
  }
77
+ // Pulls the provider error message out of either a parsed Rpc.JsonRpcError or
78
+ // the raw napi JsExn shape (`error.error.message`), so classifiers below don't
79
+ // each have to re-derive it.
80
+ let getErrorMessage = (exn: exn): option<string> =>
81
+ switch exn {
82
+ | Rpc.JsonRpcError({message}) => Some(message)
83
+ | JsExn(error) =>
84
+ try {
85
+ let message: string = (error->Obj.magic)["error"]["message"]
86
+ message->S.assertOrThrow(S.string)
87
+ Some(message)
88
+ } catch {
89
+ | _ => None
90
+ }
91
+ | _ => None
92
+ }
93
+
94
+ // Deterministic "the range returned too much data" errors that carry no numeric
95
+ // block-range suggestion (HyperRPC's 50k-log cap, response-size and result-count
96
+ // limits). They depend on log density, not on a fixed block window, so waiting
97
+ // never helps — the same range always re-trips the same cap. The reaction is to
98
+ // shrink the range and retry immediately, ratcheting the max range down.
99
+ let isResponseTooLargeError = {
100
+ // Provider-specific "too many logs" messages, matched loosely since each names
101
+ // it differently: HyperRPC ("More than 50000 logs returned"), ZkEVM ("query
102
+ // returned more than N results"), LlamaRPC ("query exceeds max results"),
103
+ // 1RPC ("response size should not..."), Optimism ("(backend) response too
104
+ // large"), Arbitrum ("logs matched by query exceeds limit"), Ankr ("block
105
+ // range is too wide"). Kept as one block rather than per-line comments — the
106
+ // ReScript formatter doesn't preserve comments attached to regex literals in
107
+ // an array.
108
+ let patterns = [
109
+ /more than \d+ logs/i,
110
+ /\d+ logs returned/i,
111
+ /too many logs/i,
112
+ /query returned more than \d+ results/i,
113
+ /query exceeds max results/i,
114
+ /response size should not/i,
115
+ /(backend )?response too large/i,
116
+ /logs matched by query exceeds limit/i,
117
+ /block range is too wide/i,
118
+ ]
119
+ (exn: exn) =>
120
+ switch exn->getErrorMessage {
121
+ | Some(message) => patterns->Array.some(re => re->RegExp.test(message))
122
+ | None => false
123
+ }
124
+ }
125
+
77
126
  let getSuggestedBlockIntervalFromExn = {
78
127
  // Unknown provider: "retry with the range 123-456"
79
128
  let suggestedRangeRegExp = /retry with the range (\d+)-(\d+)/
@@ -118,14 +167,6 @@ let getSuggestedBlockIntervalFromExn = {
118
167
  // when we send request with numeric block range instead of hex
119
168
  // Infura, ZkSync: "Try with this block range [0x123,0x456]"
120
169
 
121
- // Future handling needed for these providers that don't suggest ranges:
122
- // - Ankr: "block range is too wide"
123
- // - 1RPC: "response size should not greater than 10000000 bytes"
124
- // - ZkEVM: "query returned more than 10000 results"
125
- // - LlamaRPC: "query exceeds max results"
126
- // - Optimism: "backend response too large" or "Block range is too large"
127
- // - Arbitrum: "logs matched by query exceeds limit of 10000"
128
-
129
170
  let parseMessageForBlockRange = (message: string) => {
130
171
  // Helper to extract block range from regex match
131
172
  let extractBlockRange = (execResult, ~isMaxRange) =>
@@ -209,17 +250,9 @@ let getSuggestedBlockIntervalFromExn = {
209
250
  // Whether it's the max range that the provider allows
210
251
  bool,
211
252
  )> =>
212
- switch exn {
213
- | Rpc.JsonRpcError({message}) => parseMessageForBlockRange(message)
214
- | JsExn(error) =>
215
- try {
216
- let message: string = (error->Obj.magic)["error"]["message"]
217
- message->S.assertOrThrow(S.string)
218
- parseMessageForBlockRange(message)
219
- } catch {
220
- | _ => None
221
- }
222
- | _ => None
253
+ switch exn->getErrorMessage {
254
+ | Some(message) => parseMessageForBlockRange(message)
255
+ | None => None
223
256
  }
224
257
  }
225
258
 
@@ -230,11 +263,38 @@ type eventBatchQuery = {
230
263
 
231
264
  let maxSuggestedBlockIntervalKey = "max"
232
265
 
266
+ let getSourceMaxBlockInterval = (mutSuggestedBlockIntervals, ~intervalCeiling) =>
267
+ mutSuggestedBlockIntervals
268
+ ->Utils.Dict.dangerouslyGetNonOption(maxSuggestedBlockIntervalKey)
269
+ ->Option.getOr(intervalCeiling)
270
+
271
+ type logSelection = {
272
+ addresses: option<array<Address.t>>,
273
+ topicQuery: Rpc.GetLogs.topicQuery,
274
+ }
275
+
276
+ // A log can satisfy more than one selection when a single event's `where` is an
277
+ // OR of param groups, so dedup the fanned-out responses by (blockNumber,
278
+ // logIndex) — unique per chain — keeping the first occurrence.
279
+ let mergeAndDedupItems = (itemsPerSelection: array<array<EvmRpcClient.rpcEventItem>>) => {
280
+ let seen = Utils.Set.make()
281
+ let merged = []
282
+ itemsPerSelection->Array.forEach(items =>
283
+ items->Array.forEach((item: EvmRpcClient.rpcEventItem) => {
284
+ let key = `${item.log.blockNumber->Int.toString}-${item.log.logIndex->Int.toString}`
285
+ if seen->Utils.Set.has(key)->not {
286
+ seen->Utils.Set.add(key)->ignore
287
+ merged->Array.push(item)->ignore
288
+ }
289
+ })
290
+ )
291
+ merged
292
+ }
293
+
233
294
  let getNextPage = (
234
295
  ~fromBlock,
235
296
  ~toBlock,
236
- ~addresses,
237
- ~topicQuery: Rpc.GetLogs.topicQuery,
297
+ ~logSelections: array<logSelection>,
238
298
  ~loadBlock,
239
299
  ~syncConfig as sc: Config.sourceSync,
240
300
  ~rpcClient: EvmRpcClient.t,
@@ -252,93 +312,111 @@ let getNextPage = (
252
312
  )
253
313
 
254
314
  let latestFetchedBlockPromise = loadBlock(toBlock)
255
- Prometheus.SourceRequestCount.increment(~sourceName, ~chainId, ~method="eth_getLogs")
256
- let logsPromise = rpcClient.getLogs({
257
- fromBlock,
258
- toBlock,
259
- ?addresses,
260
- topics: topicQuery->Array.map(filter =>
261
- switch filter {
262
- | Rpc.GetLogs.Null => Nullable.null
263
- | Single(topic) => Nullable.make([topic])
264
- | Multiple(topics) => Nullable.make(topics)
315
+
316
+ let queryLogs = ({addresses, topicQuery}: logSelection) => {
317
+ Prometheus.SourceRequestCount.increment(~sourceName, ~chainId, ~method="eth_getLogs")
318
+ rpcClient.getLogs({
319
+ fromBlock,
320
+ toBlock,
321
+ ?addresses,
322
+ topics: topicQuery->Array.map(filter =>
323
+ switch filter {
324
+ | Rpc.GetLogs.Null => Nullable.null
325
+ | Single(topic) => Nullable.make([topic])
326
+ | Multiple(topics) => Nullable.make(topics)
327
+ }
328
+ ),
329
+ })
330
+ }
331
+
332
+ let logsPromise = switch logSelections {
333
+ | [] =>
334
+ latestFetchedBlockPromise->Promise.thenResolve((latestFetchedBlockInfo): eventBatchQuery => {
335
+ items: [],
336
+ latestFetchedBlockInfo,
337
+ })
338
+ // Fast path: a single selection needs no cross-request merge or dedup.
339
+ | [logSelection] =>
340
+ logSelection
341
+ ->queryLogs
342
+ ->Promise.then(async items => {
343
+ {
344
+ items,
345
+ latestFetchedBlockInfo: await latestFetchedBlockPromise,
265
346
  }
266
- ),
267
- })->Promise.then(async items => {
268
- {
269
- items,
270
- latestFetchedBlockInfo: await latestFetchedBlockPromise,
271
- }
272
- })
347
+ })
348
+ | _ =>
349
+ logSelections
350
+ ->Array.map(queryLogs)
351
+ ->Promise.all
352
+ ->Promise.then(async itemsPerSelection => {
353
+ {
354
+ items: itemsPerSelection->mergeAndDedupItems,
355
+ latestFetchedBlockInfo: await latestFetchedBlockPromise,
356
+ }
357
+ })
358
+ }
273
359
 
274
360
  [queryTimoutPromise, logsPromise]
275
361
  ->Promise.race
276
362
  ->Promise.catch(err => {
363
+ let executedBlockInterval = toBlock - fromBlock + 1
364
+ let shrunkBlockInterval = Pervasives.max(
365
+ 1,
366
+ (executedBlockInterval->Int.toFloat *. sc.backoffMultiplicative)->Float.toInt,
367
+ )
368
+
369
+ let throwFailedGettingItems = retry =>
370
+ throw(Source.GetItemsError(FailedGettingItems({exn: err, attemptedToBlock: toBlock, retry})))
371
+ let throwResize = interval =>
372
+ throwFailedGettingItems(WithSuggestedToBlock({toBlock: fromBlock + interval - 1}))
373
+
277
374
  switch getSuggestedBlockIntervalFromExn(err) {
278
- | Some((nextBlockIntervalTry, isMaxRange)) =>
279
- mutSuggestedBlockIntervals->Dict.set(
280
- isMaxRange ? maxSuggestedBlockIntervalKey : partitionId,
281
- nextBlockIntervalTry,
282
- )
283
- throw(
284
- Source.GetItemsError(
285
- FailedGettingItems({
286
- exn: err,
287
- attemptedToBlock: toBlock,
288
- retry: WithSuggestedToBlock({
289
- toBlock: fromBlock + nextBlockIntervalTry - 1,
290
- }),
291
- }),
292
- ),
375
+ // "limited to N blocks" — a structural cap on the whole source; only tighten.
376
+ | Some((interval, true)) =>
377
+ let capped = Pervasives.min(
378
+ mutSuggestedBlockIntervals->getSourceMaxBlockInterval(~intervalCeiling=sc.intervalCeiling),
379
+ interval,
293
380
  )
381
+ mutSuggestedBlockIntervals->Dict.set(maxSuggestedBlockIntervalKey, capped)
382
+ throwResize(capped)
383
+ // A one-off suggested range ("retry with range X-Y") — apply to this partition.
384
+ | Some((interval, false)) =>
385
+ mutSuggestedBlockIntervals->Dict.set(partitionId, interval)
386
+ throwResize(interval)
387
+ // Density cap with no suggested number (too many logs / response too large):
388
+ // shrink THIS partition and retry immediately (no wait); acceleration
389
+ // re-adapts on the next successful query. The interval>1 guard avoids a
390
+ // no-progress tight loop on a single over-cap block.
391
+ | None if executedBlockInterval > 1 && err->isResponseTooLargeError =>
392
+ mutSuggestedBlockIntervals->Dict.set(partitionId, shrunkBlockInterval)
393
+ throwResize(shrunkBlockInterval)
394
+ // Transient/unknown — shrink this partition and back off.
294
395
  | None =>
295
- let executedBlockInterval = toBlock - fromBlock + 1
296
- let nextBlockIntervalTry =
297
- (executedBlockInterval->Int.toFloat *. sc.backoffMultiplicative)->Float.toInt
298
- mutSuggestedBlockIntervals->Dict.set(partitionId, nextBlockIntervalTry)
299
- throw(
300
- Source.GetItemsError(
301
- Source.FailedGettingItems({
302
- exn: err,
303
- attemptedToBlock: toBlock,
304
- retry: WithBackoff({
305
- message: `Failed getting data for the block range. Will try smaller block range for the next attempt.`,
306
- backoffMillis: sc.backoffMillis,
307
- }),
308
- }),
309
- ),
396
+ mutSuggestedBlockIntervals->Dict.set(partitionId, shrunkBlockInterval)
397
+ throwFailedGettingItems(
398
+ WithBackoff({
399
+ message: `Failed getting data for the block range. Will try smaller block range for the next attempt.`,
400
+ backoffMillis: sc.backoffMillis,
401
+ }),
310
402
  )
311
403
  }
312
404
  })
313
405
  }
314
406
 
315
- type logSelection = {
316
- addresses: option<array<Address.t>>,
317
- topicQuery: Rpc.GetLogs.topicQuery,
318
- }
319
-
320
407
  type selectionConfig = {
321
- getLogSelectionOrThrow: (~addressesByContractName: dict<array<Address.t>>) => logSelection,
408
+ getLogSelectionsOrThrow: (
409
+ ~addressesByContractName: dict<array<Address.t>>,
410
+ ) => array<logSelection>,
322
411
  }
323
412
 
324
413
  let getSelectionConfig = (selection: FetchState.selection, ~chain) => {
325
- let staticTopicSelections = []
326
- let dynamicEventFilters = []
327
-
328
- selection.eventConfigs
329
- ->(Utils.magic: array<Internal.eventConfig> => array<Internal.evmEventConfig>)
330
- ->Array.forEach(({getEventFiltersOrThrow}) => {
331
- switch getEventFiltersOrThrow(chain) {
332
- | Static(s) => staticTopicSelections->Array.pushMany(s)->ignore
333
- | Dynamic(fn) => dynamicEventFilters->Array.push(fn)->ignore
334
- }
335
- })
414
+ let evmEventConfigs =
415
+ selection.eventConfigs->(
416
+ Utils.magic: array<Internal.eventConfig> => array<Internal.evmEventConfig>
417
+ )
336
418
 
337
- let getLogSelectionOrThrow = switch (
338
- staticTopicSelections->LogSelection.compressTopicSelections,
339
- dynamicEventFilters,
340
- ) {
341
- | ([], []) =>
419
+ if evmEventConfigs->Utils.Array.isEmpty {
342
420
  throw(
343
421
  Source.GetItemsError(
344
422
  UnsupportedSelection({
@@ -346,48 +424,110 @@ let getSelectionConfig = (selection: FetchState.selection, ~chain) => {
346
424
  }),
347
425
  ),
348
426
  )
349
- | ([topicSelection], []) => {
350
- let topicQuery = topicSelection->Rpc.GetLogs.mapTopicQuery
351
- (~addressesByContractName) => {
352
- addresses: switch addressesByContractName->FetchState.addressesByContractNameGetAll {
353
- | [] => None
354
- | addresses => Some(addresses)
355
- },
356
- topicQuery,
427
+ }
428
+
429
+ // eth_getLogs takes one address list and one topic selection per request, so
430
+ // fan out to one request per selection. Each address-bound event is grouped by
431
+ // its contract and later scoped to that contract's own addresses — pooling all
432
+ // contracts' addresses would let one contract's query fetch a sibling's logs,
433
+ // which route back by address and bypass the sibling's filter (routing never
434
+ // re-applies it). Pure-wildcard events carry no address constraint, so they're
435
+ // pooled and resolved once.
436
+ let noAddressTopicSelections = []
437
+ let staticByContract = Dict.make()
438
+ let dynamicByContract = Dict.make()
439
+ let dynamicWildcardByContract = Dict.make()
440
+ let contractNames = Utils.Set.make()
441
+
442
+ evmEventConfigs->Array.forEach(({
443
+ contractName,
444
+ isWildcard,
445
+ dependsOnAddresses,
446
+ getEventFiltersOrThrow,
447
+ }) => {
448
+ let eventFilters = getEventFiltersOrThrow(chain)
449
+ if dependsOnAddresses {
450
+ contractNames->Utils.Set.add(contractName)->ignore
451
+ switch eventFilters {
452
+ | Internal.Static(topicSelections) =>
453
+ staticByContract->Utils.Dict.pushMany(contractName, topicSelections)
454
+ | Dynamic(fn) =>
455
+ (isWildcard ? dynamicWildcardByContract : dynamicByContract)->Utils.Dict.push(
456
+ contractName,
457
+ fn,
458
+ )
357
459
  }
460
+ } else {
461
+ noAddressTopicSelections
462
+ ->Array.pushMany(
463
+ switch eventFilters {
464
+ | Static(s) => s
465
+ | Dynamic(fn) => fn([])
466
+ },
467
+ )
468
+ ->ignore
358
469
  }
359
- | ([], [dynamicEventFilter]) if selection.eventConfigs->Array.length === 1 =>
360
- let eventConfig = selection.eventConfigs->Utils.Array.firstUnsafe
470
+ })
361
471
 
362
- (~addressesByContractName) => {
363
- let addresses = addressesByContractName->FetchState.addressesByContractNameGetAll
364
- {
365
- addresses: eventConfig.isWildcard ? None : Some(addresses),
366
- topicQuery: switch dynamicEventFilter(addresses) {
367
- | [topicSelection] => topicSelection->Rpc.GetLogs.mapTopicQuery
368
- | _ =>
369
- throw(
370
- Source.GetItemsError(
371
- UnsupportedSelection({
372
- message: "RPC data-source currently doesn't support an array of event filters. Please, create a GitHub issue if it's a blocker for you.",
373
- }),
374
- ),
375
- )
376
- },
377
- }
472
+ // `compressTopicSelections` folds the filter-less events into a single topic0
473
+ // OR-set, keeping the common case at one request.
474
+ let toLogSelections = (~addresses, topicSelections): array<logSelection> =>
475
+ topicSelections
476
+ ->LogSelection.compressTopicSelections
477
+ ->Array.map(topicSelection => {
478
+ addresses,
479
+ topicQuery: topicSelection->Rpc.GetLogs.mapTopicQuery,
480
+ })
481
+
482
+ // Address-independent, so resolve once (the wildcard partition reuses this).
483
+ let noAddressLogSelections = toLogSelections(~addresses=None, noAddressTopicSelections)
484
+
485
+ let getLogSelectionsOrThrow = if contractNames->Utils.Set.size === 0 {
486
+ (~addressesByContractName as _) => noAddressLogSelections
487
+ } else {
488
+ (~addressesByContractName): array<logSelection> => {
489
+ let logSelections = noAddressLogSelections->Array.copy
490
+ contractNames->Utils.Set.forEach(contractName => {
491
+ switch addressesByContractName->Utils.Dict.dangerouslyGetNonOption(contractName) {
492
+ | None
493
+ | Some([]) => ()
494
+ | Some(addresses) =>
495
+ // Static + dynamic non-wildcard filters, scoped to this contract's addresses.
496
+ let addressedTopicSelections = []
497
+ switch staticByContract->Utils.Dict.dangerouslyGetNonOption(contractName) {
498
+ | Some(s) => addressedTopicSelections->Array.pushMany(s)->ignore
499
+ | None => ()
500
+ }
501
+ switch dynamicByContract->Utils.Dict.dangerouslyGetNonOption(contractName) {
502
+ | Some(fns) =>
503
+ fns->Array.forEach(fn =>
504
+ addressedTopicSelections->Array.pushMany(fn(addresses))->ignore
505
+ )
506
+ | None => ()
507
+ }
508
+ logSelections
509
+ ->Array.pushMany(toLogSelections(~addresses=Some(addresses), addressedTopicSelections))
510
+ ->ignore
511
+
512
+ // Dynamic wildcard-by-address filters fold the address into the topics,
513
+ // so they still match any address.
514
+ switch dynamicWildcardByContract->Utils.Dict.dangerouslyGetNonOption(contractName) {
515
+ | Some(fns) =>
516
+ logSelections
517
+ ->Array.pushMany(
518
+ toLogSelections(~addresses=None, fns->Array.flatMap(fn => fn(addresses))),
519
+ )
520
+ ->ignore
521
+ | None => ()
522
+ }
523
+ }
524
+ })
525
+ logSelections
378
526
  }
379
- | _ =>
380
- throw(
381
- Source.GetItemsError(
382
- UnsupportedSelection({
383
- message: "RPC data-source currently supports event filters only when there's a single wildcard event. Please, create a GitHub issue if it's a blocker for you.",
384
- }),
385
- ),
386
- )
387
527
  }
388
528
 
389
529
  {
390
- getLogSelectionOrThrow: getLogSelectionOrThrow,
530
+ getLogSelectionsOrThrow: getLogSelectionsOrThrow,
391
531
  }
392
532
  }
393
533
 
@@ -886,6 +1026,10 @@ let make = (
886
1026
 
887
1027
  let getSelectionConfig = memoGetSelectionConfig(~chain)
888
1028
 
1029
+ // Per-partition adaptive block interval (AIMD), keyed by partitionId. The
1030
+ // `max` key holds a source-wide ceiling that only ever tightens, set by
1031
+ // structural provider limits ("limited to N blocks"). A partition's own entry
1032
+ // can go stale when partitions merge/split — acceptable, it re-adapts.
889
1033
  let mutSuggestedBlockIntervals = Dict.make()
890
1034
 
891
1035
  let client = Rpc.makeClient(url, ~headers?)
@@ -1018,20 +1162,22 @@ let make = (
1018
1162
  ~knownHeight,
1019
1163
  ~partitionId,
1020
1164
  ~selection: FetchState.selection,
1165
+ ~itemsTarget as _,
1021
1166
  ~retry,
1022
1167
  ~logger as _,
1023
1168
  ) => {
1024
1169
  let startFetchingBatchTimeRef = Performance.now()
1025
1170
 
1026
- let suggestedBlockInterval = switch mutSuggestedBlockIntervals->Utils.Dict.dangerouslyGetNonOption(
1027
- maxSuggestedBlockIntervalKey,
1028
- ) {
1029
- | Some(maxSuggestedBlockInterval) => maxSuggestedBlockInterval
1030
- | None =>
1171
+ let sourceMaxBlockInterval =
1172
+ mutSuggestedBlockIntervals->getSourceMaxBlockInterval(
1173
+ ~intervalCeiling=syncConfig.intervalCeiling,
1174
+ )
1175
+ let suggestedBlockInterval = Pervasives.min(
1031
1176
  mutSuggestedBlockIntervals
1032
1177
  ->Utils.Dict.dangerouslyGetNonOption(partitionId)
1033
- ->Option.getOr(syncConfig.initialBlockInterval)
1034
- }
1178
+ ->Option.getOr(syncConfig.initialBlockInterval),
1179
+ sourceMaxBlockInterval,
1180
+ )
1035
1181
 
1036
1182
  // Always have a toBlock for an RPC worker
1037
1183
  let toBlock = switch toBlock {
@@ -1050,14 +1196,13 @@ let make = (
1050
1196
  ->Promise.thenResolve(json => Some(parseBlockInfo(json)))
1051
1197
  : Promise.resolve(None)
1052
1198
 
1053
- let {getLogSelectionOrThrow} = getSelectionConfig(selection)
1054
- let {addresses, topicQuery} = getLogSelectionOrThrow(~addressesByContractName)
1199
+ let {getLogSelectionsOrThrow} = getSelectionConfig(selection)
1200
+ let logSelections = getLogSelectionsOrThrow(~addressesByContractName)
1055
1201
 
1056
1202
  let {items, latestFetchedBlockInfo} = await getNextPage(
1057
1203
  ~fromBlock,
1058
1204
  ~toBlock=suggestedToBlock,
1059
- ~addresses,
1060
- ~topicQuery,
1205
+ ~logSelections,
1061
1206
  ~loadBlock=blockNumber =>
1062
1207
  blockLoader.contents
1063
1208
  ->LazyLoader.get(blockNumber)
@@ -1072,20 +1217,16 @@ let make = (
1072
1217
 
1073
1218
  let executedBlockInterval = suggestedToBlock - fromBlock + 1
1074
1219
 
1075
- // Increase the suggested block interval only when it was actually applied
1076
- // and we didn't query to a hard toBlock
1077
- // We also don't care about it when we have a hard max block interval
1078
- if (
1079
- executedBlockInterval >= suggestedBlockInterval &&
1080
- !(mutSuggestedBlockIntervals->Dict.has(maxSuggestedBlockIntervalKey))
1081
- ) {
1082
- // Increase batch size going forward, but do not increase past a configured maximum
1083
- // See: https://en.wikipedia.org/wiki/Additive_increase/multiplicative_decrease
1220
+ // Grow this partition's interval only when the full suggested range was
1221
+ // actually applied (not clamped by a hard toBlock). The min clamps to the
1222
+ // source-wide ceiling, which also stops growth once a structural cap tightened it.
1223
+ // See: https://en.wikipedia.org/wiki/Additive_increase/multiplicative_decrease
1224
+ if executedBlockInterval >= suggestedBlockInterval {
1084
1225
  mutSuggestedBlockIntervals->Dict.set(
1085
1226
  partitionId,
1086
1227
  Pervasives.min(
1087
1228
  executedBlockInterval + syncConfig.accelerationAdditive,
1088
- syncConfig.intervalCeiling,
1229
+ sourceMaxBlockInterval,
1089
1230
  ),
1090
1231
  )
1091
1232
  }