envio 3.15.0 → 3.16.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 (99) 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 +8 -8
  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/ChainState.res +48 -7
  11. package/src/ChainState.res.mjs +27 -11
  12. package/src/Clock.res +57 -0
  13. package/src/Clock.res.mjs +72 -0
  14. package/src/Config.res +10 -3
  15. package/src/Config.res.mjs +4 -2
  16. package/src/Core.res +4 -40
  17. package/src/EffectState.res +10 -2
  18. package/src/EffectState.res.mjs +8 -4
  19. package/src/EffectState.resi +3 -1
  20. package/src/EntityId.res +2 -7
  21. package/src/EntityId.res.mjs +1 -8
  22. package/src/EntityTables.res +4 -1
  23. package/src/EntityTables.res.mjs +1 -1
  24. package/src/FetchState.res +157 -69
  25. package/src/FetchState.res.mjs +144 -49
  26. package/src/FinalizeBackfill.res +2 -4
  27. package/src/FinalizeBackfill.res.mjs +3 -4
  28. package/src/InMemoryTable.res +17 -16
  29. package/src/InMemoryTable.res.mjs +14 -15
  30. package/src/IndexerState.res +28 -4
  31. package/src/IndexerState.res.mjs +19 -6
  32. package/src/IndexerState.resi +4 -0
  33. package/src/Internal.res +0 -49
  34. package/src/Internal.res.mjs +1 -80
  35. package/src/LoadLayer.res +14 -8
  36. package/src/LoadLayer.res.mjs +8 -6
  37. package/src/Main.res.mjs +1 -1
  38. package/src/Persistence.res +3 -23
  39. package/src/Persistence.res.mjs +3 -3
  40. package/src/Rollback.res +0 -1
  41. package/src/Rollback.res.mjs +1 -1
  42. package/src/SafeCheckpointTracking.res +4 -4
  43. package/src/SafeCheckpointTracking.res.mjs +1 -1
  44. package/src/StagedRows.res +106 -0
  45. package/src/StagedRows.res.mjs +102 -0
  46. package/src/Staging.res +32 -14
  47. package/src/Staging.res.mjs +17 -20
  48. package/src/Storage.res +277 -948
  49. package/src/Storage.res.mjs +215 -707
  50. package/src/Supervisor.res +75 -56
  51. package/src/Supervisor.res.mjs +55 -26
  52. package/src/TestIndexer.res +22 -27
  53. package/src/TestIndexer.res.mjs +20 -17
  54. package/src/Throttler.res +15 -8
  55. package/src/Throttler.res.mjs +9 -6
  56. package/src/Throttler.resi +1 -1
  57. package/src/Utils.res +0 -6
  58. package/src/Utils.res.mjs +0 -5
  59. package/src/Writing.res +0 -1
  60. package/src/Writing.res.mjs +1 -1
  61. package/src/bindings/ClickHouse.res +0 -322
  62. package/src/bindings/ClickHouse.res.mjs +0 -338
  63. package/src/bindings/StorageClient.res +107 -174
  64. package/src/bindings/StorageClient.res.mjs +19 -8
  65. package/src/bindings/Vitest.res +0 -26
  66. package/src/bindings/Vitest.res.mjs +0 -3
  67. package/src/db/Table.res +24 -150
  68. package/src/db/Table.res.mjs +38 -272
  69. package/src/sources/ChainSources.res +2 -0
  70. package/src/sources/ChainSources.res.mjs +2 -2
  71. package/src/sources/EvmChain.res +3 -0
  72. package/src/sources/EvmChain.res.mjs +4 -2
  73. package/src/sources/EvmHyperSyncSource.res +2 -2
  74. package/src/sources/EvmHyperSyncSource.res.mjs +1 -1
  75. package/src/sources/EvmRpcClient.res +13 -3
  76. package/src/sources/EvmRpcClient.res.mjs +4 -3
  77. package/src/sources/EvmRpcWs.res +4 -3
  78. package/src/sources/EvmRpcWs.res.mjs +2 -2
  79. package/src/sources/HeightFeed.res +31 -10
  80. package/src/sources/HeightFeed.res.mjs +10 -6
  81. package/src/sources/HeightStream.res +32 -17
  82. package/src/sources/HeightStream.res.mjs +10 -10
  83. package/src/sources/HyperSyncSSE.res +4 -6
  84. package/src/sources/HyperSyncSSE.res.mjs +2 -2
  85. package/src/sources/RpcSource.res +10 -1
  86. package/src/sources/RpcSource.res.mjs +4 -3
  87. package/src/sources/Source.res +3 -0
  88. package/src/sources/SourceManager.res +84 -77
  89. package/src/sources/SourceManager.res.mjs +17 -20
  90. package/src/sources/SourceManager.resi +3 -4
  91. package/src/sources/StartBlockResolver.res +1 -0
  92. package/src/sources/StartBlockResolver.res.mjs +3 -2
  93. package/src/sources/SvmHyperSyncSource.res +2 -2
  94. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
  95. package/svm.schema.json +11 -2
  96. package/src/PgWriting.res +0 -36
  97. package/src/PgWriting.res.mjs +0 -34
  98. package/src/db/IndexDefinition.res +0 -47
  99. package/src/db/IndexDefinition.res.mjs +0 -67
