envio 3.0.2 → 3.1.0-rc.1
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/README.md +0 -1
- package/evm.schema.json +15 -8
- package/fuel.schema.json +19 -12
- package/index.d.ts +0 -2
- package/package.json +6 -7
- package/rescript.json +1 -1
- package/src/Batch.res +4 -214
- package/src/Batch.res.mjs +6 -165
- package/src/ChainFetcher.res +12 -28
- package/src/ChainFetcher.res.mjs +8 -17
- package/src/ChainManager.res +10 -9
- package/src/ChainManager.res.mjs +6 -10
- package/src/Config.res +9 -25
- package/src/Config.res.mjs +17 -27
- package/src/Core.res +7 -0
- package/src/Ctx.res +1 -0
- package/src/Env.res +0 -8
- package/src/Env.res.mjs +0 -6
- package/src/EventConfigBuilder.res +13 -123
- package/src/EventConfigBuilder.res.mjs +6 -73
- package/src/EventProcessing.res +5 -29
- package/src/EventProcessing.res.mjs +11 -20
- package/src/EventUtils.res +0 -27
- package/src/EventUtils.res.mjs +0 -24
- package/src/FetchState.res +2 -15
- package/src/FetchState.res.mjs +3 -18
- package/src/GlobalState.res +26 -39
- package/src/GlobalState.res.mjs +12 -40
- package/src/HandlerLoader.res +6 -5
- package/src/HandlerLoader.res.mjs +27 -9
- package/src/HandlerRegister.res +1 -12
- package/src/HandlerRegister.res.mjs +1 -6
- package/src/HandlerRegister.resi +1 -1
- package/src/Hasura.res +96 -32
- package/src/Hasura.res.mjs +93 -38
- package/src/InMemoryStore.res +205 -45
- package/src/InMemoryStore.res.mjs +157 -40
- package/src/InMemoryTable.res +165 -249
- package/src/InMemoryTable.res.mjs +156 -227
- package/src/Internal.res +10 -34
- package/src/Internal.res.mjs +9 -3
- package/src/LoadLayer.res +5 -5
- package/src/LoadLayer.res.mjs +5 -5
- package/src/LogSelection.res +15 -19
- package/src/LogSelection.res.mjs +5 -6
- package/src/Main.res +4 -6
- package/src/Main.res.mjs +26 -15
- package/src/Persistence.res +7 -132
- package/src/Persistence.res.mjs +1 -102
- package/src/PgStorage.res +57 -40
- package/src/PgStorage.res.mjs +60 -34
- package/src/ReorgDetection.res +35 -58
- package/src/ReorgDetection.res.mjs +21 -29
- package/src/SimulateItems.res.mjs +21 -3
- package/src/Sink.res +2 -2
- package/src/Sink.res.mjs +1 -1
- package/src/TableIndices.res +9 -2
- package/src/TableIndices.res.mjs +7 -1
- package/src/TestIndexer.res +53 -60
- package/src/TestIndexer.res.mjs +77 -63
- package/src/TestIndexerProxyStorage.res +4 -14
- package/src/TestIndexerProxyStorage.res.mjs +1 -5
- package/src/UserContext.res +2 -4
- package/src/UserContext.res.mjs +4 -5
- package/src/Utils.res +0 -2
- package/src/Utils.res.mjs +0 -3
- package/src/bindings/ClickHouse.res +45 -38
- package/src/bindings/ClickHouse.res.mjs +16 -17
- package/src/bindings/Vitest.res +3 -0
- package/src/db/InternalTable.res +59 -18
- package/src/db/InternalTable.res.mjs +82 -51
- package/src/db/Table.res +9 -2
- package/src/db/Table.res.mjs +10 -7
- package/src/sources/EnvioApiClient.res +15 -0
- package/src/sources/EnvioApiClient.res.mjs +24 -0
- package/src/sources/EvmChain.res +32 -10
- package/src/sources/EvmChain.res.mjs +31 -5
- package/src/sources/HyperFuelSource.res +15 -58
- package/src/sources/HyperFuelSource.res.mjs +20 -39
- package/src/sources/HyperSync.res +54 -100
- package/src/sources/HyperSync.res.mjs +67 -96
- package/src/sources/HyperSync.resi +4 -22
- package/src/sources/HyperSyncClient.res +70 -247
- package/src/sources/HyperSyncClient.res.mjs +47 -46
- package/src/sources/HyperSyncSource.res +94 -166
- package/src/sources/HyperSyncSource.res.mjs +100 -127
- package/src/sources/RpcSource.res +43 -22
- package/src/sources/RpcSource.res.mjs +50 -35
- package/src/sources/SimulateSource.res +1 -7
- package/src/sources/SimulateSource.res.mjs +1 -7
- package/src/sources/Source.res +10 -1
- package/src/sources/Source.res.mjs +3 -0
- package/src/sources/SourceManager.res +177 -8
- package/src/sources/SourceManager.res.mjs +141 -3
- package/src/sources/SourceManager.resi +19 -0
- package/src/tui/Tui.res +44 -6
- package/src/tui/Tui.res.mjs +56 -8
- package/src/tui/components/TuiData.res +3 -0
- package/svm.schema.json +11 -4
- package/src/sources/HyperSyncJsonApi.res +0 -390
- package/src/sources/HyperSyncJsonApi.res.mjs +0 -237
|
@@ -39,13 +39,7 @@ let make = (~items: array<Internal.item>, ~endBlock: int, ~chain: ChainMap.Chain
|
|
|
39
39
|
let reportedHeight = max(endBlock, 1)
|
|
40
40
|
Promise.resolve({
|
|
41
41
|
Source.knownHeight: reportedHeight,
|
|
42
|
-
|
|
43
|
-
rangeLastBlock: {
|
|
44
|
-
blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
45
|
-
blockNumber: reportedHeight,
|
|
46
|
-
},
|
|
47
|
-
prevRangeLastBlock: None,
|
|
48
|
-
},
|
|
42
|
+
blockHashes: [],
|
|
49
43
|
parsedQueueItems: result,
|
|
50
44
|
fromBlockQueried: 0,
|
|
51
45
|
latestFetchedBlockNumber: reportedHeight,
|
|
@@ -22,13 +22,7 @@ function make(items, endBlock, chain) {
|
|
|
22
22
|
let reportedHeight = Primitive_int.max(endBlock, 1);
|
|
23
23
|
return Promise.resolve({
|
|
24
24
|
knownHeight: reportedHeight,
|
|
25
|
-
|
|
26
|
-
rangeLastBlock: {
|
|
27
|
-
blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
28
|
-
blockNumber: reportedHeight
|
|
29
|
-
},
|
|
30
|
-
prevRangeLastBlock: undefined
|
|
31
|
-
},
|
|
25
|
+
blockHashes: [],
|
|
32
26
|
parsedQueueItems: result,
|
|
33
27
|
fromBlockQueried: 0,
|
|
34
28
|
latestFetchedBlockNumber: reportedHeight,
|
package/src/sources/Source.res
CHANGED
|
@@ -12,7 +12,11 @@ Thes response returned from a block range fetch
|
|
|
12
12
|
*/
|
|
13
13
|
type blockRangeFetchResponse = {
|
|
14
14
|
knownHeight: int,
|
|
15
|
-
|
|
15
|
+
// Best-effort (blockNumber, blockHash) pairs observed while fetching this range.
|
|
16
|
+
// Used by reorg detection; gaps are OK, no extra requests are made to fill them.
|
|
17
|
+
// Duplicates with the same block number are allowed — registerReorgGuard treats
|
|
18
|
+
// a within-array hash mismatch on the same block number as a reorg.
|
|
19
|
+
blockHashes: array<ReorgDetection.blockData>,
|
|
16
20
|
parsedQueueItems: array<Internal.item>,
|
|
17
21
|
fromBlockQueried: int,
|
|
18
22
|
latestFetchedBlockNumber: int,
|
|
@@ -25,6 +29,8 @@ type getItemsRetry =
|
|
|
25
29
|
| WithBackoff({message: string, backoffMillis: int})
|
|
26
30
|
| ImpossibleForTheQuery({message: string})
|
|
27
31
|
|
|
32
|
+
exception RateLimited({resetMs: int})
|
|
33
|
+
|
|
28
34
|
type getItemsError =
|
|
29
35
|
| UnsupportedSelection({message: string})
|
|
30
36
|
| FailedGettingFieldSelection({exn: exn, blockNumber: int, logIndex: int, message: string})
|
|
@@ -58,4 +64,7 @@ type t = {
|
|
|
58
64
|
~logger: Pino.t,
|
|
59
65
|
) => promise<blockRangeFetchResponse>,
|
|
60
66
|
createHeightSubscription?: (~onHeight: int => unit) => unit => unit,
|
|
67
|
+
// Invoked by SourceManager once a rollback target is known so the source can
|
|
68
|
+
// drop any state that may now point at an orphaned chain (e.g. RPC block cache).
|
|
69
|
+
onReorg?: (~rollbackTargetBlock: int) => unit,
|
|
61
70
|
}
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
4
4
|
|
|
5
|
+
let RateLimited = /* @__PURE__ */Primitive_exceptions.create("Source.RateLimited");
|
|
6
|
+
|
|
5
7
|
let GetItemsError = /* @__PURE__ */Primitive_exceptions.create("Source.GetItemsError");
|
|
6
8
|
|
|
7
9
|
export {
|
|
10
|
+
RateLimited,
|
|
8
11
|
GetItemsError,
|
|
9
12
|
}
|
|
10
13
|
/* No side effect */
|
|
@@ -32,10 +32,92 @@ type t = {
|
|
|
32
32
|
mutable fetchingPartitionsCount: int,
|
|
33
33
|
recoveryTimeout: float,
|
|
34
34
|
mutable hasRealtime: bool,
|
|
35
|
+
mutable committedRateLimitTimeMs: float,
|
|
36
|
+
mutable rateLimitWaiters: int,
|
|
37
|
+
// Wall-clock timestamp (Date.now()) when the current rate-limit window
|
|
38
|
+
// started, or None if not currently waiting. Wall-clock so consumers
|
|
39
|
+
// (TUI) can compute elapsed time with their own Date.now() reads.
|
|
40
|
+
mutable activeRateLimitStartMs: option<float>,
|
|
41
|
+
// Wall-clock timestamp by which the server expects the longest current
|
|
42
|
+
// wait to clear. Tracks the latest reset across concurrent waiters so
|
|
43
|
+
// the displayed countdown reflects when the indexer will actually retry.
|
|
44
|
+
mutable activeRateLimitResetAtMs: option<float>,
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
let getActiveSource = sourceManager => sourceManager.activeSource
|
|
38
48
|
|
|
49
|
+
let getRateLimitTimeMs = sourceManager =>
|
|
50
|
+
sourceManager.committedRateLimitTimeMs +.
|
|
51
|
+
switch sourceManager.activeRateLimitStartMs {
|
|
52
|
+
| Some(startMs) => Date.now() -. startMs
|
|
53
|
+
| None => 0.0
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let isRateLimited = sourceManager => sourceManager.activeRateLimitStartMs->Option.isSome
|
|
57
|
+
|
|
58
|
+
let getRateLimitResetInMs = sourceManager =>
|
|
59
|
+
switch sourceManager.activeRateLimitResetAtMs {
|
|
60
|
+
| Some(resetAt) =>
|
|
61
|
+
let remaining = resetAt -. Date.now()
|
|
62
|
+
remaining > 0.0 ? Some(remaining) : None
|
|
63
|
+
| None => None
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let startRateLimitTimeout = (sourceManager, ~resetMs) => {
|
|
67
|
+
let now = Date.now()
|
|
68
|
+
if sourceManager.rateLimitWaiters === 0 {
|
|
69
|
+
sourceManager.activeRateLimitStartMs = Some(now)
|
|
70
|
+
}
|
|
71
|
+
let resetAt = now +. resetMs->Int.toFloat
|
|
72
|
+
sourceManager.activeRateLimitResetAtMs = switch sourceManager.activeRateLimitResetAtMs {
|
|
73
|
+
| Some(existing) => Some(Pervasives.max(existing, resetAt))
|
|
74
|
+
| None => Some(resetAt)
|
|
75
|
+
}
|
|
76
|
+
sourceManager.rateLimitWaiters = sourceManager.rateLimitWaiters + 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let stopRateLimitTimeout = sourceManager => {
|
|
80
|
+
sourceManager.rateLimitWaiters = sourceManager.rateLimitWaiters - 1
|
|
81
|
+
if sourceManager.rateLimitWaiters === 0 {
|
|
82
|
+
switch sourceManager.activeRateLimitStartMs {
|
|
83
|
+
| Some(startMs) =>
|
|
84
|
+
sourceManager.committedRateLimitTimeMs =
|
|
85
|
+
sourceManager.committedRateLimitTimeMs +. Date.now() -. startMs
|
|
86
|
+
sourceManager.activeRateLimitStartMs = None
|
|
87
|
+
| None => ()
|
|
88
|
+
}
|
|
89
|
+
sourceManager.activeRateLimitResetAtMs = None
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Shared between executeQuery and getBlockHashes: wait out the server's
|
|
94
|
+
// suggested reset window. Cap at 5 minutes to protect against
|
|
95
|
+
// pathologically large server values. Escalates the log from trace to
|
|
96
|
+
// warn after the second consecutive retry so the indexer doesn't go
|
|
97
|
+
// silent under chronic throttling.
|
|
98
|
+
let waitForRateLimitReset = async (sourceManager: t, ~resetMs, ~retry, ~logger) => {
|
|
99
|
+
let waitMs = Pervasives.min(resetMs, 300_000)
|
|
100
|
+
let log = retry >= 2 ? Logging.childWarn : Logging.childTrace
|
|
101
|
+
logger->log({
|
|
102
|
+
"msg": `HyperSync source is rate-limited — not critical, the indexer will retry in ${(waitMs / 1000)
|
|
103
|
+
->Int.toString}s. For higher limits upgrade your plan at https://envio.dev/app/api-tokens.`,
|
|
104
|
+
"retry": retry,
|
|
105
|
+
"waitMs": waitMs,
|
|
106
|
+
})
|
|
107
|
+
sourceManager->startRateLimitTimeout(~resetMs=waitMs)
|
|
108
|
+
await Utils.delay(waitMs)
|
|
109
|
+
sourceManager->stopRateLimitTimeout
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let onReorg = (sourceManager: t, ~rollbackTargetBlock) => {
|
|
113
|
+
sourceManager.sourcesState->Array.forEach(({source}) => {
|
|
114
|
+
switch source.onReorg {
|
|
115
|
+
| Some(cb) => cb(~rollbackTargetBlock)
|
|
116
|
+
| None => ()
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
39
121
|
type sourceRole = Primary | Secondary
|
|
40
122
|
|
|
41
123
|
// Determines whether a source is Primary or Secondary given the current mode.
|
|
@@ -120,6 +202,10 @@ let make = (
|
|
|
120
202
|
statusStart: Hrtime.makeTimer(),
|
|
121
203
|
status: Idle,
|
|
122
204
|
hasRealtime,
|
|
205
|
+
committedRateLimitTimeMs: 0.0,
|
|
206
|
+
rateLimitWaiters: 0,
|
|
207
|
+
activeRateLimitStartMs: None,
|
|
208
|
+
activeRateLimitResetAtMs: None,
|
|
123
209
|
}
|
|
124
210
|
}
|
|
125
211
|
|
|
@@ -242,14 +328,18 @@ let getSourceNewHeight = async (
|
|
|
242
328
|
let retry = ref(0)
|
|
243
329
|
|
|
244
330
|
while newHeight.contents <= knownHeight && status.contents !== Done {
|
|
245
|
-
// If subscription exists, wait for next height event
|
|
246
331
|
switch sourceState.unsubscribe {
|
|
247
332
|
| Some(_) =>
|
|
248
333
|
let subscriptionPromise = Promise.make((resolve, _reject) => {
|
|
249
334
|
sourceState.pendingHeightResolvers->Array.push(resolve)
|
|
250
335
|
})
|
|
251
|
-
// If subscription goes quiet for half the stall timeout, fall back to REST
|
|
252
|
-
|
|
336
|
+
// If the subscription goes quiet for half the stall timeout, fall back to REST
|
|
337
|
+
// polling. Jitter the trigger across [stallTimeout/2, stallTimeout) so indexers
|
|
338
|
+
// that go quiet together don't all start polling at the same instant.
|
|
339
|
+
let half = stallTimeout / 2
|
|
340
|
+
let pollingFallback = Utils.delay(
|
|
341
|
+
half + (Math.random() *. half->Belt.Int.toFloat)->Belt.Float.toInt,
|
|
342
|
+
)->Promise.then(async () => {
|
|
253
343
|
logger->Logging.childTrace({
|
|
254
344
|
"msg": "onHeight subscription stale, switching to polling fallback",
|
|
255
345
|
"source": source.name,
|
|
@@ -288,11 +378,15 @@ let getSourceNewHeight = async (
|
|
|
288
378
|
switch source.createHeightSubscription {
|
|
289
379
|
| Some(createSubscription) if isRealtime =>
|
|
290
380
|
let unsubscribe = createSubscription(~onHeight=newHeight => {
|
|
291
|
-
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
sourceState.
|
|
295
|
-
|
|
381
|
+
// Ignore non-increasing heights. The height stream re-emits the current
|
|
382
|
+
// head on every (re)connect; waking the wait loop on a height we already
|
|
383
|
+
// know spins it and leaks fallback pollers (#1270).
|
|
384
|
+
if newHeight > sourceState.knownHeight {
|
|
385
|
+
sourceState.knownHeight = newHeight
|
|
386
|
+
let resolvers = sourceState.pendingHeightResolvers
|
|
387
|
+
sourceState.pendingHeightResolvers = []
|
|
388
|
+
resolvers->Array.forEach(resolve => resolve(newHeight))
|
|
389
|
+
}
|
|
296
390
|
})
|
|
297
391
|
sourceState.unsubscribe = Some(unsubscribe)
|
|
298
392
|
| _ =>
|
|
@@ -599,6 +693,10 @@ let executeQuery = async (
|
|
|
599
693
|
sourceState.lastFailedAt = None
|
|
600
694
|
responseRef := Some(response)
|
|
601
695
|
} catch {
|
|
696
|
+
| Source.RateLimited({resetMs}) =>
|
|
697
|
+
await sourceManager->waitForRateLimitReset(~resetMs, ~retry, ~logger)
|
|
698
|
+
retryRef := retryRef.contents + 1
|
|
699
|
+
|
|
602
700
|
| Source.GetItemsError(error) =>
|
|
603
701
|
switch error {
|
|
604
702
|
| UnsupportedSelection(_)
|
|
@@ -699,3 +797,74 @@ let executeQuery = async (
|
|
|
699
797
|
|
|
700
798
|
responseRef.contents->Option.getUnsafe
|
|
701
799
|
}
|
|
800
|
+
|
|
801
|
+
let getBlockHashes = async (sourceManager: t, ~blockNumbers: array<int>, ~isRealtime: bool) => {
|
|
802
|
+
let responseRef = ref(None)
|
|
803
|
+
let retryRef = ref(0)
|
|
804
|
+
|
|
805
|
+
while responseRef.contents->Option.isNone {
|
|
806
|
+
let sourceState = switch sourceManager->getNextSource(~isRealtime) {
|
|
807
|
+
| Some(s) => s
|
|
808
|
+
| None =>
|
|
809
|
+
let logger = Logging.createChild(
|
|
810
|
+
~params={"chainId": sourceManager.activeSource.chain->ChainMap.Chain.toChainId},
|
|
811
|
+
)
|
|
812
|
+
%raw(`null`)->ErrorHandling.mkLogAndRaise(
|
|
813
|
+
~logger,
|
|
814
|
+
~msg="No data-sources available for fetching block hashes.",
|
|
815
|
+
)
|
|
816
|
+
}
|
|
817
|
+
sourceManager.activeSource = sourceState.source
|
|
818
|
+
let source = sourceState.source
|
|
819
|
+
let retry = retryRef.contents
|
|
820
|
+
|
|
821
|
+
let logger = Logging.createChild(
|
|
822
|
+
~params={
|
|
823
|
+
"chainId": source.chain->ChainMap.Chain.toChainId,
|
|
824
|
+
"logType": "Block Hash Query",
|
|
825
|
+
"source": source.name,
|
|
826
|
+
"retry": retry,
|
|
827
|
+
},
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
try {
|
|
831
|
+
let res = await source.getBlockHashes(~blockNumbers, ~logger)
|
|
832
|
+
switch res {
|
|
833
|
+
| Ok(data) =>
|
|
834
|
+
sourceState.lastFailedAt = None
|
|
835
|
+
responseRef := Some(data)
|
|
836
|
+
| Error(exn) => throw(exn)
|
|
837
|
+
}
|
|
838
|
+
} catch {
|
|
839
|
+
| Source.RateLimited({resetMs}) =>
|
|
840
|
+
await sourceManager->waitForRateLimitReset(~resetMs, ~retry, ~logger)
|
|
841
|
+
retryRef := retryRef.contents + 1
|
|
842
|
+
|
|
843
|
+
| exn =>
|
|
844
|
+
let backoffMillis = switch retry {
|
|
845
|
+
| 0 => 500
|
|
846
|
+
| _ => 1000 * retry
|
|
847
|
+
}
|
|
848
|
+
let log = retry >= 4 ? Logging.childWarn : Logging.childTrace
|
|
849
|
+
logger->log({
|
|
850
|
+
"msg": "Failed to fetch block hashes. Retrying.",
|
|
851
|
+
"retry": retry,
|
|
852
|
+
"backOffMilliseconds": backoffMillis,
|
|
853
|
+
"err": exn->Utils.prettifyExn,
|
|
854
|
+
})
|
|
855
|
+
|
|
856
|
+
let shouldSwitch = switch retry {
|
|
857
|
+
| 0 | 1 => false
|
|
858
|
+
| _ => retry->mod(2) === 0
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if shouldSwitch {
|
|
862
|
+
sourceState.lastFailedAt = Some(Date.now())
|
|
863
|
+
}
|
|
864
|
+
await Utils.delay(Pervasives.min(backoffMillis, 60_000))
|
|
865
|
+
retryRef := retryRef.contents + 1
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
responseRef.contents->Option.getUnsafe
|
|
870
|
+
}
|
|
@@ -10,6 +10,7 @@ import * as ErrorHandling from "../ErrorHandling.res.mjs";
|
|
|
10
10
|
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
11
11
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
12
12
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
13
|
+
import * as Primitive_float from "@rescript/runtime/lib/es6/Primitive_float.js";
|
|
13
14
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
14
15
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
15
16
|
|
|
@@ -17,6 +18,70 @@ function getActiveSource(sourceManager) {
|
|
|
17
18
|
return sourceManager.activeSource;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function getRateLimitTimeMs(sourceManager) {
|
|
22
|
+
let startMs = sourceManager.activeRateLimitStartMs;
|
|
23
|
+
return sourceManager.committedRateLimitTimeMs + (
|
|
24
|
+
startMs !== undefined ? Date.now() - startMs : 0.0
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isRateLimited(sourceManager) {
|
|
29
|
+
return Stdlib_Option.isSome(sourceManager.activeRateLimitStartMs);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getRateLimitResetInMs(sourceManager) {
|
|
33
|
+
let resetAt = sourceManager.activeRateLimitResetAtMs;
|
|
34
|
+
if (resetAt === undefined) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
let remaining = resetAt - Date.now();
|
|
38
|
+
if (remaining > 0.0) {
|
|
39
|
+
return remaining;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function startRateLimitTimeout(sourceManager, resetMs) {
|
|
44
|
+
let now = Date.now();
|
|
45
|
+
if (sourceManager.rateLimitWaiters === 0) {
|
|
46
|
+
sourceManager.activeRateLimitStartMs = now;
|
|
47
|
+
}
|
|
48
|
+
let resetAt = now + resetMs;
|
|
49
|
+
let existing = sourceManager.activeRateLimitResetAtMs;
|
|
50
|
+
sourceManager.activeRateLimitResetAtMs = existing !== undefined ? Primitive_float.max(existing, resetAt) : resetAt;
|
|
51
|
+
sourceManager.rateLimitWaiters = sourceManager.rateLimitWaiters + 1 | 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function waitForRateLimitReset(sourceManager, resetMs, retry, logger) {
|
|
55
|
+
let waitMs = Primitive_int.min(resetMs, 300000);
|
|
56
|
+
let log = retry >= 2 ? Logging.childWarn : Logging.childTrace;
|
|
57
|
+
log(logger, {
|
|
58
|
+
msg: `HyperSync source is rate-limited — not critical, the indexer will retry in ` + (waitMs / 1000 | 0).toString() + `s. For higher limits upgrade your plan at https://envio.dev/app/api-tokens.`,
|
|
59
|
+
retry: retry,
|
|
60
|
+
waitMs: waitMs
|
|
61
|
+
});
|
|
62
|
+
startRateLimitTimeout(sourceManager, waitMs);
|
|
63
|
+
await Utils.delay(waitMs);
|
|
64
|
+
sourceManager.rateLimitWaiters = sourceManager.rateLimitWaiters - 1 | 0;
|
|
65
|
+
if (sourceManager.rateLimitWaiters !== 0) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
let startMs = sourceManager.activeRateLimitStartMs;
|
|
69
|
+
if (startMs !== undefined) {
|
|
70
|
+
sourceManager.committedRateLimitTimeMs = sourceManager.committedRateLimitTimeMs + Date.now() - startMs;
|
|
71
|
+
sourceManager.activeRateLimitStartMs = undefined;
|
|
72
|
+
}
|
|
73
|
+
sourceManager.activeRateLimitResetAtMs = undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function onReorg(sourceManager, rollbackTargetBlock) {
|
|
77
|
+
sourceManager.sourcesState.forEach(param => {
|
|
78
|
+
let cb = param.source.onReorg;
|
|
79
|
+
if (cb !== undefined) {
|
|
80
|
+
return cb(rollbackTargetBlock);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
20
85
|
function getSourceRole(sourceFor, isRealtime, hasRealtime) {
|
|
21
86
|
if (isRealtime) {
|
|
22
87
|
switch (sourceFor) {
|
|
@@ -83,7 +148,11 @@ function make(sources, maxPartitionConcurrency, isRealtime, newBlockStallTimeout
|
|
|
83
148
|
waitingForNewBlockStateId: undefined,
|
|
84
149
|
fetchingPartitionsCount: 0,
|
|
85
150
|
recoveryTimeout: recoveryTimeout,
|
|
86
|
-
hasRealtime: hasRealtime
|
|
151
|
+
hasRealtime: hasRealtime,
|
|
152
|
+
committedRateLimitTimeMs: 0.0,
|
|
153
|
+
rateLimitWaiters: 0,
|
|
154
|
+
activeRateLimitStartMs: undefined,
|
|
155
|
+
activeRateLimitResetAtMs: undefined
|
|
87
156
|
};
|
|
88
157
|
}
|
|
89
158
|
|
|
@@ -186,7 +255,8 @@ async function getSourceNewHeight(sourceManager, sourceState, knownHeight, stall
|
|
|
186
255
|
let subscriptionPromise = new Promise((resolve, _reject) => {
|
|
187
256
|
sourceState.pendingHeightResolvers.push(resolve);
|
|
188
257
|
});
|
|
189
|
-
let
|
|
258
|
+
let half = stallTimeout / 2 | 0;
|
|
259
|
+
let pollingFallback = Utils.delay(half + (Math.random() * half | 0) | 0).then(async () => {
|
|
190
260
|
Logging.childTrace(logger, {
|
|
191
261
|
msg: "onHeight subscription stale, switching to polling fallback",
|
|
192
262
|
source: source.name,
|
|
@@ -222,6 +292,9 @@ async function getSourceNewHeight(sourceManager, sourceState, knownHeight, stall
|
|
|
222
292
|
let exit = 0;
|
|
223
293
|
if (createSubscription !== undefined && isRealtime) {
|
|
224
294
|
let unsubscribe = createSubscription(newHeight => {
|
|
295
|
+
if (newHeight <= sourceState.knownHeight) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
225
298
|
sourceState.knownHeight = newHeight;
|
|
226
299
|
let resolvers = sourceState.pendingHeightResolvers;
|
|
227
300
|
sourceState.pendingHeightResolvers = [];
|
|
@@ -440,7 +513,10 @@ async function executeQuery(sourceManager, query, knownHeight, isRealtime) {
|
|
|
440
513
|
responseRef = response;
|
|
441
514
|
} catch (raw_error) {
|
|
442
515
|
let error = Primitive_exceptions.internalToException(raw_error);
|
|
443
|
-
if (error.RE_EXN_ID === Source.
|
|
516
|
+
if (error.RE_EXN_ID === Source.RateLimited) {
|
|
517
|
+
await waitForRateLimitReset(sourceManager, error.resetMs, retry, logger$2);
|
|
518
|
+
retryRef = retryRef + 1 | 0;
|
|
519
|
+
} else if (error.RE_EXN_ID === Source.GetItemsError) {
|
|
444
520
|
let error$1 = error._1;
|
|
445
521
|
let exit = 0;
|
|
446
522
|
switch (error$1.TAG) {
|
|
@@ -543,13 +619,75 @@ async function executeQuery(sourceManager, query, knownHeight, isRealtime) {
|
|
|
543
619
|
return responseRef;
|
|
544
620
|
}
|
|
545
621
|
|
|
622
|
+
async function getBlockHashes(sourceManager, blockNumbers, isRealtime) {
|
|
623
|
+
let responseRef;
|
|
624
|
+
let retryRef = 0;
|
|
625
|
+
while (Stdlib_Option.isNone(responseRef)) {
|
|
626
|
+
let s = getNextSource(sourceManager, isRealtime, undefined);
|
|
627
|
+
let sourceState;
|
|
628
|
+
if (s !== undefined) {
|
|
629
|
+
sourceState = s;
|
|
630
|
+
} else {
|
|
631
|
+
let logger = Logging.createChild({
|
|
632
|
+
chainId: sourceManager.activeSource.chain
|
|
633
|
+
});
|
|
634
|
+
sourceState = ErrorHandling.mkLogAndRaise(logger, "No data-sources available for fetching block hashes.", null);
|
|
635
|
+
}
|
|
636
|
+
sourceManager.activeSource = sourceState.source;
|
|
637
|
+
let source = sourceState.source;
|
|
638
|
+
let retry = retryRef;
|
|
639
|
+
let logger$1 = Logging.createChild({
|
|
640
|
+
chainId: source.chain,
|
|
641
|
+
logType: "Block Hash Query",
|
|
642
|
+
source: source.name,
|
|
643
|
+
retry: retry
|
|
644
|
+
});
|
|
645
|
+
try {
|
|
646
|
+
let res = await source.getBlockHashes(blockNumbers, logger$1);
|
|
647
|
+
if (res.TAG === "Ok") {
|
|
648
|
+
sourceState.lastFailedAt = undefined;
|
|
649
|
+
responseRef = res._0;
|
|
650
|
+
} else {
|
|
651
|
+
throw res._0;
|
|
652
|
+
}
|
|
653
|
+
} catch (raw_exn) {
|
|
654
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
655
|
+
if (exn.RE_EXN_ID === Source.RateLimited) {
|
|
656
|
+
await waitForRateLimitReset(sourceManager, exn.resetMs, retry, logger$1);
|
|
657
|
+
retryRef = retryRef + 1 | 0;
|
|
658
|
+
} else {
|
|
659
|
+
let backoffMillis = retry !== 0 ? 1000 * retry | 0 : 500;
|
|
660
|
+
let log = retry >= 4 ? Logging.childWarn : Logging.childTrace;
|
|
661
|
+
log(logger$1, {
|
|
662
|
+
msg: "Failed to fetch block hashes. Retrying.",
|
|
663
|
+
retry: retry,
|
|
664
|
+
backOffMilliseconds: backoffMillis,
|
|
665
|
+
err: Utils.prettifyExn(exn)
|
|
666
|
+
});
|
|
667
|
+
let shouldSwitch = retry !== 0 && retry !== 1 ? retry % 2 === 0 : false;
|
|
668
|
+
if (shouldSwitch) {
|
|
669
|
+
sourceState.lastFailedAt = Date.now();
|
|
670
|
+
}
|
|
671
|
+
await Utils.delay(Primitive_int.min(backoffMillis, 60000));
|
|
672
|
+
retryRef = retryRef + 1 | 0;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
return responseRef;
|
|
677
|
+
}
|
|
678
|
+
|
|
546
679
|
export {
|
|
547
680
|
getSourceRole,
|
|
548
681
|
make,
|
|
549
682
|
getActiveSource,
|
|
683
|
+
onReorg,
|
|
550
684
|
fetchNext,
|
|
551
685
|
waitForNewBlock,
|
|
552
686
|
executeQuery,
|
|
553
687
|
makeGetHeightRetryInterval,
|
|
688
|
+
getBlockHashes,
|
|
689
|
+
getRateLimitTimeMs,
|
|
690
|
+
isRateLimited,
|
|
691
|
+
getRateLimitResetInMs,
|
|
554
692
|
}
|
|
555
693
|
/* Utils Not a pure module */
|
|
@@ -22,6 +22,8 @@ let make: (
|
|
|
22
22
|
|
|
23
23
|
let getActiveSource: t => Source.t
|
|
24
24
|
|
|
25
|
+
let onReorg: (t, ~rollbackTargetBlock: int) => unit
|
|
26
|
+
|
|
25
27
|
let fetchNext: (
|
|
26
28
|
t,
|
|
27
29
|
~fetchState: FetchState.t,
|
|
@@ -50,3 +52,20 @@ let makeGetHeightRetryInterval: (
|
|
|
50
52
|
~backoffMultiplicative: int,
|
|
51
53
|
~maxRetryInterval: int,
|
|
52
54
|
) => (~retry: int) => int
|
|
55
|
+
|
|
56
|
+
let getBlockHashes: (
|
|
57
|
+
t,
|
|
58
|
+
~blockNumbers: array<int>,
|
|
59
|
+
~isRealtime: bool,
|
|
60
|
+
) => promise<array<ReorgDetection.blockDataWithTimestamp>>
|
|
61
|
+
|
|
62
|
+
// Total time the indexer spent waiting on rate limits, including any
|
|
63
|
+
// currently-in-progress window measured against Date.now() at call time.
|
|
64
|
+
let getRateLimitTimeMs: t => float
|
|
65
|
+
|
|
66
|
+
// True if at least one request is currently waiting on a rate-limit reset.
|
|
67
|
+
let isRateLimited: t => bool
|
|
68
|
+
|
|
69
|
+
// Milliseconds remaining until the longest active rate-limit window resets,
|
|
70
|
+
// or None if not currently waiting (or the window has already elapsed).
|
|
71
|
+
let getRateLimitResetInMs: t => option<float>
|
package/src/tui/Tui.res
CHANGED
|
@@ -92,7 +92,7 @@ module EventsPerSecond = {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
let use = (~totalEventsProcessed: float) => {
|
|
95
|
+
let use = (~totalEventsProcessed: float, ~tick: int) => {
|
|
96
96
|
let (samples, setSamples) = React.useState((): array<sample> => [])
|
|
97
97
|
|
|
98
98
|
React.useEffect1(() => {
|
|
@@ -103,7 +103,7 @@ module EventsPerSecond = {
|
|
|
103
103
|
kept->Array.concat([{time: now, events: totalEventsProcessed}])
|
|
104
104
|
})
|
|
105
105
|
None
|
|
106
|
-
}, [
|
|
106
|
+
}, [tick])
|
|
107
107
|
|
|
108
108
|
computeEps(samples)
|
|
109
109
|
}
|
|
@@ -132,12 +132,15 @@ module App = {
|
|
|
132
132
|
@react.component
|
|
133
133
|
let make = (~getState) => {
|
|
134
134
|
let stdoutColumns = Hooks.useStdoutColumns()
|
|
135
|
-
|
|
135
|
+
// GlobalState is mutated in place — passing the same ref to useState
|
|
136
|
+
// would bail out via Object.is and skip the re-render. Tick a counter
|
|
137
|
+
// instead and read state freshly from getState() on every render.
|
|
138
|
+
let (tick, setTick) = React.useState(() => 0)
|
|
139
|
+
let state: GlobalState.t = getState()
|
|
136
140
|
|
|
137
|
-
// useEffect to refresh state every 500ms
|
|
138
141
|
React.useEffect(() => {
|
|
139
142
|
let intervalId = setInterval(() => {
|
|
140
|
-
|
|
143
|
+
setTick(t => t + 1)
|
|
141
144
|
}, 500)
|
|
142
145
|
|
|
143
146
|
Some(
|
|
@@ -210,6 +213,9 @@ module App = {
|
|
|
210
213
|
poweredByHyperSync: (
|
|
211
214
|
cf.sourceManager->SourceManager.getActiveSource
|
|
212
215
|
).poweredByHyperSync,
|
|
216
|
+
rateLimitTimeMs: cf.sourceManager->SourceManager.getRateLimitTimeMs,
|
|
217
|
+
isRateLimited: cf.sourceManager->SourceManager.isRateLimited,
|
|
218
|
+
rateLimitResetInMs: cf.sourceManager->SourceManager.getRateLimitResetInMs,
|
|
213
219
|
}: TuiData.chain
|
|
214
220
|
)
|
|
215
221
|
})
|
|
@@ -225,7 +231,7 @@ module App = {
|
|
|
225
231
|
acc
|
|
226
232
|
}
|
|
227
233
|
})
|
|
228
|
-
let eventsPerSecond = EventsPerSecond.use(~totalEventsProcessed)
|
|
234
|
+
let eventsPerSecond = EventsPerSecond.use(~totalEventsProcessed, ~tick)
|
|
229
235
|
|
|
230
236
|
<Box flexDirection={Column}>
|
|
231
237
|
<BigText
|
|
@@ -257,6 +263,38 @@ module App = {
|
|
|
257
263
|
eventsPerSecond={SyncETA.isIndexerFullySynced(chains) ? None : eventsPerSecond}
|
|
258
264
|
/>
|
|
259
265
|
<SyncETA chains indexerStartTime=state.indexerStartTime />
|
|
266
|
+
{
|
|
267
|
+
let maxRateLimitTimeMs =
|
|
268
|
+
chains->Array.reduce(0., (acc, chain) => Pervasives.max(acc, chain.rateLimitTimeMs))
|
|
269
|
+
let maxResetInMs =
|
|
270
|
+
chains->Array.reduce(0.0, (acc, chain) =>
|
|
271
|
+
Pervasives.max(acc, chain.rateLimitResetInMs->Option.getOr(0.0))
|
|
272
|
+
)
|
|
273
|
+
maxRateLimitTimeMs > 1000.
|
|
274
|
+
? {
|
|
275
|
+
let rateLimitSecs = Math.round(maxRateLimitTimeMs /. 1000.)
|
|
276
|
+
let activeSuffix = if maxResetInMs > 0.0 {
|
|
277
|
+
let resetSecs = Pervasives.max(1.0, Math.ceil(maxResetInMs /. 1000.))
|
|
278
|
+
` (⏳ ${resetSecs->TuiData.formatFloatLocaleString}s until reset)`
|
|
279
|
+
} else {
|
|
280
|
+
""
|
|
281
|
+
}
|
|
282
|
+
<Box flexDirection={Column}>
|
|
283
|
+
<Newline />
|
|
284
|
+
<Text color={Danger}>
|
|
285
|
+
{`Backfill ${rateLimitSecs->TuiData.formatFloatLocaleString}s slower due to your plan's rate limit${activeSuffix}`->React.string}
|
|
286
|
+
</Text>
|
|
287
|
+
<Text color={Danger}>
|
|
288
|
+
<Text color={Danger}> {"Upgrade at "->React.string} </Text>
|
|
289
|
+
<Text color={Danger} underline=true>
|
|
290
|
+
{"https://envio.dev/app/api-tokens"->React.string}
|
|
291
|
+
</Text>
|
|
292
|
+
<Text color={Danger}> {" for higher rate limits."->React.string} </Text>
|
|
293
|
+
</Text>
|
|
294
|
+
</Box>
|
|
295
|
+
}
|
|
296
|
+
: React.null
|
|
297
|
+
}
|
|
260
298
|
<Newline />
|
|
261
299
|
<Box flexDirection={Row}>
|
|
262
300
|
<Text> {"GraphQL: "->React.string} </Text>
|