envio 3.5.0-alpha.3 → 3.5.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/package.json +6 -6
  2. package/src/Batch.res +8 -8
  3. package/src/Batch.res.mjs +4 -3
  4. package/src/BatchProcessing.res +17 -7
  5. package/src/BatchProcessing.res.mjs +8 -4
  6. package/src/ChainFetching.res +20 -19
  7. package/src/ChainFetching.res.mjs +14 -14
  8. package/src/ChainId.res +58 -0
  9. package/src/ChainId.res.mjs +49 -0
  10. package/src/ChainId.resi +27 -0
  11. package/src/ChainMap.res +12 -26
  12. package/src/ChainMap.res.mjs +5 -41
  13. package/src/ChainMap.resi +5 -20
  14. package/src/ChainMetadata.res +1 -1
  15. package/src/ChainMetadata.res.mjs +3 -2
  16. package/src/ChainState.res +35 -20
  17. package/src/ChainState.res.mjs +34 -11
  18. package/src/ChainState.resi +2 -0
  19. package/src/Config.res +41 -24
  20. package/src/Config.res.mjs +17 -14
  21. package/src/ContractRegisterContext.res +1 -1
  22. package/src/ContractRegisterContext.res.mjs +1 -1
  23. package/src/Core.res +1 -0
  24. package/src/CrossChainState.res +61 -16
  25. package/src/CrossChainState.res.mjs +37 -13
  26. package/src/CrossChainState.resi +3 -2
  27. package/src/Env.res +0 -3
  28. package/src/Env.res.mjs +0 -3
  29. package/src/EventConfigBuilder.res +1 -1
  30. package/src/EventProcessing.res +4 -4
  31. package/src/EventProcessing.res.mjs +5 -5
  32. package/src/FetchState.res +261 -89
  33. package/src/FetchState.res.mjs +187 -63
  34. package/src/FinalizeBackfill.res +33 -6
  35. package/src/FinalizeBackfill.res.mjs +19 -1
  36. package/src/HandlerRegister.res +8 -8
  37. package/src/HandlerRegister.res.mjs +5 -4
  38. package/src/HandlerRegister.resi +1 -1
  39. package/src/InMemoryStore.res +4 -0
  40. package/src/InMemoryStore.res.mjs +1 -1
  41. package/src/IndexerLoop.res +9 -2
  42. package/src/IndexerLoop.res.mjs +8 -1
  43. package/src/IndexerState.res +41 -24
  44. package/src/IndexerState.res.mjs +36 -17
  45. package/src/IndexerState.resi +9 -7
  46. package/src/Internal.res +19 -17
  47. package/src/Internal.res.mjs +18 -9
  48. package/src/LoadLayer.res +1 -1
  49. package/src/LoadLayer.res.mjs +2 -1
  50. package/src/LogSelection.res +2 -2
  51. package/src/Main.res +7 -11
  52. package/src/Main.res.mjs +4 -6
  53. package/src/Metrics.res +6 -6
  54. package/src/Metrics.res.mjs +4 -3
  55. package/src/Persistence.res +15 -7
  56. package/src/PgStorage.res +284 -258
  57. package/src/PgStorage.res.mjs +181 -208
  58. package/src/RawEvent.res +1 -2
  59. package/src/RawEvent.res.mjs +1 -1
  60. package/src/Rollback.res +18 -20
  61. package/src/Rollback.res.mjs +9 -9
  62. package/src/RollbackCommit.res +2 -2
  63. package/src/RollbackCommit.res.mjs +3 -3
  64. package/src/SafeCheckpointTracking.res +2 -2
  65. package/src/SimulateDeadInputTracker.res +17 -21
  66. package/src/SimulateDeadInputTracker.res.mjs +9 -8
  67. package/src/SimulateItems.res +8 -10
  68. package/src/SimulateItems.res.mjs +9 -10
  69. package/src/Sink.res +2 -2
  70. package/src/Sink.res.mjs +3 -2
  71. package/src/TestIndexer.res +11 -14
  72. package/src/TestIndexer.res.mjs +14 -10
  73. package/src/UserContext.res +2 -2
  74. package/src/UserContext.res.mjs +2 -1
  75. package/src/Utils.res +0 -4
  76. package/src/Utils.res.mjs +1 -6
  77. package/src/bindings/ClickHouse.res +20 -2
  78. package/src/bindings/ClickHouse.res.mjs +25 -14
  79. package/src/db/EntityHistory.res +1 -1
  80. package/src/db/IndexCatalog.res +248 -0
  81. package/src/db/IndexCatalog.res.mjs +222 -0
  82. package/src/db/IndexDefinition.res +131 -0
  83. package/src/db/IndexDefinition.res.mjs +138 -0
  84. package/src/db/IndexManager.res +145 -0
  85. package/src/db/IndexManager.res.mjs +130 -0
  86. package/src/db/InternalTable.res +50 -26
  87. package/src/db/InternalTable.res.mjs +32 -24
  88. package/src/db/Table.res +11 -1
  89. package/src/db/Table.res.mjs +9 -4
  90. package/src/sources/AddressSet.res +5 -4
  91. package/src/sources/AddressStore.res +25 -14
  92. package/src/sources/AddressStore.res.mjs +10 -12
  93. package/src/sources/Evm.res +2 -2
  94. package/src/sources/Evm.res.mjs +1 -1
  95. package/src/sources/EvmChain.res +3 -3
  96. package/src/sources/EvmChain.res.mjs +3 -3
  97. package/src/sources/EvmHyperSyncSource.res +7 -7
  98. package/src/sources/EvmHyperSyncSource.res.mjs +6 -6
  99. package/src/sources/Fuel.res +2 -2
  100. package/src/sources/Fuel.res.mjs +1 -1
  101. package/src/sources/FuelHyperSyncClient.res +3 -0
  102. package/src/sources/FuelHyperSyncClient.res.mjs +1 -0
  103. package/src/sources/FuelHyperSyncSource.res +5 -7
  104. package/src/sources/FuelHyperSyncSource.res.mjs +5 -5
  105. package/src/sources/HyperSync.resi +2 -2
  106. package/src/sources/HyperSyncClient.res +5 -0
  107. package/src/sources/HyperSyncClient.res.mjs +1 -0
  108. package/src/sources/RpcSource.res +13 -14
  109. package/src/sources/RpcSource.res.mjs +13 -12
  110. package/src/sources/SimulateSource.res +42 -15
  111. package/src/sources/SimulateSource.res.mjs +27 -9
  112. package/src/sources/Source.res +1 -5
  113. package/src/sources/SourceManager.res +12 -12
  114. package/src/sources/SourceManager.res.mjs +11 -10
  115. package/src/sources/SourceManager.resi +2 -2
  116. package/src/sources/Svm.res +4 -5
  117. package/src/sources/Svm.res.mjs +5 -4
  118. package/src/sources/SvmHyperSyncClient.res +3 -0
  119. package/src/sources/SvmHyperSyncClient.res.mjs +1 -0
  120. package/src/sources/SvmHyperSyncSource.res +4 -4
  121. package/src/sources/SvmHyperSyncSource.res.mjs +3 -3
  122. package/src/tui/Tui.res +1 -1
  123. package/src/tui/Tui.res.mjs +2 -1
  124. package/src/tui/components/CustomHooks.res +5 -4
  125. package/src/tui/components/CustomHooks.res.mjs +4 -2
  126. package/src/db/IndexRegistry.res +0 -219
  127. package/src/db/IndexRegistry.res.mjs +0 -195