@@ -864,6 +864,17 @@ module OptimizedPartitions = {
864
864
  }
865
865
  }
866
866
 
867
+ // When a contract's addresses leave the server-side filter. A switched contract
868
+ // is fetched by the single address-free partition and gated against the address
869
+ // store on our side. The switch is sticky for the run.
870
+ type clientFilterPolicy = {
871
+ // Past this many registered addresses. None never switches on count alone.
872
+ addressThreshold: option<int>,
873
+ // Every dynamic contract, whatever its address count. Keeps a chain at one
874
+ // query per block, of one shape, however many contracts it registers.
875
+ allDynamicContracts: bool,
876
+ }
877
+
867
878
  type t = {
868
879
  optimizedPartitions: OptimizedPartitions.t,
869
880
  startBlock: int,
@@ -879,6 +890,7 @@ type t = {
879
890
  // How much blocks behind the head we should query
880
891
  // Needed to query before entering reorg threshold
881
892
  blockLag: int,
893
+ blockThrottle: int,
882
894
  // Buffer of items ordered from earliest to latest
883
895
  buffer: array<Internal.item>,
884
896
  // Caps how far ahead onBlock items are pre-generated (set to 2x the batch
@@ -890,10 +902,7 @@ type t = {
890
902
  onBlockRegistrations: array<Internal.onBlockRegistration>,
891
903
  knownHeight: int,
892
904
  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>,
905
+ clientFilterPolicy: clientFilterPolicy,
897
906
  }
898
907
 
899
908
  // The latest block whose items are all in the buffer: the lowest partition
@@ -1197,6 +1206,7 @@ let updateInternal = (
1197
1206
  optimizedPartitions,
1198
1207
  latestOnBlockBlockNumber,
1199
1208
  blockLag,
1209
+ blockThrottle: fetchState.blockThrottle,
1200
1210
  knownHeight,
1201
1211
  // Single merge point: fold any onBlock items into the sorted base buffer.
1202
1212
  buffer: switch blockItems {
@@ -1204,7 +1214,7 @@ let updateInternal = (
1204
1214
  | blockItems => base->mergeIntoBuffer(blockItems)
1205
1215
  },
1206
1216
  firstEventBlock: fetchState.firstEventBlock,
1207
- clientFilterAddressThreshold: fetchState.clientFilterAddressThreshold,
1217
+ clientFilterPolicy: fetchState.clientFilterPolicy,
1208
1218
  }
1209
1219
 
1210
1220
  updatedFetchState
@@ -1232,22 +1242,40 @@ type notice =
1232
1242
  requestedFromBlock: int,
1233
1243
  })
1234
1244
 
1235
- // Move a contract to client-side address filtering, recording why.
1236
- let addClientFilteredContract = (
1245
+ // The client-filtered set once `policy` has judged `contractNames`. A copy, so
1246
+ // older fetch state snapshots keep theirs: Utils.Set.add mutates in place.
1247
+ let switchToClientSideFiltering = (
1237
1248
  clientFilteredContracts: Utils.Set.t<string>,
1238
- ~contractName,
1249
+ ~contractNames: array<string>,
1250
+ ~policy: clientFilterPolicy,
1251
+ ~dynamicContracts: Utils.Set.t<string>,
1252
+ ~addressStore: AddressStore.t,
1239
1253
  ~onNotice,
1240
- ~addressCount,
1241
- ~threshold,
1242
1254
  // 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.
1255
+ // but it already happened - and was reported - in an earlier run.
1245
1256
  ~isAlreadyReported=false,
1246
1257
  ) => {
1247
- clientFilteredContracts->Utils.Set.add(contractName)->ignore
1248
- if !isAlreadyReported {
1249
- onNotice(SwitchedToClientSideFiltering({contractName, addressCount, threshold}))
1250
- }
1258
+ let switched = clientFilteredContracts->Utils.Set.toArray->Utils.Set.fromArray
1259
+ contractNames->Array.forEach(contractName =>
1260
+ if !(switched->Utils.Set.has(contractName)) {
1261
+ if policy.allDynamicContracts && dynamicContracts->Utils.Set.has(contractName) {
1262
+ switched->Utils.Set.add(contractName)->ignore
1263
+ } else {
1264
+ switch policy.addressThreshold {
1265
+ | Some(threshold) =>
1266
+ let addressCount = addressStore->AddressStore.contractCount(contractName)
1267
+ if addressCount > threshold {
1268
+ switched->Utils.Set.add(contractName)->ignore
1269
+ if !isAlreadyReported {
1270
+ onNotice(SwitchedToClientSideFiltering({contractName, addressCount, threshold}))
1271
+ }
1272
+ }
1273
+ | None => ()
1274
+ }
1275
+ }
1276
+ }
1277
+ )
1278
+ switched
1251
1279
  }
1252
1280
 
1253
1281
  // The block a partition will have fetched to once everything on its pending
@@ -1862,34 +1890,16 @@ let registerDynamicContracts = (
1862
1890
  }
1863
1891
  }
1864
1892
 
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
- }
1893
+ // Collapse in createPartitions folds a switched contract's partitions
1894
+ // into the single address-free partition.
1895
+ let clientFilteredContracts =
1896
+ fetchState.optimizedPartitions.clientFilteredContracts->switchToClientSideFiltering(
1897
+ ~contractNames=dynamicContractsRef.contents->Utils.Set.toArray,
1898
+ ~policy=fetchState.clientFilterPolicy,
1899
+ ~dynamicContracts=dynamicContractsRef.contents,
1900
+ ~addressStore,
1901
+ ~onNotice,
1902
+ )
1893
1903
 
