envio 3.2.0 → 3.3.0-alpha.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 (113) hide show
  1. package/package.json +6 -7
  2. package/src/Batch.res +12 -15
  3. package/src/Batch.res.mjs +4 -5
  4. package/src/BatchProcessing.res +199 -0
  5. package/src/BatchProcessing.res.mjs +125 -0
  6. package/src/ChainFetching.res +375 -0
  7. package/src/ChainFetching.res.mjs +206 -0
  8. package/src/ChainMetadata.res +27 -0
  9. package/src/ChainMetadata.res.mjs +27 -0
  10. package/src/ChainState.res +618 -0
  11. package/src/ChainState.res.mjs +461 -0
  12. package/src/ChainState.resi +85 -0
  13. package/src/Config.res +13 -4
  14. package/src/Config.res.mjs +8 -3
  15. package/src/ContractRegisterContext.res +106 -0
  16. package/src/ContractRegisterContext.res.mjs +76 -0
  17. package/src/Core.res +3 -0
  18. package/src/CrossChainState.res +269 -0
  19. package/src/CrossChainState.res.mjs +197 -0
  20. package/src/CrossChainState.resi +47 -0
  21. package/src/Ecosystem.res +58 -0
  22. package/src/Ecosystem.res.mjs +43 -1
  23. package/src/Env.res +7 -2
  24. package/src/Env.res.mjs +5 -2
  25. package/src/EventConfigBuilder.res +35 -0
  26. package/src/EventConfigBuilder.res.mjs +21 -0
  27. package/src/EventProcessing.res +60 -46
  28. package/src/EventProcessing.res.mjs +33 -32
  29. package/src/EventUtils.res +0 -4
  30. package/src/EventUtils.res.mjs +0 -4
  31. package/src/ExitOnCaughtUp.res +10 -0
  32. package/src/ExitOnCaughtUp.res.mjs +22 -0
  33. package/src/FetchState.res +162 -113
  34. package/src/FetchState.res.mjs +181 -119
  35. package/src/HandlerLoader.res +6 -1
  36. package/src/HandlerLoader.res.mjs +13 -36
  37. package/src/InMemoryStore.res +36 -451
  38. package/src/InMemoryStore.res.mjs +20 -369
  39. package/src/IndexerLoop.res +44 -0
  40. package/src/IndexerLoop.res.mjs +46 -0
  41. package/src/IndexerState.res +544 -0
  42. package/src/IndexerState.res.mjs +547 -0
  43. package/src/IndexerState.resi +132 -0
  44. package/src/Internal.res +44 -7
  45. package/src/LoadLayer.res +20 -18
  46. package/src/LoadLayer.res.mjs +14 -12
  47. package/src/LoadLayer.resi +6 -3
  48. package/src/LogSelection.res +86 -17
  49. package/src/LogSelection.res.mjs +67 -14
  50. package/src/Logging.res +11 -44
  51. package/src/Logging.res.mjs +10 -42
  52. package/src/Main.res +54 -79
  53. package/src/Main.res.mjs +44 -56
  54. package/src/PgStorage.res +8 -71
  55. package/src/PgStorage.res.mjs +3 -47
  56. package/src/Prometheus.res +7 -8
  57. package/src/Prometheus.res.mjs +7 -4
  58. package/src/PruneStaleHistory.res +45 -0
  59. package/src/PruneStaleHistory.res.mjs +46 -0
  60. package/src/RawEvent.res +73 -0
  61. package/src/RawEvent.res.mjs +51 -0
  62. package/src/Rollback.res +204 -0
  63. package/src/Rollback.res.mjs +118 -0
  64. package/src/SimulateItems.res +12 -10
  65. package/src/SimulateItems.res.mjs +1 -1
  66. package/src/TestIndexer.res +34 -27
  67. package/src/TestIndexer.res.mjs +4 -1
  68. package/src/UserContext.res +26 -114
  69. package/src/UserContext.res.mjs +15 -78
  70. package/src/Writing.res +242 -0
  71. package/src/Writing.res.mjs +215 -0
  72. package/src/db/InternalTable.res +14 -27
  73. package/src/sources/Evm.res +40 -1
  74. package/src/sources/Evm.res.mjs +94 -84
  75. package/src/sources/Fuel.res +40 -1
  76. package/src/sources/Fuel.res.mjs +36 -26
  77. package/src/sources/HyperFuel.res +41 -120
  78. package/src/sources/HyperFuel.res.mjs +42 -80
  79. package/src/sources/HyperFuel.resi +1 -24
  80. package/src/sources/HyperFuelClient.res +16 -297
  81. package/src/sources/HyperFuelClient.res.mjs +5 -4
  82. package/src/sources/HyperFuelSource.res +33 -8
  83. package/src/sources/HyperFuelSource.res.mjs +56 -10
  84. package/src/sources/HyperSyncSource.res +7 -5
  85. package/src/sources/HyperSyncSource.res.mjs +3 -3
  86. package/src/sources/RpcSource.res +2 -2
  87. package/src/sources/RpcSource.res.mjs +1 -1
  88. package/src/sources/SourceManager.res +8 -18
  89. package/src/sources/SourceManager.res.mjs +8 -5
  90. package/src/sources/SourceManager.resi +4 -1
  91. package/src/sources/Svm.res +18 -1
  92. package/src/sources/Svm.res.mjs +30 -22
  93. package/src/sources/SvmHyperSyncSource.res +1 -1
  94. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
  95. package/src/tui/Tui.res +38 -36
  96. package/src/tui/Tui.res.mjs +51 -51
  97. package/src/ChainFetcher.res +0 -519
  98. package/src/ChainFetcher.res.mjs +0 -314
  99. package/src/ChainManager.res +0 -358
  100. package/src/ChainManager.res.mjs +0 -291
  101. package/src/Ctx.res +0 -6
  102. package/src/Ctx.res.mjs +0 -2
  103. package/src/GlobalState.res +0 -1056
  104. package/src/GlobalState.res.mjs +0 -1086
  105. package/src/GlobalStateManager.res +0 -57
  106. package/src/GlobalStateManager.res.mjs +0 -68
  107. package/src/GlobalStateManager.resi +0 -7
  108. package/src/Ports.res +0 -5
  109. package/src/Ports.res.mjs +0 -9
  110. package/src/adapters/MarkBatchProcessedAdapter.res +0 -5
  111. package/src/adapters/MarkBatchProcessedAdapter.res.mjs +0 -14
  112. package/src/sources/EnvioApiClient.res +0 -15
  113. package/src/sources/EnvioApiClient.res.mjs +0 -24
