envio 3.2.1 → 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.
- package/package.json +6 -7
- package/src/Batch.res +12 -15
- package/src/Batch.res.mjs +4 -5
- package/src/BatchProcessing.res +199 -0
- package/src/BatchProcessing.res.mjs +125 -0
- package/src/ChainFetching.res +375 -0
- package/src/ChainFetching.res.mjs +206 -0
- package/src/ChainMetadata.res +27 -0
- package/src/ChainMetadata.res.mjs +27 -0
- package/src/ChainState.res +618 -0
- package/src/ChainState.res.mjs +461 -0
- package/src/ChainState.resi +85 -0
- package/src/Config.res +13 -4
- package/src/Config.res.mjs +8 -3
- package/src/ContractRegisterContext.res +106 -0
- package/src/ContractRegisterContext.res.mjs +76 -0
- package/src/Core.res +3 -0
- package/src/CrossChainState.res +269 -0
- package/src/CrossChainState.res.mjs +197 -0
- package/src/CrossChainState.resi +47 -0
- package/src/Ecosystem.res +58 -0
- package/src/Ecosystem.res.mjs +43 -1
- package/src/Env.res +7 -2
- package/src/Env.res.mjs +5 -2
- package/src/EventConfigBuilder.res +2 -2
- package/src/EventProcessing.res +60 -46
- package/src/EventProcessing.res.mjs +33 -32
- package/src/EventUtils.res +0 -4
- package/src/EventUtils.res.mjs +0 -4
- package/src/ExitOnCaughtUp.res +10 -0
- package/src/ExitOnCaughtUp.res.mjs +22 -0
- package/src/FetchState.res +124 -62
- package/src/FetchState.res.mjs +159 -93
- package/src/InMemoryStore.res +36 -451
- package/src/InMemoryStore.res.mjs +20 -369
- package/src/IndexerLoop.res +44 -0
- package/src/IndexerLoop.res.mjs +46 -0
- package/src/IndexerState.res +544 -0
- package/src/IndexerState.res.mjs +547 -0
- package/src/IndexerState.resi +132 -0
- package/src/Internal.res +29 -8
- package/src/LoadLayer.res +20 -18
- package/src/LoadLayer.res.mjs +14 -12
- package/src/LoadLayer.resi +6 -3
- package/src/Logging.res +11 -44
- package/src/Logging.res.mjs +10 -42
- package/src/Main.res +54 -79
- package/src/Main.res.mjs +44 -56
- package/src/PgStorage.res +8 -71
- package/src/PgStorage.res.mjs +3 -47
- package/src/Prometheus.res +7 -8
- package/src/Prometheus.res.mjs +7 -4
- package/src/PruneStaleHistory.res +45 -0
- package/src/PruneStaleHistory.res.mjs +46 -0
- package/src/RawEvent.res +73 -0
- package/src/RawEvent.res.mjs +51 -0
- package/src/Rollback.res +204 -0
- package/src/Rollback.res.mjs +118 -0
- package/src/SimulateItems.res +12 -10
- package/src/SimulateItems.res.mjs +1 -1
- package/src/UserContext.res +26 -114
- package/src/UserContext.res.mjs +15 -78
- package/src/Writing.res +242 -0
- package/src/Writing.res.mjs +215 -0
- package/src/db/InternalTable.res +14 -27
- package/src/sources/Evm.res +40 -1
- package/src/sources/Evm.res.mjs +94 -84
- package/src/sources/Fuel.res +40 -1
- package/src/sources/Fuel.res.mjs +36 -26
- package/src/sources/HyperFuel.res +41 -120
- package/src/sources/HyperFuel.res.mjs +42 -80
- package/src/sources/HyperFuel.resi +1 -24
- package/src/sources/HyperFuelClient.res +16 -297
- package/src/sources/HyperFuelClient.res.mjs +5 -4
- package/src/sources/HyperFuelSource.res +33 -8
- package/src/sources/HyperFuelSource.res.mjs +56 -10
- package/src/sources/HyperSyncSource.res +5 -3
- package/src/sources/HyperSyncSource.res.mjs +1 -1
- package/src/sources/RpcSource.res +2 -2
- package/src/sources/RpcSource.res.mjs +1 -1
- package/src/sources/SourceManager.res +8 -18
- package/src/sources/SourceManager.res.mjs +8 -5
- package/src/sources/SourceManager.resi +4 -1
- package/src/sources/Svm.res +18 -1
- package/src/sources/Svm.res.mjs +30 -22
- package/src/sources/SvmHyperSyncSource.res +1 -1
- package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
- package/src/tui/Tui.res +38 -36
- package/src/tui/Tui.res.mjs +51 -51
- package/src/ChainFetcher.res +0 -519
- package/src/ChainFetcher.res.mjs +0 -314
- package/src/ChainManager.res +0 -358
- package/src/ChainManager.res.mjs +0 -291
- package/src/Ctx.res +0 -6
- package/src/Ctx.res.mjs +0 -2
- package/src/GlobalState.res +0 -1056
- package/src/GlobalState.res.mjs +0 -1086
- package/src/GlobalStateManager.res +0 -57
- package/src/GlobalStateManager.res.mjs +0 -68
- package/src/GlobalStateManager.resi +0 -7
- package/src/Ports.res +0 -5
- package/src/Ports.res.mjs +0 -9
- package/src/adapters/MarkBatchProcessedAdapter.res +0 -5
- package/src/adapters/MarkBatchProcessedAdapter.res.mjs +0 -14
- package/src/sources/EnvioApiClient.res +0 -15
- 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 */
|