1894
1904
  // Only this batch's additions need partitions; everything already
1895
1905
  // registered has one.
@@ -1920,6 +1930,48 @@ let registerDynamicContracts = (
1920
1930
  }
1921
1931
  }
1922
1932
 
1933
+ // Every dynamic contract, the ones registered from now on included, is fetched
1934
+ // address-free and filtered on our side. Its partitions fold into the single
1935
+ // address-free partition through the same collapse the address threshold uses.
1936
+ let switchDynamicContractsToClientSideFiltering = (fetchState: t, ~addressStore: AddressStore.t) =>
1937
+ if fetchState.clientFilterPolicy.allDynamicContracts {
1938
+ fetchState
1939
+ } else {
1940
+ let fetchState = {
1941
+ ...fetchState,
1942
+ clientFilterPolicy: {...fetchState.clientFilterPolicy, allDynamicContracts: true},
1943
+ }
1944
+ let {
1945
+ dynamicContracts,
1946
+ clientFilteredContracts,
1947
+ maxAddrInPartition,
1948
+ nextPartitionIndex,
1949
+ } = fetchState.optimizedPartitions
1950
+ let switched =
1951
+ clientFilteredContracts
1952
+ ->Utils.Set.toArray
1953
+ ->Array.concat(dynamicContracts->Utils.Set.toArray)
1954
+ ->Utils.Set.fromArray
1955
+ if switched->Utils.Set.size === clientFilteredContracts->Utils.Set.size {
1956
+ fetchState
1957
+ } else {
1958
+ fetchState->updateInternal(
1959
+ ~optimizedPartitions=createPartitions(
1960
+ ~registeringSetsByContract=Dict.make(),
1961
+ ~addressStore,
1962
+ ~dynamicContracts,
1963
+ ~clientFilteredContracts=switched,
1964
+ ~normalSelection=fetchState.normalSelection,
1965
+ ~maxAddrInPartition,
1966
+ ~nextPartitionIndex,
1967
+ ~existingPartitions=fetchState.optimizedPartitions.entities->Dict.valuesToArray,
1968
+ ~progressBlockNumber=0,
1969
+ ~knownHeight=fetchState.knownHeight,
1970
+ ),
1971
+ )
1972
+ }
1973
+ }
1974
+
1923
1975
  /*
1924
1976
  Updates fetchState with a response for a given query.
1925
1977
  Throws if the partition with given query cannot be found (unexpected)
@@ -2387,6 +2439,20 @@ let acceptCandidates = (
2387
2439
  }
2388
2440
  }
2389
2441
 
2442
+ // A throttled chain lets `blockThrottle - 1` new blocks build up before it
2443
+ // fetches them, so a frontier that far behind the lagged head is waiting, not
2444
+ // behind. An end block the head has reached is fetched without waiting.
2445
+ let blocksBehindHeadAllowedByThrottle = ({blockThrottle, endBlock, knownHeight, blockLag}: t) =>
2446
+ switch endBlock {
2447
+ | Some(endBlock) if knownHeight - blockLag >= endBlock => 0
2448
+ | _ => Pervasives.max(0, blockThrottle - 1)
2449
+ }
2450
+
2451
+ // The highest block the chain fetches up to right now: the lagged head, less the
2452
+ // blocks a throttled chain lets build up first.
2453
+ let headFetchTarget = ({knownHeight, blockLag} as fetchState: t) =>
2454
+ knownHeight - blockLag - fetchState->blocksBehindHeadAllowedByThrottle
2455
+
2390
2456
  // Candidate queries are sized against chainTargetBlock, the soft querying
2391
2457
  // horizon the owning chain wants to reach this tick — derived by ChainState
2392
2458
  // from its share of the indexer-wide buffer budget and its chain-level event
@@ -2415,7 +2481,7 @@ let acceptCandidates = (
2415
2481
  // rangeTargetDensity × (chainTargetBlock − fromBlock + 1) / inRangeCount — so
2416
2482
  // unknown-density partitions probe in parallel within one budget.
2417
2483
  let getNextQuery = (
2418
- {optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight, endBlock}: t,
2484
+ {optimizedPartitions, blockLag, latestOnBlockBlockNumber, knownHeight, endBlock} as fetchState: t,
2419
2485
  ~chainTargetBlock: int,
2420
2486
  ~chainTargetItems: float,
2421
2487
  ) => {
@@ -2423,7 +2489,8 @@ let getNextQuery = (
2423
2489
  if headBlockNumber <= 0 {
2424
2490
  WaitingForNewBlock
2425
2491
  } else {
2426
- let isOnBlockBehindTheHead = latestOnBlockBlockNumber < headBlockNumber
2492
+ let caughtUpBlockNumber = headBlockNumber - fetchState->blocksBehindHeadAllowedByThrottle
2493
+ let isOnBlockBehindTheHead = latestOnBlockBlockNumber < caughtUpBlockNumber
2427
2494
  let shouldWaitForNewBlock = ref(
2428
2495
  switch endBlock {
2429
2496
  | Some(endBlock) => headBlockNumber < endBlock
@@ -2468,7 +2535,7 @@ let getNextQuery = (
2468
2535
  }
2469
2536
  }
2470
2537
  inFlightCounts->Array.setUnsafe(idx, inFlightCount.contents)
2471
- if p.mutPendingQueries->Array.length > 0 || p.latestFetchedBlock < headBlockNumber {
2538
+ if p.mutPendingQueries->Array.length > 0 || p.latestFetchedBlock < caughtUpBlockNumber {
2472
2539
  // Even if there are some partitions waiting for the new block
2473
2540
  // We still want to wait for all partitions reaching the head
2474
2541
  // because they might update knownHeight in their response
@@ -2647,6 +2714,7 @@ let make = (
2647
2714
  ~progressBlockNumber=startBlock - 1,
2648
2715
  ~onBlockRegistrations=[],
2649
2716
  ~blockLag=0,
2717
+ ~blockThrottle=0,
2650
2718
  ~firstEventBlock=None,
2651
2719
  ~clientFilterAddressThreshold=None,
2652
2720
  ~isResumed=false,
@@ -2729,7 +2797,6 @@ let make = (
2729
2797
  })
2730
2798
 
2731
2799
  let dynamicContracts = Utils.Set.make()
2732
- let clientFilteredContracts = Utils.Set.make()
2733
2800
  let registeringSetsByContract = Dict.make()
2734
2801
 
2735
2802
  // What each contract needs a partition for is read back off the store rather
@@ -2753,26 +2820,24 @@ let make = (
2753
2820
  }
2754
2821
  })
2755
2822
 
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 => ()
2823
+ let clientFilterPolicy = {
2824
+ addressThreshold: clientFilterAddressThreshold,
2825
+ allDynamicContracts: false,
2774
2826
  }
2775
2827
 
2828
+ // Switch at creation whatever the threshold already covers: a config
2829
+ // contract with a large static address list, or a dynamic contract restored
2830
+ // from a large persisted set.
2831
+ let clientFilteredContracts =
2832
+ Utils.Set.make()->switchToClientSideFiltering(
2833
+ ~contractNames=registeringSetsByContract->Dict.keysToArray,
2834
+ ~policy=clientFilterPolicy,
2835
+ ~dynamicContracts,
2836
+ ~addressStore,
2837
+ ~onNotice,
2838
+ ~isAlreadyReported=isResumed,
2839
+ )
2840
+
2776
2841
  let optimizedPartitions = createPartitions(
2777
2842
  ~registeringSetsByContract,
2778
2843
  ~addressStore,
@@ -2836,12 +2901,13 @@ let make = (
2836
2901
  latestOnBlockBlockNumber,
2837
2902
  normalSelection,
2838
2903
  blockLag,
2904
+ blockThrottle,
2839
2905
  onBlockRegistrations,
2840
2906
  maxOnBlockBufferSize,
2841
2907
  knownHeight,
2842
2908
  buffer,
2843
2909
  firstEventBlock,
2844
- clientFilterAddressThreshold,
2910
+ clientFilterPolicy,
2845
2911
  }
2846
2912
 
2847
2913
  fetchState
@@ -3054,15 +3120,37 @@ let isActivelyIndexing = ({endBlock} as fetchState: t) => {
3054
3120
  // True once the fetch frontier has reached the (lagged) head or endBlock,
3055
3121
  // regardless of whether the buffer has been consumed yet. Unlike
3056
3122
  // isReadyToEnterReorgThreshold, fetched-but-unprocessed items still count.
3057
- let isFetchingAtHead = ({endBlock, blockLag, knownHeight} as fetchState: t) => {
3123
+ let isFetchingAtHead = ({endBlock, knownHeight} as fetchState: t) => {
3058
3124
  let bufferBlockNumber = fetchState->bufferBlockNumber
3059
3125
  knownHeight !== 0 &&
3060
3126
  switch endBlock {
3061
3127
  | Some(endBlock) if bufferBlockNumber >= endBlock => true
3062
- | _ => bufferBlockNumber >= knownHeight - blockLag
3128
+ | _ => bufferBlockNumber >= fetchState->headFetchTarget
3063
3129
  }
3064
3130
  }
3065
3131
 
3132
+ // Counted in blocks whatever the chain's block time: the chain still fetches
3133
+ // block by block near the head, and an address-free query must never reach back
3134
+ // into a range a partition is still backfilling.
3135
+ let headClientFilteringDistance = 200
3136
+
3137
+ // Every partition is within `headClientFilteringDistance` of the fetch target. A
3138
+ // chain with no head to follow never is: no known height yet, or an end block it
3139
+ // stops at before the head.
3140
+ let isWithinHeadClientFilteringDistance = (
3141
+ {endBlock, blockLag, knownHeight, optimizedPartitions} as fetchState: t,
3142
+ ) =>
3143
+ knownHeight !== 0 &&
3144
+ switch endBlock {
3145
+ | Some(endBlock) => endBlock > knownHeight - blockLag
3146
+ | None => true
3147
+ } &&
3148
+ switch optimizedPartitions->OptimizedPartitions.getLatestFullyFetchedBlock {
3149
+ | Some(latestFetchedBlock) =>
3150
+ fetchState->headFetchTarget - latestFetchedBlock <= headClientFilteringDistance
3151
+ | None => true
3152
+ }
3153
+
3066
3154
  // Frontier at (or within `tolerance` of) the lagged head with no processable
3067
3155
  // items left — the moment the chain can cross into the reorg threshold.
3068
3156
  // `tolerance` absorbs the head advancing between a chain catching up and this