@@ -19,6 +19,42 @@ type selection = {
19
19
  // gates each item against the chain's address store instead of the
20
20
  // partition's set. Absent for normal partitions.
21
21
  clientFilteredContracts?: array<string>,
22
+ // The earliest block any of these registrations can produce an item at.
23
+ // Derived once here, by `makeSelection`, because `getNextQuery` reads it for
24
+ // every partition on every tick and a chain can hold hundreds of them.
25
+ //
26
+ // Absent when a registration is unrestricted and so can fire from the chain
27
+ // start — which is also the only case where nothing can be skipped, so absent
28
+ // and "block 0" are the same answer here. Build selections through
29
+ // `makeSelection` rather than as literals, or this goes stale.
30
+ startBlock?: int,
31
+ }
32
+
33
+ // The earliest block any of the registrations can produce an item at, or `None`
34
+ // when one of them is unrestricted. Nothing below it is worth querying.
35
+ let deriveSelectionStartBlock = (onEventRegistrations: array<Internal.onEventRegistration>) => {
36
+ let earliest = ref(None)
37
+ let idx = ref(0)
38
+ let unrestricted = ref(onEventRegistrations->Utils.Array.isEmpty)
39
+ while !unrestricted.contents && idx.contents < onEventRegistrations->Array.length {
40
+ switch (onEventRegistrations->Array.getUnsafe(idx.contents)).startBlock {
41
+ | None => unrestricted := true
42
+ | Some(startBlock) =>
43
+ switch earliest.contents {
44
+ | Some(current) if current <= startBlock => ()
45
+ | _ => earliest := Some(startBlock)
46
+ }
47
+ }
48
+ idx := idx.contents + 1
49
+ }
50
+ unrestricted.contents ? None : earliest.contents
51
+ }
52
+
53
+ let makeSelection = (~onEventRegistrations, ~dependsOnAddresses, ~clientFilteredContracts=?) => {
54
+ onEventRegistrations,
55
+ dependsOnAddresses,
56
+ ?clientFilteredContracts,
57
+ startBlock: ?deriveSelectionStartBlock(onEventRegistrations),
22
58
  }
