envio 3.15.0 → 3.16.0-subgraph

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 (186) hide show
  1. package/README.md +40 -34
  2. package/evm.schema.json +11 -2
  3. package/fuel.schema.json +10 -1
  4. package/index.d.ts +6 -0
  5. package/licenses/README.md +1 -1
  6. package/package.json +10 -9
  7. package/src/Batch.res.mjs +5 -5
  8. package/src/BatchProcessing.res +1 -0
  9. package/src/BatchProcessing.res.mjs +1 -0
  10. package/src/Bin.res +7 -0
  11. package/src/Bin.res.mjs +8 -0
  12. package/src/ChainFetching.res +450 -16
  13. package/src/ChainFetching.res.mjs +403 -32
  14. package/src/ChainState.res +124 -7
  15. package/src/ChainState.res.mjs +128 -67
  16. package/src/ChainState.resi +26 -0
  17. package/src/Clock.res +57 -0
  18. package/src/Clock.res.mjs +72 -0
  19. package/src/Config.res +31 -3
  20. package/src/Config.res.mjs +15 -4
  21. package/src/ContractRegisterContext.res +3 -6
  22. package/src/ContractRegisterContext.res.mjs +4 -2
  23. package/src/Core.res +95 -36
  24. package/src/Core.res.mjs +90 -1
  25. package/src/CrossChainState.res +12 -2
  26. package/src/CrossChainState.res.mjs +3 -3
  27. package/src/Ecosystem.res +7 -4
  28. package/src/Ecosystem.res.mjs +8 -8
  29. package/src/EffectCacheLookup.res +98 -0
  30. package/src/EffectCacheLookup.res.mjs +116 -0
  31. package/src/EffectState.res +10 -2
  32. package/src/EffectState.res.mjs +8 -4
  33. package/src/EffectState.resi +3 -1
  34. package/src/EntityId.res +2 -7
  35. package/src/EntityId.res.mjs +1 -8
  36. package/src/EntityTables.res +4 -1
  37. package/src/EntityTables.res.mjs +1 -1
  38. package/src/Env.res +20 -0
  39. package/src/Env.res.mjs +16 -0
  40. package/src/EventProcessing.res +38 -1
  41. package/src/EventProcessing.res.mjs +28 -1
  42. package/src/FetchState.res +408 -73
  43. package/src/FetchState.res.mjs +361 -91
  44. package/src/FinalizeBackfill.res +2 -4
  45. package/src/FinalizeBackfill.res.mjs +3 -4
  46. package/src/HandlerLoader.res +39 -15
  47. package/src/HandlerLoader.res.mjs +27 -8
  48. package/src/HandlerRegister.res +4 -0
  49. package/src/HandlerRegister.res.mjs +5 -2
  50. package/src/HandlerRegister.resi +2 -0
  51. package/src/InMemoryTable.res +17 -16
  52. package/src/InMemoryTable.res.mjs +14 -15
  53. package/src/IndexerState.res +28 -4
  54. package/src/IndexerState.res.mjs +19 -6
  55. package/src/IndexerState.resi +4 -0
  56. package/src/Internal.res +61 -49
  57. package/src/Internal.res.mjs +36 -80
  58. package/src/LoadLayer.res +54 -15
  59. package/src/LoadLayer.res.mjs +75 -45
  60. package/src/LoadLayer.resi +26 -0
  61. package/src/Main.res +14 -1
  62. package/src/Main.res.mjs +4 -3
  63. package/src/Persistence.res +3 -23
  64. package/src/Persistence.res.mjs +3 -3
  65. package/src/PreloadLookahead.res +202 -0
  66. package/src/PreloadLookahead.res.mjs +218 -0
  67. package/src/Rollback.res +0 -1
  68. package/src/Rollback.res.mjs +1 -1
  69. package/src/RunScope.res +3 -0
  70. package/src/SafeCheckpointTracking.res +4 -4
  71. package/src/SafeCheckpointTracking.res.mjs +1 -1
  72. package/src/SimulateItems.res +42 -26
  73. package/src/SimulateItems.res.mjs +40 -16
  74. package/src/StagedRows.res +106 -0
  75. package/src/StagedRows.res.mjs +102 -0
  76. package/src/Staging.res +32 -14
  77. package/src/Staging.res.mjs +17 -20
  78. package/src/Storage.res +303 -948
  79. package/src/Storage.res.mjs +229 -708
  80. package/src/SubgraphLog.res +19 -0
  81. package/src/SubgraphLog.res.mjs +26 -0
  82. package/src/Supervisor.res +75 -56
  83. package/src/Supervisor.res.mjs +55 -26
  84. package/src/SyncWrites.res +107 -0
  85. package/src/SyncWrites.res.mjs +98 -0
  86. package/src/TestIndexer.res +22 -27
  87. package/src/TestIndexer.res.mjs +20 -17
  88. package/src/Throttler.res +15 -8
  89. package/src/Throttler.res.mjs +9 -6
  90. package/src/Throttler.resi +1 -1
  91. package/src/UserContext.res +419 -46
  92. package/src/UserContext.res.mjs +318 -30
  93. package/src/Utils.res +17 -6
  94. package/src/Utils.res.mjs +12 -6
  95. package/src/Writing.res +0 -1
  96. package/src/Writing.res.mjs +1 -1
  97. package/src/bindings/ClickHouse.res +0 -322
  98. package/src/bindings/ClickHouse.res.mjs +0 -338
  99. package/src/bindings/StorageClient.res +108 -174
  100. package/src/bindings/StorageClient.res.mjs +19 -8
  101. package/src/bindings/Vitest.res +0 -26
  102. package/src/bindings/Vitest.res.mjs +0 -3
  103. package/src/db/Table.res +24 -150
  104. package/src/db/Table.res.mjs +38 -272
  105. package/src/sources/AddressSet.res +2 -0
  106. package/src/sources/AddressStore.res +3 -0
  107. package/src/sources/CallSource.res +274 -0
  108. package/src/sources/CallSource.res.mjs +196 -0
  109. package/src/sources/ChainSources.res +2 -0
  110. package/src/sources/ChainSources.res.mjs +2 -2
  111. package/src/sources/EvmChain.res +10 -1
  112. package/src/sources/EvmChain.res.mjs +6 -3
  113. package/src/sources/EvmHyperSyncSource.res +2 -2
  114. package/src/sources/EvmHyperSyncSource.res.mjs +1 -1
  115. package/src/sources/EvmRpcClient.res +13 -3
  116. package/src/sources/EvmRpcClient.res.mjs +4 -3
  117. package/src/sources/EvmRpcWs.res +4 -3
  118. package/src/sources/EvmRpcWs.res.mjs +2 -2
  119. package/src/sources/HeightFeed.res +31 -10
  120. package/src/sources/HeightFeed.res.mjs +10 -6
  121. package/src/sources/HeightStream.res +32 -17
  122. package/src/sources/HeightStream.res.mjs +10 -10
  123. package/src/sources/HyperSync.res +2 -1
  124. package/src/sources/HyperSync.res.mjs +2 -1
  125. package/src/sources/HyperSyncSSE.res +4 -6
  126. package/src/sources/HyperSyncSSE.res.mjs +2 -2
  127. package/src/sources/RpcSource.res +10 -1
  128. package/src/sources/RpcSource.res.mjs +4 -3
  129. package/src/sources/Source.res +3 -0
  130. package/src/sources/SourceManager.res +84 -77
  131. package/src/sources/SourceManager.res.mjs +17 -20
  132. package/src/sources/SourceManager.resi +3 -4
  133. package/src/sources/StartBlockResolver.res +1 -0
  134. package/src/sources/StartBlockResolver.res.mjs +3 -2
  135. package/src/sources/SvmHyperSyncSource.res +2 -2
  136. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
  137. package/src/subgraph/abi-types.ts +84 -0
  138. package/src/subgraph/aggregations.ts +745 -0
  139. package/src/subgraph/as-package.ts +143 -0
  140. package/src/subgraph/assemblyscript-regex.ts +67 -0
  141. package/src/subgraph/assemblyscript.ts +731 -0
  142. package/src/subgraph/blocks.ts +398 -0
  143. package/src/subgraph/calls-source.ts +702 -0
  144. package/src/subgraph/calls.ts +258 -0
  145. package/src/subgraph/conformance.ts +106 -0
  146. package/src/subgraph/errors.ts +85 -0
  147. package/src/subgraph/graft.ts +73 -0
  148. package/src/subgraph/graph-cli-worker.mjs +35 -0
  149. package/src/subgraph/graph-cli.ts +900 -0
  150. package/src/subgraph/graph-node-decimal.ts +199 -0
  151. package/src/subgraph/graph-ts-enums.ts +28 -0
  152. package/src/subgraph/graph-ts-types/VERSION +2 -0
  153. package/src/subgraph/graph-ts-types/chain/arweave.d.ts +70 -0
  154. package/src/subgraph/graph-ts-types/chain/cosmos.d.ts +327 -0
  155. package/src/subgraph/graph-ts-types/chain/ethereum.d.ts +233 -0
  156. package/src/subgraph/graph-ts-types/chain/near.d.ts +253 -0
  157. package/src/subgraph/graph-ts-types/chain/starknet.d.ts +32 -0
  158. package/src/subgraph/graph-ts-types/common/collections.d.ts +136 -0
  159. package/src/subgraph/graph-ts-types/common/conversion.d.ts +11 -0
  160. package/src/subgraph/graph-ts-types/common/datasource.d.ts +30 -0
  161. package/src/subgraph/graph-ts-types/common/eager-offset.d.ts +0 -0
  162. package/src/subgraph/graph-ts-types/common/json.d.ts +17 -0
  163. package/src/subgraph/graph-ts-types/common/numbers.d.ts +120 -0
  164. package/src/subgraph/graph-ts-types/common/value.d.ts +120 -0
  165. package/src/subgraph/graph-ts-types/common/yaml.d.ts +90 -0
  166. package/src/subgraph/graph-ts-types/global/global.d.ts +194 -0
  167. package/src/subgraph/graph-ts-types/helper-functions.d.ts +22 -0
  168. package/src/subgraph/graph-ts-types/index.d.ts +102 -0
  169. package/src/subgraph/graph-ts.ts +4339 -0
  170. package/src/subgraph/hosts.ts +554 -0
  171. package/src/subgraph/log.ts +28 -0
  172. package/src/subgraph/receipts.ts +184 -0
  173. package/src/subgraph/register-slots.ts +93 -0
  174. package/src/subgraph/rollup.ts +109 -0
  175. package/src/subgraph/rpc.ts +337 -0
  176. package/src/subgraph/runtime.ts +2059 -0
  177. package/src/subgraph/scope.ts +176 -0
  178. package/src/subgraph/stats.ts +42 -0
  179. package/src/subgraph/template-instances.ts +66 -0
  180. package/src/subgraph/toolchain.ts +327 -0
  181. package/src/subgraph/transient.ts +176 -0
  182. package/svm.schema.json +11 -2
  183. package/src/PgWriting.res +0 -36
  184. package/src/PgWriting.res.mjs +0 -34
  185. package/src/db/IndexDefinition.res +0 -47
  186. package/src/db/IndexDefinition.res.mjs +0 -67