@@ -0,0 +1,106 @@
1
+ // The contractRegister handler context: context.chain.ContractName.add(address).
2
+ // Independent of the in-memory store, so it stays off IndexerState and the
3
+ // fetch-time contract registration in ChainState doesn't pull the state in.
4
+
5
+ type contractRegisterParams = {
6
+ item: Internal.item,
7
+ onRegister: (~item: Internal.item, ~contractAddress: Address.t, ~contractName: string) => unit,
8
+ config: Config.t,
9
+ mutable isResolved: bool,
10
+ }
11
+
12
+ // Helper to create a validated add function for contract registration.
13
+ // The isResolved check has to live inside the returned closure (not just in the
14
+ // outer proxy trap) because users can capture `const add = context.chain.X.add`
15
+ // before awaiting — a later call would otherwise bypass the resolved guard.
16
+ let makeAddFunction = (~params: contractRegisterParams, ~contractName: string): (
17
+ Address.t => unit
18
+ ) => {
19
+ (contractAddress: Address.t) => {
20
+ if params.isResolved {
21
+ Utils.Error.make(`Impossible to access context.chain after the contract register is resolved. Make sure you didn't miss an await in the handler.`)->ErrorHandling.mkLogAndRaise(
22
+ ~logger=Ecosystem.getItemLogger(params.item, ~ecosystem=params.config.ecosystem),
23
+ )
24
+ }
25
+ let validatedAddress = if params.config.ecosystem.name === Evm {
26
+ // The value is passed from the user-land,
27
+ // so we need to validate and checksum/lowercase the address.
28
+ if params.config.lowercaseAddresses {
29
+ contractAddress->Address.Evm.fromAddressLowercaseOrThrow
30
+ } else {
31
+ contractAddress->Address.Evm.fromAddressOrThrow
32
+ }
33
+ } else {
34
+ // TODO: Ideally we should do the same for other ecosystems
35
+ contractAddress
36
+ }
37
+
38
+ params.onRegister(~item=params.item, ~contractAddress=validatedAddress, ~contractName)
39
+ }
40
+ }
41
+
42
+ // Chain proxy for contractRegister context: context.chain.ContractName.add(address)
43
+ let contractRegisterChainTraps: Utils.Proxy.traps<contractRegisterParams> = {
44
+ get: (~target as params, ~prop: unknown) => {
45
+ let prop = prop->(Utils.magic: unknown => string)
46
+ switch prop {
47
+ | "id" =>
48
+ let eventItem = params.item->Internal.castUnsafeEventItem
49
+ eventItem.chain->ChainMap.Chain.toChainId->(Utils.magic: int => unknown)
50
+ | _ =>
51
+ // Look up the contract name directly in config contracts across all chains.
52
+ let contractName = prop
53
+ let isValidContract =
54
+ params.config.chainMap
55
+ ->ChainMap.values
56
+ ->Array.some(chain => chain.contracts->Array.some(c => c.name === contractName))
57
+ if isValidContract {
58
+ let addFn = makeAddFunction(~params, ~contractName)
59
+ {"add": addFn}->(Utils.magic: {"add": Address.t => unit} => unknown)
60
+ } else {
61
+ JsError.throwWithMessage(
62
+ `Invalid contract name '${prop}' on context.chain. ${EntityFilter.codegenHelpMessage}`,
63
+ )
64
+ }
65
+ }
66
+ },
67
+ }
68
+
69
+ let contractRegisterTraps: Utils.Proxy.traps<contractRegisterParams> = {
70
+ get: (~target as params, ~prop: unknown) => {
71
+ let prop = prop->(Utils.magic: unknown => string)
72
+ if params.isResolved {
73
+ Utils.Error.make(
74
+ `Impossible to access context.${prop} after the contract register is resolved. Make sure you didn't miss an await in the handler.`,
75
+ )->ErrorHandling.mkLogAndRaise(
76
+ ~logger=Ecosystem.getItemLogger(params.item, ~ecosystem=params.config.ecosystem),
77
+ )
78
+ }
79
+ switch prop {
80
+ | "log" =>
81
+ Ecosystem.getItemUserLogger(params.item, ~ecosystem=params.config.ecosystem)->(
82
+ Utils.magic: Envio.logger => unknown
83
+ )
84
+
85
+ | "chain" =>
86
+ params
87
+ ->Utils.Proxy.make(contractRegisterChainTraps)
88
+ ->(Utils.magic: contractRegisterParams => unknown)
89
+ | _ =>
90
+ JsError.throwWithMessage(
91
+ `Invalid context access by '${prop}' property. Use context.chain.ContractName.add(address) to register contracts. ${EntityFilter.codegenHelpMessage}`,
92
+ )
93
+ }
94
+ },
95
+ }
96
+
97
+ let getContractRegisterContext = (params: contractRegisterParams) => {
98
+ params
99
+ ->Utils.Proxy.make(contractRegisterTraps)
100
+ ->(Utils.magic: contractRegisterParams => Internal.contractRegisterContext)
101
+ }
102
+
103
+ let getContractRegisterArgs = (params: contractRegisterParams): Internal.contractRegisterArgs => {
104
+ event: params.item->Ecosystem.getItemEvent(~ecosystem=params.config.ecosystem),
105
+ context: getContractRegisterContext(params),
106
+ }
@@ -0,0 +1,76 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Address from "./Address.res.mjs";
4
+ import * as ChainMap from "./ChainMap.res.mjs";
5
+ import * as Ecosystem from "./Ecosystem.res.mjs";
6
+ import * as EntityFilter from "./db/EntityFilter.res.mjs";
7
+ import * as ErrorHandling from "./ErrorHandling.res.mjs";
8
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
9
+
10
+ function makeAddFunction(params, contractName) {
11
+ return contractAddress => {
12
+ if (params.isResolved) {
13
+ ErrorHandling.mkLogAndRaise(Ecosystem.getItemLogger(params.item, params.config.ecosystem), undefined, new Error(`Impossible to access context.chain after the contract register is resolved. Make sure you didn't miss an await in the handler.`));
14
+ }
15
+ let validatedAddress = params.config.ecosystem.name === "evm" ? (
16
+ params.config.lowercaseAddresses ? Address.Evm.fromAddressLowercaseOrThrow(contractAddress) : Address.Evm.fromAddressOrThrow(contractAddress)
17
+ ) : contractAddress;
18
+ params.onRegister(params.item, validatedAddress, contractName);
19
+ };
20
+ }
21
+
22
+ let contractRegisterChainTraps_get = (params, prop) => {
23
+ if (prop === "id") {
24
+ return params.item.chain;
25
+ }
26
+ let isValidContract = ChainMap.values(params.config.chainMap).some(chain => chain.contracts.some(c => c.name === prop));
27
+ if (isValidContract) {
28
+ return {
29
+ add: makeAddFunction(params, prop)
30
+ };
31
+ } else {
32
+ return Stdlib_JsError.throwWithMessage(`Invalid contract name '` + prop + `' on context.chain. ` + EntityFilter.codegenHelpMessage);
33
+ }
34
+ };
35
+
36
+ let contractRegisterChainTraps = {
37
+ get: contractRegisterChainTraps_get
38
+ };
39
+
40
+ let contractRegisterTraps_get = (params, prop) => {
41
+ if (params.isResolved) {
42
+ ErrorHandling.mkLogAndRaise(Ecosystem.getItemLogger(params.item, params.config.ecosystem), undefined, new Error(`Impossible to access context.` + prop + ` after the contract register is resolved. Make sure you didn't miss an await in the handler.`));
43
+ }
44
+ switch (prop) {
45
+ case "chain" :
46
+ return new Proxy(params, contractRegisterChainTraps);
47
+ case "log" :
48
+ return Ecosystem.getItemUserLogger(params.item, params.config.ecosystem);
49
+ default:
50
+ return Stdlib_JsError.throwWithMessage(`Invalid context access by '` + prop + `' property. Use context.chain.ContractName.add(address) to register contracts. ` + EntityFilter.codegenHelpMessage);
51
+ }
52
+ };
53
+
54
+ let contractRegisterTraps = {
55
+ get: contractRegisterTraps_get
56
+ };
57
+
58
+ function getContractRegisterContext(params) {
59
+ return new Proxy(params, contractRegisterTraps);
60
+ }
61
+
62
+ function getContractRegisterArgs(params) {
63
+ return {
64
+ event: Ecosystem.getItemEvent(params.item, params.config.ecosystem),
65
+ context: new Proxy(params, contractRegisterTraps)
66
+ };
67
+ }
68
+
69
+ export {
70
+ makeAddFunction,
71
+ contractRegisterChainTraps,
72
+ contractRegisterTraps,
73
+ getContractRegisterContext,
74
+ getContractRegisterArgs,
75
+ }
76
+ /* Address Not a pure module */
package/src/Core.res CHANGED
@@ -8,6 +8,7 @@ type evmHypersyncClientCtor
8
8
  type evmRpcClientCtor