23
59
 
24
60
  type pendingQuery = {
@@ -100,6 +136,43 @@ type query = {
100
136
 
101
137
  let withAddresses = (p: partition, addresses: AddressSet.t) => {...p, addresses}
102
138
 
139
+ // The selection a query over [fromBlock, toBlock] actually needs: a registration
140
+ // whose own start block sits past the range can't produce an item there, so
141
+ // leaving it out keeps the source from asking the server for its logs at all.
142
+ // The address gate can't express this — it's contract-wide, so an unrestricted
143
+ // sibling holds it open from the chain start for every registration alike.
144
+ //
145
+ // Returns the selection as-is when nothing is dropped (the common case), and
146
+ // for an open-ended query, which may reach any block and so can exclude nothing.
147
+ // Also when every registration would be dropped: `selectionStartBlock` keeps a
148
+ // partition's cursor at or above its earliest start block, so a query below all
149
+ // of them shouldn't exist — and an empty selection is one no source can build.
150
+ let narrowSelectionToRange = (selection: selection, ~toBlock) =>
151
+ switch toBlock {
152
+ | None => selection
153
+ | Some(toBlock) =>
154
+ let inRange = selection.onEventRegistrations->Array.filter(reg =>
155
+ switch reg.startBlock {
156
+ | Some(startBlock) => startBlock <= toBlock
157
+ | None => true
158
+ }
159
+ )
160
+ switch inRange->Array.length {
161
+ // Every registration starts above the range. Narrowing to nothing would
162
+ // build a selection with no log filters, which each source reads as
163
+ // "select everything" — the opposite of the intent. The partition is
164
+ // queried unnarrowed instead and the routers drop what hasn't started.
165
+ | 0 => selection
166
+ | length if length === selection.onEventRegistrations->Array.length => selection
167
+ | _ =>
168
+ makeSelection(
169
+ ~onEventRegistrations=inRange,
170
+ ~dependsOnAddresses=selection.dependsOnAddresses,
171
+ ~clientFilteredContracts=?selection.clientFilteredContracts,
172
+ )
173
+ }
174
+ }
175
+
103
176
  // itemsEst for a query over [fromBlock, toBlock] at the given event density
104
177
  // (items/block). toBlock None is the open-ended tail, capped at
105
178
  // chainTargetBlock — the soft per-tick horizon the owning chain wants to reach
@@ -127,6 +200,14 @@ let getMinHistoryRange = (p: partition) => {
127
200
  // source-capacity history, including when an itemsTarget cap truncates a response.
128
201
  let getTrustedDensity = (p: partition) => p.eventDensity
129
202
 
203
+ // A response is still owed for this partition, so it owns range nothing else
204
+ // accounts for: removing it would let the frontier advance over blocks the
205
+ // response can still deliver items from, and orphan the response itself.
206
+ // Narrower than a non-empty queue on purpose — a settled query parked behind a
207
+ // gap is data already in hand, and waiting on it would wedge a partition that
208
+ // no longer queries.
209
+ let isFetching = (p: partition) => p.mutPendingQueries->Array.some(pq => pq.fetchedBlock === None)
210
+
130
211
  let getMinQueryRange = (partitions: array<partition>) => {
131
212
  let min = ref(0)
132
213
  for i in 0 to partitions->Array.length - 1 {
@@ -362,9 +443,10 @@ module OptimizedPartitions = {
362
443
  // A partition already at or past its merge block is done — normally
363
444
  // handleQueryResponse removes it when the response lands, but a rollback
364
445
  // can cap the merge block at the rolled-back frontier, leaving no
365
- // response to ever remove it on.
446
+ // response to ever remove it on. A retired partition still awaiting a
447
+ // response stays until it lands.
366
448
  | {mergeBlock: Some(mergeBlock)} =>
367
- if p.latestFetchedBlock.blockNumber < mergeBlock {
449
+ if p.latestFetchedBlock.blockNumber < mergeBlock || p->isFetching {
368
450
  newPartitions->Array.push(p)->ignore
369
451
  }
370
452
  // Since it's not a dynamic contract partition,
@@ -487,6 +569,12 @@ module OptimizedPartitions = {
487
569
  | Some(anchorSafeBlock) =>
488
570
  if p.latestFetchedBlock.blockNumber < anchorSafeBlock {
489
571
  anchored->Array.push({...p, mergeBlock: Some(anchorSafeBlock)})->ignore
572
+ } else if p->isFetching {
573
+ // Caught up, but a response is still owed: keep it until that
574
+ // lands rather than dropping the range it owns.
575
+ anchored
576
+ ->Array.push({...p, mergeBlock: Some(p.latestFetchedBlock.blockNumber)})
577
+ ->ignore
490
578
  }
491
579
  }
492
580
  | _ => anchored->Array.push(p)->ignore
@@ -565,6 +653,10 @@ module OptimizedPartitions = {
565
653
  }
566
654
  }
567
655
 
656
+ // Every path that stops a partition from fetching keeps it until its last
657
+ // response lands, so a response always has a partition to advance. A rollback
658
+ // does delete partitions mid-run, but it bumps the indexer epoch and
659
+ // ChainFetching drops older responses before they reach here.
568
660
  let rec handleQueryResponse = (
569
661
  optimizedPartitions: t,
570
662
  ~query,
@@ -572,21 +664,13 @@ module OptimizedPartitions = {
572
664
  ~itemsCount,
573
665
  ~latestFetchedBlock: blockNumberAndTimestamp,
574
666
  ) =>
575
- switch optimizedPartitions.entities->Utils.Dict.dangerouslyGetNonOption(query.partitionId) {
576
- // The partition was absorbed into the address-free client-filtered partition
577
- // while this query was in flight. Its items are still merged into the buffer
578
- // by the caller (and deduped); there's no partition bookkeeping left to do,
579
- // and its reservation is released by ChainState regardless.
580
- | None => optimizedPartitions
581
- | Some(p) =>
582
- optimizedPartitions->handleQueryResponseForPartition(
583
- ~p,
584
- ~query,
585
- ~knownHeight,
586
- ~itemsCount,
587
- ~latestFetchedBlock,
588
- )
589
- }
667
+ optimizedPartitions->handleQueryResponseForPartition(
668
+ ~p=optimizedPartitions->getOrThrow(~partitionId=query.partitionId),
669
+ ~query,
670
+ ~knownHeight,
671
+ ~itemsCount,
672
+ ~latestFetchedBlock,
673
+ )
590
674
 
591
675
  and handleQueryResponseForPartition = (
592
676
  optimizedPartitions: t,
@@ -659,11 +743,15 @@ module OptimizedPartitions = {
659
743
  ~initialLatestFetchedBlock=p.latestFetchedBlock,
660
744
  )
661
745
 
662
- // Check if partition reached its mergeBlock and should be removed
663
- let partitionReachedMergeBlock = switch p.mergeBlock {
664
- | Some(mergeBlock) => updatedLatestFetchedBlock.blockNumber >= mergeBlock
665
- | None => false
666
- }
746
+ // Check if partition reached its mergeBlock and should be removed. A
747
+ // retired partition can hold several queries at once, so it goes only when
748
+ // the last of them has landed.
749
+ let partitionReachedMergeBlock =
750
+ switch p.mergeBlock {
751
+ | Some(mergeBlock) => updatedLatestFetchedBlock.blockNumber >= mergeBlock
752
+ | None => false
753
+ } &&
754
+ !(p->isFetching)
667
755
 
668
756
  if partitionReachedMergeBlock {
669
757
  mutEntities->Utils.Dict.deleteInPlace(p.id)
@@ -745,7 +833,7 @@ type t = {
745
833
  endBlock: option<int>,
746
834
  normalSelection: selection,
747
835
  // Not used for logic - only metadata
748
- chainId: int,
836
+ chainId: ChainId.t,
749
837
  // The block number of the latest block which was added to the queue
750
838
  // by the onBlock configs
751
839
  // Need a separate pointer for this
@@ -1090,9 +1178,10 @@ let claimedFetchedBlock = (p: partition, ~knownHeight) =>
1090
1178
  // fetching without tearing down established state:
1091
1179
  // - The standing address-free partition (no mergeBlock) keeps its id, frontier,
1092
1180
  // in-flight queries and learned density. Only when a newly-switched contract
1093
- // must be added does its selection change — under a fresh id, so in-flight
1094
- // responses built from the old selection are orphaned instead of advancing
1095
- // the frontier past ranges the new contract wasn't fetched for.
1181
+ // must be added does its selection change — under a fresh id, so responses
1182
+ // built from the old selection can't advance the frontier past ranges the new
1183
+ // contract wasn't fetched for. The old generation is retired beside it,
1184
+ // keeping the queue those responses belong to.
1096
1185
  // - Partitions absorbed below the standing partition's claimed block
1097
1186
  // (single-contract dynamic partitions and config partitions all of whose
1098
1187
  // contracts are client-filtered, plus any prior backfill) become one bounded
@@ -1137,6 +1226,21 @@ let collapseClientFilteredContracts = (
1137
1226
  let strippedFrontiers = []
1138
1227
  let anchoredContracts = partitions->OptimizedPartitions.anchoredContracts
1139
1228
 
1229
+ // Retire a partition in place of removing it: same id and selection, same
1230
+ // pending queue, capped at its own frontier so it issues nothing more. It
1231
+ // holds the frontier down and owns its in-flight response until that lands,
1232
+ // then handleQueryResponse drops it.
1233
+ let retire = (p: partition) => {
1234
+ ...p,
1235
+ mergeBlock: Some(p.latestFetchedBlock.blockNumber),
1236
+ }
1237
+ let absorb = p => {
1238
+ absorbedPartitions->Array.push(p)->ignore
1239
+ if p->isFetching {
1240
+ kept->Array.push(p->retire)->ignore
1241
+ }
1242
+ }
1243
+
1140
1244
  partitions->Array.forEach(p =>
1141
1245
  switch p {
1142
1246
  | {selection: {dependsOnAddresses: false}, mergeBlock: None}
@@ -1144,7 +1248,14 @@ let collapseClientFilteredContracts = (
1144
1248
  standingRef := Some(p)
1145
1249
  | {selection: {dependsOnAddresses: false}, mergeBlock: Some(_)} =>
1146
1250
  backfills->Array.push(p)->ignore
1147
- | {selection: {dependsOnAddresses: false}} => absorbedPartitions->Array.push(p)->ignore
1251
+
1252
+ // The backfill this call builds covers its remaining range from the
1253
+ // same min frontier, so it is superseded — but not before the response
1254
+ // it is waiting on lands.
1255
+ if p->isFetching {
1256
+ kept->Array.push(p->retire)->ignore
1257
+ }
1258
+ | {selection: {dependsOnAddresses: false}} => p->absorb
1148
1259
  | _ =>
1149
1260
  let contractNames = p.addresses->AddressSet.contractNames
1150
1261
  let serverSideNames =
@@ -1163,7 +1274,7 @@ let collapseClientFilteredContracts = (
1163
1274
  // hand that job back to a guessed ceiling.
1164
1275
  kept->Array.push(p)->ignore
1165
1276
  } else if serverSideNames->Utils.Array.isEmpty {
1166
- absorbedPartitions->Array.push(p)->ignore
1277
+ p->absorb
1167
1278
  } else {
1168
1279
  strippedFrontiers->Array.push(p.latestFetchedBlock)->ignore
1169
1280
  kept
@@ -1218,11 +1329,11 @@ let collapseClientFilteredContracts = (
1218
1329
  clientFilteredContracts->Utils.Set.has(reg.eventConfig.contractName)
1219
1330
  ),
1220
1331
  )
1221
- let newSelection = {
1222
- dependsOnAddresses: false,
1223
- onEventRegistrations: regByIndex->Dict.valuesToArray,
1224
- clientFilteredContracts: clientFilteredContracts->Utils.Set.toArray,
1225
- }
1332
+ let newSelection = makeSelection(
1333
+ ~dependsOnAddresses=false,
1334
+ ~onEventRegistrations=regByIndex->Dict.valuesToArray,
1335
+ ~clientFilteredContracts=clientFilteredContracts->Utils.Set.toArray,
1336
+ )
1226
1337
 
1227
1338
  switch standingRef.contents {
1228
1339
  | None =>
@@ -1256,6 +1367,15 @@ let collapseClientFilteredContracts = (
1256
1367
  let standingOut = if selectionChanged {
1257
1368
  let id = nextPartitionIndexRef.contents->Int.toString
1258
1369
  nextPartitionIndexRef := nextPartitionIndexRef.contents + 1
1370
+
1371
+ // A response built from the old selection must not advance the new
1372
+ // one's frontier over ranges the newly-switched contract wasn't
1373
+ // fetched for, so the continuing partition takes a fresh id and an
1374
+ // empty queue. The old generation is retired beside it rather than
1375
+ // dropped: it keeps the queue those responses belong to.
1376
+ if standing->isFetching {
1377
+ kept->Array.push(standing->retire)->ignore
1378
+ }
1259
1379
  {...standing, id, selection: newSelection, mutPendingQueries: []}
1260
1380
  } else {
1261
1381
  standing
@@ -1297,6 +1417,65 @@ let collapseClientFilteredContracts = (
1297
1417
  }
1298
1418
  }
1299
1419
 
1420
+ let warnAddressRegistration = (
1421
+ ~chainId: ChainId.t,
1422
+ ~contractAddress: Address.t,
1423
+ ~params,
1424
+ message,
1425
+ ) =>
1426
+ Logging.createChild(
1427
+ ~params={
1428
+ "chainId": chainId,
1429
+ "contractAddress": contractAddress->Address.toString,
1430
+ "details": params,
1431
+ },
1432
+ )->Logging.childWarn(message)
1433
+
1434
+ // A rejected registration is simply absent from every partition, so without a
1435
+ // warning the user sees a contract that never indexes and nothing saying why.
1436
+ // Shared by config-time registration in `make` and by dynamic registration.
1437
+ let warnRejectedRegistration = (
1438
+ verdict: AddressStore.verdict,
1439
+ ~chainId: ChainId.t,
1440
+ ~contractAddress: Address.t,
1441
+ ~contractName: string,
1442
+ ) =>
1443
+ switch verdict {
1444
+ | Conflict({existingContractName}) =>
1445
+ warnAddressRegistration(
1446
+ ~chainId,
1447
+ ~contractAddress,
1448
+ ~params={
1449
+ "existingContractType": existingContractName,
1450
+ "newContractType": contractName,
1451
+ },
1452
+ `Skipping contract registration: Contract address is already registered for one contract and cannot be registered for another contract.`,
1453
+ )
1454
+ | Duplicate({effectiveStartBlock, existingEffectiveStartBlock}) =>
1455
+ // FIXME: Instead of filtering out duplicates, we should check the block
1456
+ // number first. If a new registration has an earlier block number we
1457
+ // should register it for the missing block range.
1458
+ if existingEffectiveStartBlock > effectiveStartBlock {
1459
+ warnAddressRegistration(
1460
+ ~chainId,
1461
+ ~contractAddress,
1462
+ ~params={
1463
+ "existingBlockNumber": existingEffectiveStartBlock,
1464
+ "newBlockNumber": effectiveStartBlock,
1465
+ },
1466
+ `Skipping contract registration: Contract address is already registered at a later block number. Currently registration of the same contract address is not supported by Envio. Reach out to us if it's a problem for you.`,
1467
+ )
1468
+ }
1469
+ | Invalid =>
1470
+ warnAddressRegistration(
1471
+ ~chainId,
1472
+ ~contractAddress,
1473
+ ~params={"contractName": contractName},
1474
+ `Skipping contract registration: Not a valid address for this chain's ecosystem.`,
1475
+ )
1476
+ | Added(_) | NoEvents(_) => ()
1477
+ }
1478
+
1300
1479
  /**
1301
1480
  Creates partitions from indexing addresses with two phases:
1302
1481
  Phase 1: Create per-contract-name partitions (smart grouping by startBlock)
@@ -1543,21 +1722,12 @@ let registerDynamicContracts = (
1543
1722
  // single batch conflict the same way as across batches.
1544
1723
  let verdicts = addressStore->AddressStore.registerBatch(registrations)
1545
1724
 
1546
- let warn = (~contractAddress, ~params, message) =>
1547
- Logging.createChild(
1548
- ~params={
1549
- "chainId": fetchState.chainId,
1550
- "contractAddress": contractAddress,
1551
- "details": params,
1552
- },
1553
- )->Logging.childWarn(message)
1554
-
1555
1725
  let registeringContractNames = []
1556
1726
  let hasNoEventsUpdatesRef = ref(false)
1557
1727
  for idx in 0 to verdicts->Array.length - 1 {
1558
1728
  let registration = registrations->Array.getUnsafe(idx)
1559
- let contractAddress = registration.address->Address.toString
1560
- let keep = switch verdicts->Array.getUnsafe(idx) {
1729
+ let verdict = verdicts->Array.getUnsafe(idx)
1730
+ let keep = switch verdict {
1561
1731
  | Added(_) =>
1562
1732
  if !(registeringContractNames->Array.includes(registration.contractName)) {
1563
1733
  registeringContractNames->Array.push(registration.contractName)->ignore
@@ -1568,42 +1738,18 @@ let registerDynamicContracts = (
1568
1738
  // Persist the address to the db so a future config change that adds
1569
1739
  // events for this contract can pick it up on restart, but skip partition
1570
1740
  // registration since there's nothing to fetch.
1571
- warn(
1572
- ~contractAddress,
1741
+ warnAddressRegistration(
1742
+ ~chainId=fetchState.chainId,
1743
+ ~contractAddress=registration.address,
1573
1744
  ~params={"contractName": registration.contractName},
1574
1745
  `Persisting contract registration without fetching: Contract doesn't have any events to fetch. It'll be picked up on restart if you add events for the contract.`,
1575
1746
  )
1576
1747
  true
1577
- | Conflict({existingContractName}) =>
1578
- warn(
1579
- ~contractAddress,
1580
- ~params={
1581
- "existingContractType": existingContractName,
1582
- "newContractType": registration.contractName,
1583
- },
1584
- `Skipping contract registration: Contract address is already registered for one contract and cannot be registered for another contract.`,
1585
- )
1586
- false
1587
- | Duplicate({effectiveStartBlock, existingEffectiveStartBlock}) =>
1588
- // FIXME: Instead of filtering out duplicates, we should check the block
1589
- // number first. If a new registration has an earlier block number we
1590
- // should register it for the missing block range.
1591
- if existingEffectiveStartBlock > effectiveStartBlock {
1592
- warn(
1593
- ~contractAddress,
1594
- ~params={
1595
- "existingBlockNumber": existingEffectiveStartBlock,
1596
- "newBlockNumber": effectiveStartBlock,
1597
- },
1598
- `Skipping contract registration: Contract address is already registered at a later block number. Currently registration of the same contract address is not supported by Envio. Reach out to us if it's a problem for you.`,
1599
- )
1600
- }
1601
- false
1602
- | Invalid =>
1603
- warn(
1604
- ~contractAddress,
1605
- ~params={"contractName": registration.contractName},
1606
- `Skipping contract registration: Not a valid address for this chain's ecosystem.`,
1748
+ | Conflict(_) | Duplicate(_) | Invalid =>
1749
+ verdict->warnRejectedRegistration(
1750
+ ~chainId=fetchState.chainId,
1751
+ ~contractAddress=registration.address,
1752
+ ~contractName=registration.contractName,
1607
1753
  )
1608
1754
  false
1609
1755
  }
@@ -1764,7 +1910,7 @@ let registerDynamicContracts = (
1764
1910
 
1765
1911
  /*
1766
1912
  Updates fetchState with a response for a given query.
1767
- Returns Error if the partition with given query cannot be found (unexpected)
1913
+ Throws if the partition with given query cannot be found (unexpected)
1768
1914
 
1769
1915
  newItems are ordered earliest to latest (as they are returned from the worker)
1770
1916
  */
@@ -2038,11 +2184,27 @@ let walkPartitionPending = (
2038
2184
  pqIdx := pqIdx.contents + 1
2039
2185
  }
2040
2186
 
2187
+ // Nothing in this partition's selection can match below its earliest start
2188
+ // block, so forward work skips straight to it instead of scanning up to it and
2189
+ // discarding whole pages. Only the cursor moves — `latestFetchedBlock` still
2190
+ // advances solely on a response, so no block is ever reported fetched that
2191
+ // wasn't. Bounded by the head and the query end block: past either, the
2192
+ // partition would offer no candidate at all, so it queries as before rather
2193
+ // than going quiet until the chain reaches its start block.
2194
+ let cursor = switch p.selection.startBlock {
2195
+ | Some(startBlock) if startBlock > cursor.contents =>
2196
+ switch Utils.Math.minOptInt(Some(headBlockNumber), queryEndBlock) {
2197
+ | Some(reachable) if startBlock <= reachable => startBlock
2198
+ | _ => cursor.contents
2199
+ }
2200
+ | _ => cursor.contents
2201
+ }
2202
+
2041
2203
  canContinue.contents
2042
2204
  ? Some({
2043
2205
  partitionId,
2044
2206
  p,
2045
- cursor: cursor.contents,
2207
+ cursor,
2046
2208
  chunksUsedThisCall: chunksUsedThisCall.contents,
2047
2209
  inFlightCount,
2048
2210
  queryEndBlock,
@@ -2489,7 +2651,7 @@ let make = (
2489
2651
  ~addressStore: AddressStore.t,
2490
2652
  ~addresses: array<Internal.indexingAddress>,
2491
2653
  ~maxAddrInPartition,
2492
- ~chainId,
2654
+ ~chainId: ChainId.t,
2493
2655
  ~maxOnBlockBufferSize,
2494
2656
  ~knownHeight,
2495
2657
  ~progressBlockNumber=startBlock - 1,
@@ -2523,10 +2685,10 @@ let make = (
2523
2685
  partitions->Array.push({
2524
2686
  id: partitions->Array.length->Int.toString,
2525
2687
  latestFetchedBlock,
2526
- selection: {
2527
- dependsOnAddresses: false,
2528
- onEventRegistrations: notDependingOnAddresses,
2529
- },
2688
+ selection: makeSelection(
2689
+ ~dependsOnAddresses=false,
2690
+ ~onEventRegistrations=notDependingOnAddresses,
2691
+ ),
2530
2692
  addresses: addressStore->AddressStore.emptySet,
2531
2693
  mergeBlock: None,
2532
2694
  dynamicContract: None,
@@ -2538,10 +2700,10 @@ let make = (
2538
2700
  })
2539
2701
  }
2540
2702
 
2541
- let normalSelection = {
2542
- dependsOnAddresses: true,
2543
- onEventRegistrations: normalRegistrations,
2544
- }
2703
+ let normalSelection = makeSelection(
2704
+ ~dependsOnAddresses=true,
2705
+ ~onEventRegistrations=normalRegistrations,
2706
+ )
2545
2707
 
2546
2708
  // Every address the chain indexes goes into the store — including ones whose
2547
2709
  // contract has no address-dependent events, so a later registration of the
@@ -2554,7 +2716,17 @@ let make = (
2554
2716
  registrationBlock: contract.registrationBlock,
2555
2717
  }),
2556
2718
  )
2557
- ->ignore
2719
+ // Verdicts are in the batch's order, so they line up with `addresses`. A
2720
+ // config address the store rejects is dropped exactly like a dynamic one, and
2721
+ // needs the same warning — restored dynamic addresses come through here too.
2722
+ ->Array.forEachWithIndex((verdict, idx) => {
2723
+ let contract = addresses->Array.getUnsafe(idx)
2724
+ verdict->warnRejectedRegistration(
2725
+ ~chainId,
2726
+ ~contractAddress=contract.address,
2727
+ ~contractName=contract.contractName,
2728
+ )
2729
+ })
2558
2730
 
2559
2731
  let dynamicContracts = Utils.Set.make()
2560
2732
  let clientFilteredContracts = Utils.Set.make()
@@ -2618,7 +2790,7 @@ let make = (
2618
2790
  onBlockRegistrations->Utils.Array.isEmpty
2619
2791
  ) {
2620
2792
  JsError.throwWithMessage(
2621
- `Invalid configuration: Nothing to fetch on chain ${chainId->Int.toString}. ` ++
2793
+ `Invalid configuration: Nothing to fetch on chain ${chainId->ChainId.toString}. ` ++
2622
2794
  `addresses=${addresses->Array.length->Int.toString}, ` ++
2623
2795
  `onEventRegistrations=${onEventRegistrations->Array.length->Int.toString}, ` ++
2624
2796
  `normalRegistrations=${normalRegistrations