@@ -864,6 +864,22 @@ module OptimizedPartitions = {
864
864
  }
865
865
  }
866
866
 
867
+ // Subgraph mode: where a dynamically registered address was registered and in
868
+ // which round of its block (1 for a data source created by the block's own
869
+ // triggers, 2 for one created by those, and so on).
870
+ type creationBlock = {block: int, round: int}
871
+
872
+ // When a contract's addresses leave the server-side filter. A switched contract
873
+ // is fetched by the single address-free partition and gated against the address
874
+ // store on our side. The switch is sticky for the run.
875
+ type clientFilterPolicy = {
876
+ // Past this many registered addresses. None never switches on count alone.
877
+ addressThreshold: option<int>,
878
+ // Every dynamic contract, whatever its address count. Keeps a chain at one
879
+ // query per block, of one shape, however many contracts it registers.
880
+ allDynamicContracts: bool,
881
+ }
882
+
867
883
  type t = {
868
884
  optimizedPartitions: OptimizedPartitions.t,
869
885
  startBlock: int,
@@ -879,6 +895,7 @@ type t = {
879
895
  // How much blocks behind the head we should query
880
896
  // Needed to query before entering reorg threshold
881
897
  blockLag: int,
898
+ blockThrottle: int,
882
899
  // Buffer of items ordered from earliest to latest
883
900
  buffer: array<Internal.item>,
884
901
  // Caps how far ahead onBlock items are pre-generated (set to 2x the batch
@@ -890,10 +907,17 @@ type t = {
890
907
  onBlockRegistrations: array<Internal.onBlockRegistration>,
891
908
  knownHeight: int,
892
909
  firstEventBlock: option<int>,
893
- // Per-contract registered-address count past which a dynamic contract is
894
- // switched to client-side filtering. None disables the switch, leaving every
895
- // contract filtered server-side.
896
- clientFilterAddressThreshold: option<int>,
910
+ clientFilterPolicy: clientFilterPolicy,
911
+ // Subgraph mode only: the block each dynamically registered address was
912
+ // registered in, keyed by contract and lowercase address, until fetching
913
+ // passes it. Its events in that block run after the rest of the block, as
914
+ // graph-node runs a new data source's creation-block triggers.
915
+ creationBlocks?: dict<creationBlock>,
916
+ // Set when an onBlock registration has a contractRegister: the last block
917
+ // whose block-handler register passes have run and whose registrations are
918
+ // applied. No block item past it is generated, so none is processed before
919
+ // the data sources its block created are fetched.
920
+ blockRegisterThrough?: int,
897
921
  }
898
922
 
899
923
  // The latest block whose items are all in the buffer: the lowest partition
@@ -983,6 +1007,65 @@ let compareTiebreak = (a: Internal.item, b: Internal.item): int => {
983
1007
  }
984
1008
  }
985
1009
 
1010
+ // Within a block, graph-node runs a subgraph's triggers in this order: logs and
1011
+ // calls by transaction — a transaction's logs, then its calls in trace order —
1012
+ // then the block triggers of `filter: call` handlers, one per call, then the
1013
+ // block handlers. A call item is told apart by its rank (see
1014
+ // `Internal.getItemCallRank`), and only a run that has one pays for this
1015
+ // comparison. Creation-block items (rank 4) keep their place after everything
1016
+ // else, ordered among themselves by the same transaction rule. A `once`
1017
+ // handler's item (rank -1) comes before all of them.
1018
+ let blockOrderRank = (item: Internal.item) =>
1019
+ switch item->Internal.getItemCreationRound {
1020
+ | Some(round) => 3 + round
1021
+ | None =>
1022
+ if item->Internal.getItemKind !== 0 {
1023
+ item->Internal.itemRunsFirst ? -1 : 3
1024
+ } else if item->Internal.getItemCallRank === Some(2) {
1025
+ 2
1026
+ } else {
1027
+ 0
1028
+ }
1029
+ }
1030
+
1031
+ let compareWithCalls = (a: Internal.item, b: Internal.item): int => {
1032
+ let ra = blockOrderRank(a)
1033
+ let rb = blockOrderRank(b)
1034
+ if ra !== rb {
1035
+ ra < rb ? -1 : 1
1036
+ } else if ra === 3 || ra === -1 {
1037
+ // Block handlers: as before.
1038
+ let la = a->Internal.getItemLogIndex
1039
+ let lb = b->Internal.getItemLogIndex
1040
+ if la != lb {
1041
+ la < lb ? -1 : 1
1042
+ } else {
1043
+ compareTiebreak(a, b)
1044
+ }
1045
+ } else {
1046
+ let ta = a->Internal.getItemTransactionIndex
1047
+ let tb = b->Internal.getItemTransactionIndex
1048
+ if ta != tb {
1049
+ ta < tb ? -1 : 1
1050
+ } else {
1051
+ // A transaction's logs precede its calls.
1052
+ let ca = a->Internal.getItemCallRank->Option.isSome ? 1 : 0
1053
+ let cb = b->Internal.getItemCallRank->Option.isSome ? 1 : 0
1054
+ if ca !== cb {
1055
+ ca < cb ? -1 : 1
1056
+ } else {
1057
+ let la = a->Internal.getItemLogIndex
1058
+ let lb = b->Internal.getItemLogIndex
1059
+ if la != lb {
1060
+ la < lb ? -1 : 1
1061
+ } else {
1062
+ compareTiebreak(a, b)
1063
+ }
1064
+ }
1065
+ }
1066
+ }
1067
+ }
1068
+
986
1069
  // Total order on buffer items: block, then item kind, then the ecosystem's
987
1070
  // within-block order, then registration index. Returns a plain int (-1/0/1)
988
1071
  // with explicit field comparisons so it can be called directly from the
@@ -994,15 +1077,32 @@ let compareTiebreak = (a: Internal.item, b: Internal.item): int => {
994
1077
  // block's handlers by construction. A sentinel log index for block items
995
1078
  // would put the same guarantee at the mercy of how large an ecosystem's key
996
1079
  // grows — which is how SVM, keyed by transaction index, came to run slot
997
- // handlers ahead of most of a slot's instructions.
1080
+ // handlers ahead of most of a slot's instructions. A subgraph's `once` handler
1081
+ // is the exception: it runs ahead of its block's events.
998
1082
  let compareBufferItem = (a: Internal.item, b: Internal.item): int => {
999
1083
  let ba = a->Internal.getItemBlockNumber
1000
1084
  let bb = b->Internal.getItemBlockNumber
1001
1085
  if ba != bb {
1002
1086
  ba < bb ? -1 : 1
1087
+ } else if Internal.callOrderingActive.contents {
1088
+ compareWithCalls(a, b)
1003
1089
  } else {
1004
- let ka = a->Internal.getItemKind
1005
- let kb = b->Internal.getItemKind
1090
+ let ka = switch a->Internal.getItemCreationRound {
1091
+ | Some(round) => 1 + round
1092
+ | None =>
1093
+ switch a->Internal.getItemKind {
1094
+ | 0 => 0
1095
+ | kind => a->Internal.itemRunsFirst ? -1 : kind
1096
+ }
1097
+ }
1098
+ let kb = switch b->Internal.getItemCreationRound {
1099
+ | Some(round) => 1 + round
1100
+ | None =>
1101
+ switch b->Internal.getItemKind {
1102
+ | 0 => 0
1103
+ | kind => b->Internal.itemRunsFirst ? -1 : kind
1104
+ }
1105
+ }
1006
1106
  if ka !== kb {
1007
1107
  ka < kb ? -1 : 1
1008
1108
  } else {
@@ -1024,6 +1124,7 @@ let compareBufferItem = (a: Internal.item, b: Internal.item): int => {
1024
1124
  let isSameLog = (a: Internal.item, b: Internal.item): bool =>
1025
1125
  a->Internal.getItemKind === 0 &&
1026
1126
  b->Internal.getItemKind === 0 &&
1127
+ a->Internal.getItemCallRank === b->Internal.getItemCallRank &&
1027
1128
  a->Internal.getItemBlockNumber === b->Internal.getItemBlockNumber &&
1028
1129
  a->Internal.getItemLogIndex === b->Internal.getItemLogIndex &&
1029
1130
  switch (a->Internal.getItemOrderPath, b->Internal.getItemOrderPath) {
@@ -1177,6 +1278,10 @@ let updateInternal = (
1177
1278
  },
1178
1279
  knownHeight,
1179
1280
  )
1281
+ let maxBlockNumber = switch fetchState.blockRegisterThrough {
1282
+ | Some(through) => Pervasives.min(maxBlockNumber, through)
1283
+ | None => maxBlockNumber
1284
+ }
1180
1285
  appendOnBlockItems(
1181
1286
  ~mutItems=blockItems,
1182
1287
  ~onBlockRegistrations,
@@ -1197,6 +1302,7 @@ let updateInternal = (
1197
1302
  optimizedPartitions,
1198
1303
  latestOnBlockBlockNumber,
1199
1304
  blockLag,
1305
+ blockThrottle: fetchState.blockThrottle,
1200
1306
  knownHeight,
1201
1307
  // Single merge point: fold any onBlock items into the sorted base buffer.
1202
1308
  buffer: switch blockItems {
@@ -1204,7 +1310,9 @@ let updateInternal = (
1204
1310
  | blockItems => base->mergeIntoBuffer(blockItems)
1205
1311
  },
1206
1312
  firstEventBlock: fetchState.firstEventBlock,
1207
- clientFilterAddressThreshold: fetchState.clientFilterAddressThreshold,
1313
+ clientFilterPolicy: fetchState.clientFilterPolicy,
1314
+ creationBlocks: ?fetchState.creationBlocks,
1315
+ blockRegisterThrough: ?fetchState.blockRegisterThrough,
1208
1316
  }
1209
1317
 
1210
1318
  updatedFetchState
@@ -1232,22 +1340,40 @@ type notice =
1232
1340
  requestedFromBlock: int,
1233
1341
  })
1234
1342
 
1235
- // Move a contract to client-side address filtering, recording why.
1236
- let addClientFilteredContract = (
1343
+ // The client-filtered set once `policy` has judged `contractNames`. A copy, so
1344
+ // older fetch state snapshots keep theirs: Utils.Set.add mutates in place.
1345
+ let switchToClientSideFiltering = (
1237
1346
  clientFilteredContracts: Utils.Set.t<string>,
1238
- ~contractName,
1347
+ ~contractNames: array<string>,
1348
+ ~policy: clientFilterPolicy,
1349
+ ~dynamicContracts: Utils.Set.t<string>,
1350
+ ~addressStore: AddressStore.t,
1239
1351
  ~onNotice,
1240
- ~addressCount,
1241
- ~threshold,
1242
1352
  // A resumed fetch state re-derives the switch from the persisted addresses,
1243
- // but it already happened - and was reported - in the run that crossed the
1244
- // threshold.
1353
+ // but it already happened - and was reported - in an earlier run.
1245
1354
  ~isAlreadyReported=false,
1246
1355
  ) => {
1247
- clientFilteredContracts->Utils.Set.add(contractName)->ignore
1248
- if !isAlreadyReported {
1249
- onNotice(SwitchedToClientSideFiltering({contractName, addressCount, threshold}))
1250
- }
1356
+ let switched = clientFilteredContracts->Utils.Set.toArray->Utils.Set.fromArray
1357
+ contractNames->Array.forEach(contractName =>
1358
+ if !(switched->Utils.Set.has(contractName)) {
1359
+ if policy.allDynamicContracts && dynamicContracts->Utils.Set.has(contractName) {
1360
+ switched->Utils.Set.add(contractName)->ignore
1361
+ } else {
1362
+ switch policy.addressThreshold {
1363
+ | Some(threshold) =>
1364
+ let addressCount = addressStore->AddressStore.contractCount(contractName)
1365
+ if addressCount > threshold {
1366
+ switched->Utils.Set.add(contractName)->ignore
1367
+ if !isAlreadyReported {
1368
+ onNotice(SwitchedToClientSideFiltering({contractName, addressCount, threshold}))
1369
+ }
1370
+ }
1371
+ | None => ()
1372
+ }
1373
+ }
1374
+ }
1375
+ )
1376
+ switched
1251
1377
  }
1252
1378
 
1253
1379
  // The block a partition will have fetched to once everything on its pending
@@ -1723,6 +1849,54 @@ OptimizedPartitions.t => {
1723
1849
  )
1724
1850
  }
1725
1851
 
1852
+ let creationBlockKey = (~contractName, ~address: Address.t) =>
1853
+ `${contractName}:${address->Address.toString->String.toLowerCase}`
1854
+
1855
+ // Marks the response's events that were logged in the block their address was
1856
+ // registered in, once the registrations fetching has already passed are
1857
+ // dropped: those can't receive another item.
1858
+ let markCreationBlockEvents = (creationBlocks: dict<creationBlock>, ~newItems, ~fetchedThrough) => {
1859
+ creationBlocks
1860
+ ->Dict.toArray
1861
+ ->Array.forEach(((key, {block})) =>
1862
+ if block < fetchedThrough {
1863
+ creationBlocks->Utils.Dict.deleteInPlace(key)
1864
+ }
1865
+ )
1866
+ newItems->Array.forEach(item =>
1867
+ if item->Internal.getItemKind === 0 {
1868
+ let eventItem = item->Internal.castUnsafeEventItem
1869
+ let key = creationBlockKey(
1870
+ ~contractName=eventItem.onEventRegistration.eventConfig.contractName,
1871
+ ~address=eventItem.payload->Internal.getPayloadSrcAddress,
1872
+ )
1873
+ switch creationBlocks->Utils.Dict.dangerouslyGetNonOption(key) {
1874
+ | Some({block, round}) if block === eventItem.blockNumber =>
1875
+ item->Internal.markItemCreationRound(round)
1876
+ | _ => ()
1877
+ }
1878
+ }
1879
+ )
1880
+ }
1881
+
1882
+ // The round a registration made by `item`'s handler belongs to: one past the
1883
+ // round `item` itself runs in, which is its address's registration round when
1884
+ // the event was logged in the block that address was registered in.
1885
+ let creationRoundOfRegistration = (fetchState: t, ~item: Internal.item) =>
1886
+ switch fetchState.creationBlocks {
1887
+ | None => 1
1888
+ | Some(creationBlocks) =>
1889
+ let eventItem = item->Internal.castUnsafeEventItem
1890
+ let key = creationBlockKey(
1891
+ ~contractName=eventItem.onEventRegistration.eventConfig.contractName,
1892
+ ~address=eventItem.payload->Internal.getPayloadSrcAddress,
1893
+ )
1894
+ switch creationBlocks->Utils.Dict.dangerouslyGetNonOption(key) {
1895
+ | Some({block, round}) if block === eventItem.blockNumber => round + 1
1896
+ | _ => 1
1897
+ }
1898
+ }
1899
+
1726
1900
  let registerDynamicContracts = (
1727
1901
  fetchState: t,
1728
1902
  ~addressStore: AddressStore.t,
@@ -1761,6 +1935,17 @@ let registerDynamicContracts = (
1761
1935
  let verdict = verdicts->Array.getUnsafe(idx)
1762
1936
  switch verdict {
1763
1937
  | Added({fetchable: true}) =>
1938
+ switch fetchState.creationBlocks {
1939
+ | Some(creationBlocks) =>
1940
+ creationBlocks->Dict.set(
1941
+ creationBlockKey(~contractName=registration.contractName, ~address=registration.address),
1942
+ {
1943
+ block: registration.registrationBlock,
1944
+ round: registration.creationRound->Option.getOr(1),
1945
+ },
1946
+ )
1947
+ | None => ()
1948
+ }
1764
1949
  if !(registeringContractNames->Array.includes(registration.contractName)) {
1765
1950
  registeringContractNames->Array.push(registration.contractName)->ignore
1766
1951
  }
@@ -1862,34 +2047,16 @@ let registerDynamicContracts = (
1862
2047
  }
1863
2048
  }
1864
2049
 
1865
- // Switch any dynamic contract that has just crossed the server-side
1866
- // address threshold to client-side filtering. Sticky: the set only grows, and
1867
- // collapse in createPartitions folds the contract's
1868
- // partitions into the single address-free partition.
1869
- // Clone the sticky set before mutating so this update owns its copy and
1870
- // older fetchState snapshots keep theirs (Utils.Set.add mutates in place).
1871
- let clientFilteredContracts = switch fetchState.clientFilterAddressThreshold {
1872
- | Some(threshold) =>
1873
- let clientFilteredContracts =
1874
- fetchState.optimizedPartitions.clientFilteredContracts
1875
- ->Utils.Set.toArray
1876
- ->Utils.Set.fromArray
1877
- dynamicContractsRef.contents
1878
- ->Utils.Set.toArray
1879
- ->Array.forEach(contractName => {
1880
- let addressCount = addressStore->AddressStore.contractCount(contractName)
1881
- if !(clientFilteredContracts->Utils.Set.has(contractName)) && addressCount > threshold {
1882
- clientFilteredContracts->addClientFilteredContract(
1883
- ~contractName,
1884
- ~onNotice,
1885
- ~addressCount,
1886
- ~threshold,
1887
- )
1888
- }
1889
- })
1890
- clientFilteredContracts
1891
- | None => fetchState.optimizedPartitions.clientFilteredContracts
1892
- }
2050
+ // Collapse in createPartitions folds a switched contract's partitions
2051
+ // into the single address-free partition.
2052
+ let clientFilteredContracts =
2053
+ fetchState.optimizedPartitions.clientFilteredContracts->switchToClientSideFiltering(
2054
+ ~contractNames=dynamicContractsRef.contents->Utils.Set.toArray,
2055
+ ~policy=fetchState.clientFilterPolicy,
2056
+ ~dynamicContracts=dynamicContractsRef.contents,
2057
+ ~addressStore,
2058
+ ~onNotice,
2059
+ )
1893
2060
 
1894
2061
  // Only this batch's additions need partitions; everything already
1895
2062
  // registered has one.
@@ -1920,6 +2087,48 @@ let registerDynamicContracts = (
1920
2087
  }
1921
2088
  }
1922
2089
 
2090
+ // Every dynamic contract, the ones registered from now on included, is fetched
2091
+ // address-free and filtered on our side. Its partitions fold into the single
2092
+ // address-free partition through the same collapse the address threshold uses.
2093
+ let switchDynamicContractsToClientSideFiltering = (fetchState: t, ~addressStore: AddressStore.t) =>
2094
+ if fetchState.clientFilterPolicy.allDynamicContracts {
2095
+ fetchState
2096
+ } else {
2097
+ let fetchState = {
2098
+ ...fetchState,
2099
+ clientFilterPolicy: {...fetchState.clientFilterPolicy, allDynamicContracts: true},
2100
+ }
2101
+ let {
2102
+ dynamicContracts,
2103
+ clientFilteredContracts,
2104
+ maxAddrInPartition,
2105
+ nextPartitionIndex,
2106
+ } = fetchState.optimizedPartitions
2107
+ let switched =
2108
+ clientFilteredContracts
2109
+ ->Utils.Set.toArray
2110
+ ->Array.concat(dynamicContracts->Utils.Set.toArray)
2111
+ ->Utils.Set.fromArray
2112
+ if switched->Utils.Set.size === clientFilteredContracts->Utils.Set.size {
2113
+ fetchState
2114
+ } else {
2115
+ fetchState->updateInternal(
2116
+ ~optimizedPartitions=createPartitions(
2117
+ ~registeringSetsByContract=Dict.make(),
2118
+ ~addressStore,
2119
+ ~dynamicContracts,
2120
+ ~clientFilteredContracts=switched,
2121
+ ~normalSelection=fetchState.normalSelection,
2122
+ ~maxAddrInPartition,
2123
+ ~nextPartitionIndex,
2124
+ ~existingPartitions=fetchState.optimizedPartitions.entities->Dict.valuesToArray,
2125
+ ~progressBlockNumber=0,
2126
+ ~knownHeight=fetchState.knownHeight,
2127
+ ),
2128
+ )
2129
+ }
2130
+ }
2131
+
1923
2132
  /*
1924
2133
  Updates fetchState with a response for a given query.
1925
2134
  Throws if the partition with given query cannot be found (unexpected)
@@ -1941,12 +2150,80 @@ let handleQueryResult = (fetchState: t, ~query: query, ~latestFetchedBlock: int,
1941
2150
  ~mutItems=?{
1942
2151
  switch newItems {
1943
2152
  | [] => None
1944
- | _ => Some(fetchState.buffer->mergeIntoBuffer(newItems))
2153
+ | _ =>
2154
+ switch fetchState.creationBlocks {
2155
+ | Some(creationBlocks) =>
2156
+ creationBlocks->markCreationBlockEvents(
2157
+ ~newItems,
2158
+ ~fetchedThrough=fetchState->bufferBlockNumber,
2159
+ )
2160
+ | None => ()
2161
+ }
2162
+ Some(fetchState.buffer->mergeIntoBuffer(newItems))
1945
2163
  }
1946
2164
  },
1947
2165
  )
1948
2166
  }
1949
2167
 
2168
+ // The items of the block handlers' register passes for the blocks after
2169
+ // `after`, through `through`, at most about `limit` of them in whole blocks;
2170
+ // and the last block they cover. `nextBlockRegisterItems` starts after the
2171
+ // last block passed.
2172
+ let capAtEndBlock = (fetchState: t, block) =>
2173
+ switch fetchState.endBlock {
2174
+ | Some(endBlock) => Pervasives.min(block, endBlock)
2175
+ | None => block
2176
+ }
2177
+
2178
+ let canAdvanceBlockRegisters = (fetchState: t, ~through) =>
2179
+ switch fetchState.blockRegisterThrough {
2180
+ | Some(after) => fetchState->capAtEndBlock(through) > after
2181
+ | None => false
2182
+ }
2183
+
2184
+ let blockRegisterItemsAfter = (fetchState: t, ~after, ~through, ~limit) =>
2185
+ switch fetchState.blockRegisterThrough {
2186
+ | Some(_) if fetchState->capAtEndBlock(through) > after =>
2187
+ let through = fetchState->capAtEndBlock(through)
2188
+ let items = []
2189
+ let covered = appendOnBlockItems(
2190
+ ~mutItems=items,
2191
+ ~onBlockRegistrations=fetchState.onBlockRegistrations->Array.filter((
2192
+ r: Internal.onBlockRegistration,
2193
+ ) => r.contractRegister !== None),
2194
+ ~indexerStartBlock=fetchState.startBlock,
2195
+ ~fromBlock=after,
2196
+ ~maxBlockNumber=through,
2197
+ ~maxOnBlockBufferSize=limit,
2198
+ )
2199
+ Some((items, covered))
2200
+ | _ => None
2201
+ }
2202
+
2203
+ let nextBlockRegisterItems = (fetchState: t, ~through, ~limit) =>
2204
+ switch fetchState.blockRegisterThrough {
2205
+ | Some(after) => fetchState->blockRegisterItemsAfter(~after, ~through, ~limit)
2206
+ | None => None
2207
+ }
2208
+
2209
+ let setBlockRegisterThrough = (fetchState: t, ~through) =>
2210
+ switch fetchState.blockRegisterThrough {
2211
+ | Some(current) if through > current =>
2212
+ {...fetchState, blockRegisterThrough: through}->updateInternal
2213
+ | _ => fetchState
2214
+ }
2215
+
2216
+ // How far block-handler register passes can run with no response to run them
2217
+ // with: through the block every partition has fetched, or the head when there
2218
+ // is no partition, and never past the chain's end block.
2219
+ let blockRegisterFrontier = (fetchState: t) =>
2220
+ fetchState->capAtEndBlock(
2221
+ switch fetchState.optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
2222
+ | Some(block) => Pervasives.min(block, fetchState.knownHeight)
2223
+ | None => fetchState.knownHeight
2224
+ },
2225
+ )
2226
+
1950
2227
  type nextQuery =
1951
2228
  | WaitingForNewBlock
1952
2229
  | NothingToQuery
@@ -2387,6 +2664,20 @@ let acceptCandidates = (
2387
2664
  }
2388
2665
  }
2389
2666
 
2667
+ // A throttled chain lets `blockThrottle - 1` new blocks build up before it
2668
+ // fetches them, so a frontier that far behind the lagged head is waiting, not
2669
+ // behind. An end block the head has reached is fetched without waiting.
2670
+ let blocksBehindHeadAllowedByThrottle = ({blockThrottle, endBlock, knownHeight, blockLag}: t) =>
2671
+ switch endBlock {
2672
+ | Some(endBlock) if knownHeight - blockLag >= endBlock => 0
2673
+ | _ => Pervasives.max(0, blockThrottle - 1)
2674
+ }
2675
+
2676
+ // The highest block the chain fetches up to right now: the lagged head, less the
2677
+ // blocks a throttled chain lets build up first.
2678
+ let headFetchTarget = ({knownHeight, blockLag} as fetchState: t) =>
2679
+ knownHeight - blockLag - fetchState->blocksBehindHeadAllowedByThrottle
2680
+
2390
2681
  // Candidate queries are sized against chainTargetBlock, the soft querying
2391
2682
  // horizon the owning chain wants to reach this tick — derived by ChainState
2392
2683
  // from its share of the indexer-wide buffer budget and its chain-level event
@@ -2415,7 +2706,7 @@ let acceptCandidates = (
2415
2706
  // rangeTargetDensity × (chainTargetBlock − fromBlock + 1) / inRangeCount — so
2416
2707
  // unknown-density partitions probe in parallel within one budget.
2417
2708
  let getNextQuery = (
2418
- {optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight, endBlock}: t,
2709
+ {optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight, endBlock} as fetchState: t,
2419
2710
  ~chainTargetBlock: int,
2420
2711
  ~chainTargetItems: float,
2421
2712
  ) => {
@@ -2423,7 +2714,8 @@ let getNextQuery = (
2423
2714
  if headBlockNumber <= 0 {
2424
2715
  WaitingForNewBlock
2425
2716
  } else {
2426
- let isOnBlockBehindTheHead = latestOnBlockBlockNumber < headBlockNumber
2717
+ let caughtUpBlockNumber = headBlockNumber - fetchState->blocksBehindHeadAllowedByThrottle
2718
+ let isOnBlockBehindTheHead = latestOnBlockBlockNumber < caughtUpBlockNumber
2427
2719
  let shouldWaitForNewBlock = ref(
2428
2720
  switch endBlock {
2429
2721
  | Some(endBlock) => headBlockNumber < endBlock
@@ -2468,7 +2760,7 @@ let getNextQuery = (
2468
2760
  }
2469
2761
  }
2470
2762
  inFlightCounts->Array.setUnsafe(idx, inFlightCount.contents)
2471
- if p.mutPendingQueries->Array.length > 0 || p.latestFetchedBlock < headBlockNumber {
2763
+ if p.mutPendingQueries->Array.length > 0 || p.latestFetchedBlock < caughtUpBlockNumber {
2472
2764
  // Even if there are some partitions waiting for the new block
2473
2765
  // We still want to wait for all partitions reaching the head
2474
2766
  // because they might update knownHeight in their response
@@ -2647,9 +2939,11 @@ let make = (
2647
2939
  ~progressBlockNumber=startBlock - 1,
2648
2940
  ~onBlockRegistrations=[],
2649
2941
  ~blockLag=0,
2942
+ ~blockThrottle=0,
2650
2943
  ~firstEventBlock=None,
2651
2944
  ~clientFilterAddressThreshold=None,
2652
2945
  ~isResumed=false,
2946
+ ~deferCreationBlockEvents=false,
2653
2947
  ): t => {
2654
2948
  let latestFetchedBlock = progressBlockNumber
2655
2949
 
@@ -2729,7 +3023,6 @@ let make = (
2729
3023
  })
2730
3024
 
2731
3025
  let dynamicContracts = Utils.Set.make()
2732
- let clientFilteredContracts = Utils.Set.make()
2733
3026
  let registeringSetsByContract = Dict.make()
2734
3027
 
2735
3028
  // What each contract needs a partition for is read back off the store rather
@@ -2753,26 +3046,24 @@ let make = (
2753
3046
  }
2754
3047
  })
2755
3048
 
2756
- // Switch any contract already over the server-side address threshold to
2757
- // client-side filtering at creation — a config contract with a large static
2758
- // address list, or a dynamic contract restored from a large persisted set.
2759
- switch clientFilterAddressThreshold {
2760
- | Some(threshold) =>
2761
- registeringSetsByContract->Utils.Dict.forEachWithKey((set, contractName) => {
2762
- let addressCount = set->AddressSet.size
2763
- if addressCount > threshold {
2764
- clientFilteredContracts->addClientFilteredContract(
2765
- ~contractName,
2766
- ~onNotice,
2767
- ~addressCount,
2768
- ~threshold,
2769
- ~isAlreadyReported=isResumed,
2770
- )
2771
- }
2772
- })
2773
- | None => ()
3049
+ let clientFilterPolicy = {
3050
+ addressThreshold: clientFilterAddressThreshold,
3051
+ allDynamicContracts: false,
2774
3052
  }
2775
3053
 
3054
+ // Switch at creation whatever the threshold already covers: a config
3055
+ // contract with a large static address list, or a dynamic contract restored
3056
+ // from a large persisted set.
3057
+ let clientFilteredContracts =
3058
+ Utils.Set.make()->switchToClientSideFiltering(
3059
+ ~contractNames=registeringSetsByContract->Dict.keysToArray,
3060
+ ~policy=clientFilterPolicy,
3061
+ ~dynamicContracts,
3062
+ ~addressStore,
3063
+ ~onNotice,
3064
+ ~isAlreadyReported=isResumed,
3065
+ )
3066
+
2776
3067
  let optimizedPartitions = createPartitions(
2777
3068
  ~registeringSetsByContract,
2778
3069
  ~addressStore,
@@ -2805,6 +3096,14 @@ let make = (
2805
3096
  // fetching, so without seeding the buffer here getNextQuery would return
2806
3097
  // NothingToQuery and the indexer would get stuck.
2807
3098
  let buffer = []
3099
+ // Every block up to the progress block was processed, so its register passes
3100
+ // ran and what they registered is in the seeded rows.
3101
+ let blockRegisterThrough =
3102
+ onBlockRegistrations->Array.some((r: Internal.onBlockRegistration) =>
3103
+ r.contractRegister !== None
3104
+ )
3105
+ ? Some(progressBlockNumber)
3106
+ : None
2808
3107
  let latestOnBlockBlockNumber = switch onBlockRegistrations {
2809
3108
  // As updateInternal keeps it: with nothing to generate per block the pointer
2810
3109
  // is the head cap on the buffer frontier, and left at the progress block it
@@ -2817,6 +3116,10 @@ let make = (
2817
3116
  | None => knownHeight
2818
3117
  | Some(latestFullyFetchedBlock) => Pervasives.min(latestFullyFetchedBlock, knownHeight)
2819
3118
  }
3119
+ let maxBlockNumber = switch blockRegisterThrough {
3120
+ | Some(through) => Pervasives.min(maxBlockNumber, through)
3121
+ | None => maxBlockNumber
3122
+ }
2820
3123
  appendOnBlockItems(
2821
3124
  ~mutItems=buffer,
2822
3125
  ~onBlockRegistrations,
@@ -2836,12 +3139,15 @@ let make = (
2836
3139
  latestOnBlockBlockNumber,
2837
3140
  normalSelection,
2838
3141
  blockLag,
3142
+ blockThrottle,
2839
3143
  onBlockRegistrations,
2840
3144
  maxOnBlockBufferSize,
2841
3145
  knownHeight,
2842
3146
  buffer,
2843
3147
  firstEventBlock,
2844
- clientFilterAddressThreshold,
3148
+ clientFilterPolicy,
3149
+ creationBlocks: ?(deferCreationBlockEvents ? Some(Dict.make()) : None),
3150
+ ?blockRegisterThrough,
2845
3151
  }
2846
3152
 
2847
3153
  fetchState
@@ -2993,6 +3299,13 @@ let rollback = (
2993
3299
  fetchState.latestOnBlockBlockNumber,
2994
3300
  targetBlockNumber,
2995
3301
  ),
3302
+ // The passes past the target run again on the refetched blocks: what they
3303
+ // registered was pruned from the store above.
3304
+ blockRegisterThrough: ?(
3305
+ fetchState.blockRegisterThrough->Option.map(through =>
3306
+ Pervasives.min(through, targetBlockNumber)
3307
+ )
3308
+ ),
2996
3309
  }->updateInternal(
2997
3310
  ~optimizedPartitions,
2998
3311
  // Filtering the sorted buffer keeps it sorted and deduped.
@@ -3054,15 +3367,37 @@ let isActivelyIndexing = ({endBlock} as fetchState: t) => {
3054
3367
  // True once the fetch frontier has reached the (lagged) head or endBlock,
3055
3368
  // regardless of whether the buffer has been consumed yet. Unlike
3056
3369
  // isReadyToEnterReorgThreshold, fetched-but-unprocessed items still count.
3057
- let isFetchingAtHead = ({endBlock, blockLag, knownHeight} as fetchState: t) => {
3370
+ let isFetchingAtHead = ({endBlock, knownHeight} as fetchState: t) => {
3058
3371
  let bufferBlockNumber = fetchState->bufferBlockNumber
3059
3372
  knownHeight !== 0 &&
3060
3373
  switch endBlock {
3061
3374
  | Some(endBlock) if bufferBlockNumber >= endBlock => true
3062
- | _ => bufferBlockNumber >= knownHeight - blockLag
3375
+ | _ => bufferBlockNumber >= fetchState->headFetchTarget
3063
3376
  }
3064
3377
  }
3065
3378
 
3379
+ // Counted in blocks whatever the chain's block time: the chain still fetches
3380
+ // block by block near the head, and an address-free query must never reach back
3381
+ // into a range a partition is still backfilling.
3382
+ let headClientFilteringDistance = 200
3383
+
3384
+ // Every partition is within `headClientFilteringDistance` of the fetch target. A
3385
+ // chain with no head to follow never is: no known height yet, or an end block it
3386
+ // stops at before the head.
3387
+ let isWithinHeadClientFilteringDistance = (
3388
+ {endBlock, blockLag, knownHeight, optimizedPartitions} as fetchState: t,
3389
+ ) =>
3390
+ knownHeight !== 0 &&
3391
+ switch endBlock {
3392
+ | Some(endBlock) => endBlock > knownHeight - blockLag
3393
+ | None => true
3394
+ } &&
3395
+ switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
3396
+ | Some(latestFetchedBlock) =>
3397
+ fetchState->headFetchTarget - latestFetchedBlock <= headClientFilteringDistance
3398
+ | None => true
3399
+ }
3400
+
3066
3401
  // Frontier at (or within `tolerance` of) the lagged head with no processable
3067
3402
  // items left — the moment the chain can cross into the reorg threshold.
3068
3403
  // `tolerance` absorbs the head advancing between a chain catching up and this