9
9
  type evmDecoderCtor
10
10
  type svmHypersyncClientCtor
11
+ type hyperfuelClientCtor
11
12
 
12
13
  type addon = {
13
14
  getConfigJson: (~configPath: Null.t<string>, ~directory: Null.t<string>) => string,
@@ -20,6 +21,8 @@ type addon = {
20
21
  evmDecoder: evmDecoderCtor,
21
22
  @as("SvmHypersyncClient")
22
23
  svmHypersyncClient: svmHypersyncClientCtor,
24
+ @as("HyperfuelClient")
25
+ hyperfuelClient: hyperfuelClientCtor,
23
26
  }
24
27
 
25
28
  @module("node:module") external createRequire: string => {..} = "createRequire"
@@ -0,0 +1,269 @@
1
+ // Whole-indexer view over every chain's ChainState: the chain collection plus
2
+ // the run-wide flags derived from it. Owns the cross-chain reads and the
3
+ // batch/threshold transitions that touch every chain. `t` is mutated in place
4
+ // through the transitions below; the type is opaque in the interface.
5
+
6
+ type t = {
7
+ chainStates: dict<ChainState.t>,
8
+ // Chain ids in a stable order, so the cross-chain loops iterate the chains
9
+ // without allocating a values array on every tick.
10
+ chainIds: array<int>,
11
+ // True once every chain has caught up to head/endBlock. Monotonic during a run.
12
+ mutable isRealtime: bool,
13
+ mutable isInReorgThreshold: bool,
14
+ // Indexer-wide caps on concurrent data-source queries, shared across all
15
+ // chains. The realtime budget applies once every chain is at head.
16
+ maxBackfillConcurrency: int,
17
+ maxRealtimeConcurrency: int,
18
+ // Indexer-wide fetch buffer pool (item count), shared across all chains.
19
+ targetBufferSize: int,
20
+ }
21
+
22
+ // The whole-indexer fetch buffer pool, independent of chain count.
23
+ let calculateTargetBufferSize = () =>
24
+ switch Env.targetBufferSize {
25
+ | Some(size) => size
26
+ | None => 100_000
27
+ }
28
+
29
+ // The concurrency budget in force for the current phase.
30
+ let maxConcurrency = (crossChainState: t) =>
31
+ crossChainState.isRealtime
32
+ ? crossChainState.maxRealtimeConcurrency
33
+ : crossChainState.maxBackfillConcurrency
34
+
35
+ let make = (
36
+ ~chainStates,
37
+ ~isInReorgThreshold,
38
+ ~isRealtime,
39
+ ~maxBackfillConcurrency=Env.maxBackfillConcurrency,
40
+ ~maxRealtimeConcurrency=Env.maxRealtimeConcurrency,
41
+ ~targetBufferSize=calculateTargetBufferSize(),
42
+ ): t => {
43
+ let crossChainState = {
44
+ chainStates,
45
+ chainIds: chainStates->Dict.valuesToArray->Array.map(cs => (cs->ChainState.chainConfig).id),
46
+ isRealtime,
47
+ isInReorgThreshold,
48
+ maxBackfillConcurrency,
49
+ maxRealtimeConcurrency,
50
+ targetBufferSize,
51
+ }
52
+ Prometheus.IndexingMaxConcurrency.set(~maxConcurrency=crossChainState->maxConcurrency)
53
+ Prometheus.IndexingTargetBufferSize.set(~targetBufferSize)
54
+ crossChainState
55
+ }
56
+
57
+ // Resolve a chain's state by id. The id always comes from `chainIds`, which is
58
+ // derived from `chainStates`, so the entry is guaranteed present.
59
+ let getChainState = (crossChainState: t, chainId) =>
60
+ crossChainState.chainStates->Utils.Dict.dangerouslyGetByIntNonOption(chainId)->Option.getUnsafe
61
+
62
+ // --- Accessors. ---
63
+
64
+ let chainStates = (crossChainState: t) => crossChainState.chainStates
65
+ let isRealtime = (crossChainState: t) => crossChainState.isRealtime
66
+ let isInReorgThreshold = (crossChainState: t) => crossChainState.isInReorgThreshold
67
+
68
+ // Partition queries in flight across every chain — the live draw against
69
+ // maxConcurrency.
70
+ let inFlight = (crossChainState: t) => {
71
+ let total = ref(0)
72
+ for i in 0 to crossChainState.chainIds->Array.length - 1 {
73
+ let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
74
+ total := total.contents + cs->ChainState.sourceManager->SourceManager.inFlightCount
75
+ }
76
+ total.contents
77
+ }
78
+
79
+ // Ready-to-process items across every chain — the live draw against
80
+ // targetBufferSize, which is a budget of processable events (items stuck behind
81
+ // a gap don't count toward the goal of keeping ~targetBufferSize ready).
82
+ let totalReadyCount = (crossChainState: t) => {
83
+ let total = ref(0)
84
+ for i in 0 to crossChainState.chainIds->Array.length - 1 {
85
+ let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
86
+ total := total.contents + cs->ChainState.fetchState->FetchState.bufferReadyCount
87
+ }
88
+ total.contents
89
+ }
90
+
91
+ // --- Derived (pure). ---
92
+
93
+ let nextItemIsNone = (crossChainState: t): bool =>
94
+ !Batch.hasReadyItem(
95
+ crossChainState.chainStates->Dict.valuesToArray->Array.map(ChainState.fetchState),
96
+ )
97
+
98
+ let getSafeCheckpointId = (crossChainState: t) => {
99
+ let result: ref<option<bigint>> = ref(None)
100
+
101
+ for i in 0 to crossChainState.chainIds->Array.length - 1 {
102
+ let cs = crossChainState->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
103
+ switch cs->ChainState.safeCheckpointTracking {
104
+ | None => () // Skip chains with maxReorgDepth = 0
105
+ | Some(safeCheckpointTracking) =>
106
+ let safeCheckpointId =
107
+ safeCheckpointTracking->SafeCheckpointTracking.getSafeCheckpointId(
108
+ ~sourceBlockNumber=(cs->ChainState.fetchState).knownHeight,
109
+ )
110
+ switch result.contents {
111
+ | None => result := Some(safeCheckpointId)
112
+ | Some(current) if safeCheckpointId < current => result := Some(safeCheckpointId)
113
+ | _ => ()
114
+ }
115
+ }
116
+ }
117
+
118
+ switch result.contents {
119
+ | Some(id) if id > 0n => Some(id)
120
+ | _ => None // No safe checkpoint found
121
+ }
122
+ }
123
+
124
+ // --- Cross-chain transitions. ---
125
+
126
+ let createBatch = (
127
+ crossChainState: t,
128
+ ~processedCheckpointId,
129
+ ~batchSizeTarget: int,
130
+ ~isRollback: bool,
131
+ ): Batch.t => {
132
+ Batch.make(
133
+ ~isInReorgThreshold=crossChainState.isInReorgThreshold,
134
+ ~checkpointIdBeforeBatch=processedCheckpointId->BigInt.add(
135
+ // Since for rollback we have a diff checkpoint id.
136
+ // This is needed to currectly overwrite old state
137
+ // in an append-only ClickHouse insert.
138
+ isRollback ? 1n : 0n,
139
+ ),
140
+ ~chainsBeforeBatch=crossChainState.chainStates->Utils.Dict.mapValues((
141
+ cs
142
+ ): Batch.chainBeforeBatch => {
143
+ fetchState: cs->ChainState.fetchState,
144
+ progressBlockNumber: cs->ChainState.committedProgressBlockNumber,
145
+ totalEventsProcessed: cs->ChainState.numEventsProcessed,
146
+ sourceBlockNumber: (cs->ChainState.fetchState).knownHeight,
147
+ reorgDetection: cs->ChainState.reorgDetection,
148
+ chainConfig: cs->ChainState.chainConfig,
149
+ }),
150
+ ~batchSizeTarget,
151
+ )
152
+ }
153
+
154
+ // Enter the reorg threshold: shrink each chain's buffer by its configured
155
+ // blockLag and flip the flag.
156
+ let enterReorgThreshold = (crossChainState: t) => {
157
+ Logging.info("Reorg threshold reached")
158
+ Prometheus.ReorgThreshold.set(~isInReorgThreshold=true)
159
+
160
+ for i in 0 to crossChainState.chainIds->Array.length - 1 {
161
+ crossChainState
162
+ ->getChainState(crossChainState.chainIds->Array.getUnsafe(i))
163
+ ->ChainState.enterReorgThreshold
164
+ }
165
+
166
+ crossChainState.isInReorgThreshold = true
167
+ }
168
+
169
+ // Commit each progressed chain's batch progress, then decide readiness for the
170
+ // whole indexer. A chain is marked caught up only once EVERY chain is caught up
171
+ // (reached endblock or fetched/processed to head) with no processable events
172
+ // left — so no chain flips to ready while another is still backfilling.
173
+ let applyBatchProgress = (crossChainState: t, ~batch: Batch.t) => {
174
+ let chainIds = crossChainState.chainIds
175
+
176
+ let everyChainCaughtUp = ref(true)
177
+ for i in 0 to chainIds->Array.length - 1 {
178
+ let cs = crossChainState->getChainState(chainIds->Array.getUnsafe(i))
179
+ cs->ChainState.applyBatchProgress(~batch)
180
+ if !(cs->ChainState.hasProcessedToEndblock || cs->ChainState.isProgressAtHead) {
181
+ everyChainCaughtUp := false
182
+ }
183
+ }
184
+
185
+ let indexerCaughtUp = crossChainState->nextItemIsNone && everyChainCaughtUp.contents
186
+
187
+ let allChainsReady = ref(true)
188
+ for i in 0 to chainIds->Array.length - 1 {
189
+ let cs = crossChainState->getChainState(chainIds->Array.getUnsafe(i))
190
+ if indexerCaughtUp {
191
+ cs->ChainState.markReady
192
+ }
193
+ if !(cs->ChainState.isReady) {
194
+ allChainsReady := false
195
+ }
196
+ }
197
+
198
+ if allChainsReady.contents {
199
+ Prometheus.ProgressReady.setAllReady()
200
+ }
201
+
202
+ let wasRealtime = crossChainState.isRealtime
203
+ crossChainState.isRealtime = crossChainState.isRealtime || allChainsReady.contents
204
+ if !wasRealtime && crossChainState.isRealtime {
205
+ // The realtime budget takes over now that every chain is at head.
206
+ Prometheus.IndexingMaxConcurrency.set(~maxConcurrency=crossChainState->maxConcurrency)
207
+ }
208
+ }
209
+
210
+ // --- Fetch control. ---
211
+
212
+ // Chains ordered furthest-behind first, so the shared concurrency and buffer
213
+ // pools go to the chains with the most backfill work before the rest.
214
+ let priorityOrder = (crossChainState: t) =>
215
+ crossChainState.chainStates
216
+ ->Dict.valuesToArray
217
+ ->Array.toSorted((a, b) =>
218
+ Float.compare(
219
+ a->ChainState.fetchState->FetchState.getProgressPercentage,
220
+ b->ChainState.fetchState->FetchState.getProgressPercentage,
221
+ )
222
+ )
223
+
224
+ // Dispatch a fetch tick across every chain in priority order, drawing from the
225
+ // shared concurrency and buffer pools. Chains are visited in turn; fetchChain
226
+ // bumps the in-flight count synchronously before it suspends, so a later chain
227
+ // sees the slots an earlier one already claimed and the budget is honored
228
+ // indexer-wide.
229
+ //
230
+ // bufferLimit is each chain's slice of the shared pool: it may grow its buffer
231
+ // into whatever the other chains leave free, so a lone backfilling chain can use
232
+ // the whole pool while head-following chains stay shallow.
233
+ let checkAndFetch = async (
234
+ crossChainState: t,
235
+ ~fetchChain: (
236
+ ~chain: ChainMap.Chain.t,
237
+ ~concurrencyLimit: int,
238
+ ~bufferLimit: int,
239
+ ) => promise<unit>,
240
+ ) => {
241
+ let maxConcurrency = crossChainState->maxConcurrency
242
+ // Pool is a budget of ready-to-process items, but the cap fed to getNextQuery
243
+ // is still a buffer position (it tracks the sorted buffer, ready items first),
244
+ // so add this chain's own buffer size back rather than its ready count. A chain
245
+ // with a gap therefore gets extra headroom to fetch the not-ready overhang
246
+ // while still aiming for its share of ~targetBufferSize ready events.
247
+ let totalReady = crossChainState->totalReadyCount
248
+ // Track the in-flight total as a running counter (summed once up front, then
249
+ // adjusted by each chain's delta) instead of re-summing every chain — O(chains)
250
+ // per tick rather than O(chains^2). fetchChain bumps the chain's count
251
+ // synchronously, so the delta is observable right after the call.
252
+ let inFlight = ref(crossChainState->inFlight)
253
+ let priorityOrdered = crossChainState->priorityOrder
254
+ let promises = []
255
+ for i in 0 to priorityOrdered->Array.length - 1 {
256
+ let cs = priorityOrdered->Array.getUnsafe(i)
257
+ let chain = ChainMap.Chain.makeUnsafe(~chainId=(cs->ChainState.chainConfig).id)
258
+ let sourceManager = cs->ChainState.sourceManager
259
+ let concurrencyLimit = Pervasives.max(0, maxConcurrency - inFlight.contents)
260
+ let bufferLimit =
261
+ crossChainState.targetBufferSize -
262
+ (totalReady -
263
+ cs->ChainState.fetchState->FetchState.bufferSize)
264
+ let inFlightBefore = sourceManager->SourceManager.inFlightCount
265
+ promises->Array.push(fetchChain(~chain, ~concurrencyLimit, ~bufferLimit))
266
+ inFlight := inFlight.contents + (sourceManager->SourceManager.inFlightCount - inFlightBefore)
267
+ }
268
+ let _ = await promises->Promise.all
269
+ }
@@ -0,0 +1,197 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Env from "./Env.res.mjs";
4
+ import * as Batch from "./Batch.res.mjs";
5
+ import * as Utils from "./Utils.res.mjs";
6
+ import * as Logging from "./Logging.res.mjs";
7
+ import * as ChainMap from "./ChainMap.res.mjs";
8
+ import * as ChainState from "./ChainState.res.mjs";
9
+ import * as FetchState from "./FetchState.res.mjs";
10
+ import * as Prometheus from "./Prometheus.res.mjs";
11
+ import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
12
+ import * as SourceManager from "./sources/SourceManager.res.mjs";
13
+ import * as Primitive_float from "@rescript/runtime/lib/es6/Primitive_float.js";
14
+ import * as SafeCheckpointTracking from "./SafeCheckpointTracking.res.mjs";
15
+
16
+ function calculateTargetBufferSize() {
17
+ if (Env.targetBufferSize !== undefined) {
18
+ return Env.targetBufferSize;
19
+ } else {
20
+ return 100000;
21
+ }
22
+ }
23
+
24
+ function maxConcurrency(crossChainState) {
25
+ if (crossChainState.isRealtime) {
26
+ return crossChainState.maxRealtimeConcurrency;
27
+ } else {
28
+ return crossChainState.maxBackfillConcurrency;
29
+ }
30
+ }
31
+
32
+ function make(chainStates, isInReorgThreshold, isRealtime, maxBackfillConcurrencyOpt, maxRealtimeConcurrencyOpt, targetBufferSizeOpt) {
33
+ let maxBackfillConcurrency = maxBackfillConcurrencyOpt !== undefined ? maxBackfillConcurrencyOpt : Env.maxBackfillConcurrency;
34
+ let maxRealtimeConcurrency = maxRealtimeConcurrencyOpt !== undefined ? maxRealtimeConcurrencyOpt : Env.maxRealtimeConcurrency;
35
+ let targetBufferSize = targetBufferSizeOpt !== undefined ? targetBufferSizeOpt : (
36
+ Env.targetBufferSize !== undefined ? Env.targetBufferSize : 100000
37
+ );
38
+ let crossChainState = {
39
+ chainStates: chainStates,
40
+ chainIds: Object.values(chainStates).map(cs => ChainState.chainConfig(cs).id),
41
+ isRealtime: isRealtime,
42
+ isInReorgThreshold: isInReorgThreshold,
43
+ maxBackfillConcurrency: maxBackfillConcurrency,
44
+ maxRealtimeConcurrency: maxRealtimeConcurrency,
45
+ targetBufferSize: targetBufferSize
46
+ };
47
+ Prometheus.IndexingMaxConcurrency.set(maxConcurrency(crossChainState));
48
+ Prometheus.IndexingTargetBufferSize.set(targetBufferSize);
49
+ return crossChainState;
50
+ }
51
+
52
+ function chainStates(crossChainState) {
53
+ return crossChainState.chainStates;
54
+ }
55
+
56
+ function isRealtime(crossChainState) {
57
+ return crossChainState.isRealtime;
58
+ }
59
+
60
+ function isInReorgThreshold(crossChainState) {
61
+ return crossChainState.isInReorgThreshold;
62
+ }
63
+
64
+ function inFlight(crossChainState) {
65
+ let total = 0;
66
+ for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
67
+ let cs = crossChainState.chainStates[crossChainState.chainIds[i]];
68
+ total = total + SourceManager.inFlightCount(ChainState.sourceManager(cs)) | 0;
69
+ }
70
+ return total;
71
+ }
72
+
73
+ function totalReadyCount(crossChainState) {
74
+ let total = 0;
75
+ for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
76
+ let cs = crossChainState.chainStates[crossChainState.chainIds[i]];
77
+ total = total + FetchState.bufferReadyCount(ChainState.fetchState(cs)) | 0;
78
+ }
79
+ return total;
80
+ }
81
+
82
+ function nextItemIsNone(crossChainState) {
83
+ return !Batch.hasReadyItem(Object.values(crossChainState.chainStates).map(ChainState.fetchState));
84
+ }
85
+
86
+ function getSafeCheckpointId(crossChainState) {
87
+ let result;
88
+ for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
89
+ let cs = crossChainState.chainStates[crossChainState.chainIds[i]];
90
+ let safeCheckpointTracking = ChainState.safeCheckpointTracking(cs);
91
+ if (safeCheckpointTracking !== undefined) {
92
+ let safeCheckpointId = SafeCheckpointTracking.getSafeCheckpointId(safeCheckpointTracking, ChainState.fetchState(cs).knownHeight);
93
+ let current = result;
94
+ if (!(current !== undefined && safeCheckpointId >= current)) {
95
+ result = safeCheckpointId;
96
+ }
97
+ }
98
+ }
99
+ let id = result;
100
+ if (id !== undefined && id > 0n) {
101
+ return id;
102
+ }
103
+ }
104
+
105
+ function createBatch(crossChainState, processedCheckpointId, batchSizeTarget, isRollback) {
106
+ return Batch.make(processedCheckpointId + (
107
+ isRollback ? 1n : 0n
108
+ ), Utils.Dict.mapValues(crossChainState.chainStates, cs => ({
109
+ fetchState: ChainState.fetchState(cs),
110
+ reorgDetection: ChainState.reorgDetection(cs),
111
+ progressBlockNumber: ChainState.committedProgressBlockNumber(cs),
112
+ sourceBlockNumber: ChainState.fetchState(cs).knownHeight,
113
+ totalEventsProcessed: ChainState.numEventsProcessed(cs),
114
+ chainConfig: ChainState.chainConfig(cs)
115
+ })), batchSizeTarget, crossChainState.isInReorgThreshold);
116
+ }
117
+
118
+ function enterReorgThreshold(crossChainState) {
119
+ Logging.info("Reorg threshold reached");
120
+ Prometheus.ReorgThreshold.set(true);
121
+ for (let i = 0, i_finish = crossChainState.chainIds.length; i < i_finish; ++i) {
122
+ ChainState.enterReorgThreshold(crossChainState.chainStates[crossChainState.chainIds[i]]);
123
+ }
124
+ crossChainState.isInReorgThreshold = true;
125
+ }
126
+
127
+ function applyBatchProgress(crossChainState, batch) {
128
+ let chainIds = crossChainState.chainIds;
129
+ let everyChainCaughtUp = true;
130
+ for (let i = 0, i_finish = chainIds.length; i < i_finish; ++i) {
131
+ let cs = crossChainState.chainStates[chainIds[i]];
132
+ ChainState.applyBatchProgress(cs, batch);
133
+ if (!(ChainState.hasProcessedToEndblock(cs) || ChainState.isProgressAtHead(cs))) {
134
+ everyChainCaughtUp = false;
135
+ }
136
+ }
137
+ let indexerCaughtUp = nextItemIsNone(crossChainState) && everyChainCaughtUp;
138
+ let allChainsReady = true;
139
+ for (let i$1 = 0, i_finish$1 = chainIds.length; i$1 < i_finish$1; ++i$1) {
140
+ let cs$1 = crossChainState.chainStates[chainIds[i$1]];
141
+ if (indexerCaughtUp) {
142
+ ChainState.markReady(cs$1);
143
+ }
144
+ if (!ChainState.isReady(cs$1)) {
145
+ allChainsReady = false;
146
+ }
147
+ }
148
+ if (allChainsReady) {
149
+ Prometheus.ProgressReady.setAllReady();
150
+ }
151
+ let wasRealtime = crossChainState.isRealtime;
152
+ crossChainState.isRealtime = crossChainState.isRealtime || allChainsReady;
153
+ if (!wasRealtime && crossChainState.isRealtime) {
154
+ return Prometheus.IndexingMaxConcurrency.set(maxConcurrency(crossChainState));
155
+ }
156
+ }
157
+
158
+ function priorityOrder(crossChainState) {
159
+ return Object.values(crossChainState.chainStates).toSorted((a, b) => Primitive_float.compare(FetchState.getProgressPercentage(ChainState.fetchState(a)), FetchState.getProgressPercentage(ChainState.fetchState(b))));
160
+ }
161
+
162
+ async function checkAndFetch(crossChainState, fetchChain) {
163
+ let maxConcurrency$1 = maxConcurrency(crossChainState);
164
+ let totalReady = totalReadyCount(crossChainState);
165
+ let inFlight$1 = inFlight(crossChainState);
166
+ let priorityOrdered = priorityOrder(crossChainState);
167
+ let promises = [];
168
+ for (let i = 0, i_finish = priorityOrdered.length; i < i_finish; ++i) {
169
+ let cs = priorityOrdered[i];
170
+ let chain = ChainMap.Chain.makeUnsafe(ChainState.chainConfig(cs).id);
171
+ let sourceManager = ChainState.sourceManager(cs);
172
+ let concurrencyLimit = Primitive_int.max(0, maxConcurrency$1 - inFlight$1 | 0);
173
+ let bufferLimit = crossChainState.targetBufferSize - (totalReady - FetchState.bufferSize(ChainState.fetchState(cs)) | 0) | 0;
174
+ let inFlightBefore = SourceManager.inFlightCount(sourceManager);
175
+ promises.push(fetchChain(chain, concurrencyLimit, bufferLimit));
176
+ inFlight$1 = inFlight$1 + (SourceManager.inFlightCount(sourceManager) - inFlightBefore | 0) | 0;
177
+ }
178
+ await Promise.all(promises);
179
+ }
180
+
181
+ export {
182
+ calculateTargetBufferSize,
183
+ make,
184
+ chainStates,
185
+ isRealtime,
186
+ isInReorgThreshold,
187
+ maxConcurrency,
188
+ inFlight,
189
+ nextItemIsNone,
190
+ getSafeCheckpointId,
191
+ createBatch,
192
+ enterReorgThreshold,
193
+ applyBatchProgress,
194
+ priorityOrder,
195
+ checkAndFetch,
196
+ }
197
+ /* Env Not a pure module */