envio 3.3.0-alpha.1 → 3.3.0-alpha.11

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 (143) hide show
  1. package/evm.schema.json +10 -0
  2. package/index.d.ts +45 -25
  3. package/licenses/CLA.md +35 -0
  4. package/licenses/EULA.md +67 -0
  5. package/licenses/LICENSE.md +45 -0
  6. package/licenses/README.md +35 -0
  7. package/package.json +10 -9
  8. package/src/Address.res +5 -2
  9. package/src/Address.res.mjs +3 -1
  10. package/src/Api.res +1 -1
  11. package/src/Api.res.mjs +1 -1
  12. package/src/Batch.res +0 -6
  13. package/src/Batch.res.mjs +1 -12
  14. package/src/BatchProcessing.res +7 -12
  15. package/src/BatchProcessing.res.mjs +8 -7
  16. package/src/Bin.res +3 -0
  17. package/src/Bin.res.mjs +4 -0
  18. package/src/ChainFetching.res +53 -16
  19. package/src/ChainFetching.res.mjs +37 -19
  20. package/src/ChainMetadata.res +1 -11
  21. package/src/ChainMetadata.res.mjs +1 -10
  22. package/src/ChainState.res +630 -161
  23. package/src/ChainState.res.mjs +438 -101
  24. package/src/ChainState.resi +77 -4
  25. package/src/Config.res +70 -38
  26. package/src/Config.res.mjs +52 -12
  27. package/src/ContractRegisterContext.res +2 -12
  28. package/src/ContractRegisterContext.res.mjs +3 -5
  29. package/src/Core.res +34 -3
  30. package/src/Core.res.mjs +12 -0
  31. package/src/CrossChainState.res +130 -89
  32. package/src/CrossChainState.res.mjs +66 -61
  33. package/src/CrossChainState.resi +1 -1
  34. package/src/Ecosystem.res +5 -6
  35. package/src/Ecosystem.res.mjs +3 -3
  36. package/src/Envio.res +17 -11
  37. package/src/EnvioGlobal.res +53 -0
  38. package/src/EnvioGlobal.res.mjs +31 -0
  39. package/src/EventConfigBuilder.res +191 -127
  40. package/src/EventConfigBuilder.res.mjs +116 -66
  41. package/src/EventProcessing.res +77 -28
  42. package/src/EventProcessing.res.mjs +56 -29
  43. package/src/ExitOnCaughtUp.res +10 -2
  44. package/src/ExitOnCaughtUp.res.mjs +10 -1
  45. package/src/FetchState.res +684 -415
  46. package/src/FetchState.res.mjs +505 -486
  47. package/src/HandlerLoader.res +8 -104
  48. package/src/HandlerLoader.res.mjs +2 -81
  49. package/src/HandlerRegister.res +520 -144
  50. package/src/HandlerRegister.res.mjs +286 -133
  51. package/src/HandlerRegister.resi +24 -8
  52. package/src/IndexerState.res +10 -3
  53. package/src/IndexerState.res.mjs +11 -4
  54. package/src/IndexerState.resi +2 -1
  55. package/src/IndexingAddresses.res +108 -0
  56. package/src/IndexingAddresses.res.mjs +101 -0
  57. package/src/IndexingAddresses.resi +34 -0
  58. package/src/Internal.res +166 -48
  59. package/src/Internal.res.mjs +30 -2
  60. package/src/LoadLayer.res +5 -5
  61. package/src/LoadLayer.res.mjs +6 -6
  62. package/src/LogSelection.res +92 -217
  63. package/src/LogSelection.res.mjs +95 -184
  64. package/src/Main.res +71 -236
  65. package/src/Main.res.mjs +60 -145
  66. package/src/Metrics.res +74 -0
  67. package/src/Metrics.res.mjs +75 -0
  68. package/src/PgStorage.res +4 -4
  69. package/src/PgStorage.res.mjs +5 -5
  70. package/src/Prometheus.res +10 -63
  71. package/src/Prometheus.res.mjs +124 -171
  72. package/src/PruneStaleHistory.res +2 -2
  73. package/src/PruneStaleHistory.res.mjs +3 -3
  74. package/src/RawEvent.res +7 -2
  75. package/src/RawEvent.res.mjs +4 -4
  76. package/src/Rollback.res +3 -3
  77. package/src/Rollback.res.mjs +4 -4
  78. package/src/RollbackCommit.res +4 -1
  79. package/src/RollbackCommit.res.mjs +3 -2
  80. package/src/SimulateDeadInputTracker.res +85 -0
  81. package/src/SimulateDeadInputTracker.res.mjs +73 -0
  82. package/src/SimulateDeadInputTracker.resi +12 -0
  83. package/src/SimulateItems.res +128 -28
  84. package/src/SimulateItems.res.mjs +82 -40
  85. package/src/TestIndexer.res +55 -41
  86. package/src/TestIndexer.res.mjs +39 -32
  87. package/src/TopicFilter.res +1 -25
  88. package/src/TopicFilter.res.mjs +0 -74
  89. package/src/bindings/Performance.res +7 -0
  90. package/src/bindings/Performance.res.mjs +21 -0
  91. package/src/bindings/Performance.resi +7 -0
  92. package/src/bindings/Viem.res +0 -46
  93. package/src/bindings/Viem.res.mjs +1 -43
  94. package/src/sources/BlockStore.res +46 -0
  95. package/src/sources/BlockStore.res.mjs +24 -0
  96. package/src/sources/EventRouter.res +31 -49
  97. package/src/sources/EventRouter.res.mjs +12 -25
  98. package/src/sources/Evm.res +83 -72
  99. package/src/sources/Evm.res.mjs +53 -69
  100. package/src/sources/EvmChain.res +7 -34
  101. package/src/sources/EvmChain.res.mjs +6 -27
  102. package/src/sources/EvmRpcClient.res +71 -5
  103. package/src/sources/EvmRpcClient.res.mjs +13 -4
  104. package/src/sources/FieldMask.res +39 -0
  105. package/src/sources/FieldMask.res.mjs +42 -0
  106. package/src/sources/Fuel.res +9 -6
  107. package/src/sources/Fuel.res.mjs +5 -10
  108. package/src/sources/HyperFuelSource.res +46 -50
  109. package/src/sources/HyperFuelSource.res.mjs +64 -54
  110. package/src/sources/HyperSync.res +45 -42
  111. package/src/sources/HyperSync.res.mjs +68 -60
  112. package/src/sources/HyperSync.resi +14 -4
  113. package/src/sources/HyperSyncClient.res +107 -119
  114. package/src/sources/HyperSyncClient.res.mjs +36 -31
  115. package/src/sources/HyperSyncHeightStream.res +1 -8
  116. package/src/sources/HyperSyncHeightStream.res.mjs +0 -2
  117. package/src/sources/HyperSyncSource.res +60 -254
  118. package/src/sources/HyperSyncSource.res.mjs +57 -180
  119. package/src/sources/Rpc.res +15 -79
  120. package/src/sources/Rpc.res.mjs +25 -101
  121. package/src/sources/RpcSource.res +221 -514
  122. package/src/sources/RpcSource.res.mjs +216 -411
  123. package/src/sources/RpcWebSocketHeightStream.res +0 -5
  124. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -2
  125. package/src/sources/SimulateSource.res +44 -20
  126. package/src/sources/SimulateSource.res.mjs +41 -14
  127. package/src/sources/Source.res +34 -6
  128. package/src/sources/SourceManager.res +77 -19
  129. package/src/sources/SourceManager.res.mjs +63 -21
  130. package/src/sources/SourceManager.resi +10 -0
  131. package/src/sources/Svm.res +31 -16
  132. package/src/sources/Svm.res.mjs +36 -13
  133. package/src/sources/SvmHyperSyncClient.res +5 -32
  134. package/src/sources/SvmHyperSyncSource.res +159 -134
  135. package/src/sources/SvmHyperSyncSource.res.mjs +164 -129
  136. package/src/sources/TransactionStore.res +49 -0
  137. package/src/sources/TransactionStore.res.mjs +30 -0
  138. package/src/tui/Tui.res +13 -16
  139. package/src/tui/Tui.res.mjs +12 -14
  140. package/svm.schema.json +49 -37
  141. package/src/bindings/Hrtime.res +0 -58
  142. package/src/bindings/Hrtime.res.mjs +0 -90
  143. package/src/bindings/Hrtime.resi +0 -30
@@ -1,7 +1,5 @@
1
1
  open Source
2
2
 
3
- exception QueryTimout(string)
4
-
5
3
  // eth_getTransactionByHash/eth_getTransactionReceipt returning null is usually
6
4
  // transient: a load-balanced provider can route the lookup to a node that
7
5
  // hasn't caught up with the one that served eth_getLogs. Must stay retryable,
@@ -48,18 +46,16 @@ let getKnownRawBlockWithBackoff = async (
48
46
  ~chain,
49
47
  ~blockNumber,
50
48
  ~backoffMsOnFailure,
49
+ ~recordRequest: (~method: string, ~seconds: float) => unit,
51
50
  ) => {
52
51
  let currentBackoff = ref(backoffMsOnFailure)
53
52
  let result = ref(None)
54
53
 
55
54
  while result.contents->Option.isNone {
56
- Prometheus.SourceRequestCount.increment(
57
- ~sourceName,
58
- ~chainId=chain->ChainMap.Chain.toChainId,
59
- ~method="eth_getBlockByNumber",
60
- )
55
+ let timerRef = Performance.now()
61
56
  switch await getKnownRawBlock(~client, ~blockNumber) {
62
57
  | exception err =>
58
+ recordRequest(~method="eth_getBlockByNumber", ~seconds=timerRef->Performance.secondsSince)
63
59
  Logging.warn({
64
60
  "err": err->Utils.prettifyExn,
65
61
  "msg": `Issue while running fetching batch of events from the RPC. Will wait ${currentBackoff.contents->Int.toString}ms and try again.`,
@@ -69,328 +65,98 @@ let getKnownRawBlockWithBackoff = async (
69
65
  })
70
66
  await Time.resolvePromiseAfterDelay(~delayMilliseconds=currentBackoff.contents)
71
67
  currentBackoff := currentBackoff.contents * 2
72
- | json => result := Some(json)
68
+ | json =>
69
+ recordRequest(~method="eth_getBlockByNumber", ~seconds=timerRef->Performance.secondsSince)
70
+ result := Some(json)
73
71
  }
74
72
  }
75
73
  result.contents->Option.getOrThrow
76
74
  }
77
- let getSuggestedBlockIntervalFromExn = {
78
- // Unknown provider: "retry with the range 123-456"
79
- let suggestedRangeRegExp = /retry with the range (\d+)-(\d+)/
80
-
81
- // QuickNode, 1RPC, Blast: "limited to a 1000 blocks range"
82
- let blockRangeLimitRegExp = /limited to a (\d+) blocks range/
83
-
84
- // Alchemy: "up to a 500 block range"
85
- let alchemyRangeRegExp = /up to a (\d+) block range/
86
-
87
- // Cloudflare: "Max range: 3500"
88
- let cloudflareRangeRegExp = /Max range: (\d+)/
89
-
90
- // Thirdweb: "Maximum allowed number of requested blocks is 3500"
91
- let thirdwebRangeRegExp = /Maximum allowed number of requested blocks is (\d+)/
92
-
93
- // BlockPI: "limited to 2000 block"
94
- let blockpiRangeRegExp = /limited to (\d+) block/
95
-
96
- // Base: "block range too large" - fixed 2000 block limit
97
- let baseRangeRegExp = /block range too large/
98
-
99
- // evm-rpc.sei-apis.com: "block range too large (2000), maximum allowed is 1000 blocks"
100
- let maxAllowedBlocksRegExp = /maximum allowed is (\d+) blocks/
101
-
102
- // Blast (paid): "exceeds the range allowed for your plan (5000 > 3000)"
103
- let blastPaidRegExp = /exceeds the range allowed for your plan \(\d+ > (\d+)\)/
104
-
105
- // Chainstack: "Block range limit exceeded" - 10000 block limit
106
- let chainstackRegExp = /Block range limit exceeded./
107
-
108
- // Coinbase: "please limit the query to at most 1000 blocks"
109
- let coinbaseRegExp = /please limit the query to at most (\d+) blocks/
110
-
111
- // PublicNode: "maximum block range: 2000"
112
- let publicNodeRegExp = /maximum block range: (\d+)/
113
-
114
- // Hyperliquid: "query exceeds max block range 1000"
115
- let hyperliquidRegExp = /query exceeds max block range (\d+)/
116
-
117
- // TODO: Reproduce how the error message looks like
118
- // when we send request with numeric block range instead of hex
119
- // Infura, ZkSync: "Try with this block range [0x123,0x456]"
120
-
121
- // Future handling needed for these providers that don't suggest ranges:
122
- // - Ankr: "block range is too wide"
123
- // - 1RPC: "response size should not greater than 10000000 bytes"
124
- // - ZkEVM: "query returned more than 10000 results"
125
- // - LlamaRPC: "query exceeds max results"
126
- // - Optimism: "backend response too large" or "Block range is too large"
127
- // - Arbitrum: "logs matched by query exceeds limit of 10000"
128
-
129
- let parseMessageForBlockRange = (message: string) => {
130
- // Helper to extract block range from regex match
131
- let extractBlockRange = (execResult, ~isMaxRange) =>
132
- switch execResult->RegExp.Result.matches {
133
- | [Some(blockRangeLimit)] =>
134
- switch blockRangeLimit->Int.fromString {
135
- | Some(blockRangeLimit) if blockRangeLimit > 0 => Some(blockRangeLimit, isMaxRange)
75
+ // Pulls the underlying provider error message back out of a caught exn, for
76
+ // logging/debugging. Provider JSON-RPC errors surface as `Rpc.JsonRpcError`;
77
+ // the paging retry decision (see `parseGetNextPageRetryError` below) surfaces
78
+ // as a napi `JsExn` whose message is the JSON payload `EvmRpcClient.getNextPage`
79
+ // throws, carrying the classified message (if any) under `errorMessage`.
80
+ let getErrorMessage = (exn: exn): option<string> =>
81
+ switch exn {
82
+ | Rpc.JsonRpcError({message}) => Some(message)
83
+ | JsExn(e) =>
84
+ switch e->JsExn.message {
85
+ | Some(msg) =>
86
+ switch msg->JSON.parseOrThrow->JSON.Decode.object {
87
+ | exception _ => None
88
+ | None => None
89
+ | Some(obj) =>
90
+ switch obj->Dict.get("errorMessage") {
91
+ | Some(String(message)) => Some(message)
136
92
  | _ => None
137
93
  }
138
- | _ => None
139
94
  }
95
+ | None => None
96
+ }
97
+ | _ => None
98
+ }
140
99
 
141
- // Try each regex pattern in order
142
- switch suggestedRangeRegExp->RegExp.exec(message) {
143
- | Some(execResult) =>
144
- switch execResult->RegExp.Result.matches {
145
- | [Some(fromBlock), Some(toBlock)] =>
146
- switch (fromBlock->Int.fromString, toBlock->Int.fromString) {
147
- | (Some(fromBlock), Some(toBlock)) if toBlock >= fromBlock =>
148
- Some(toBlock - fromBlock + 1, false)
149
- | _ => None
150
- }
151
- | _ => None
152
- }
153
- | None =>
154
- // Try each provider's specific error pattern
155
- switch blockRangeLimitRegExp->RegExp.exec(message) {
156
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
157
- | None =>
158
- switch alchemyRangeRegExp->RegExp.exec(message) {
159
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
160
- | None =>
161
- switch cloudflareRangeRegExp->RegExp.exec(message) {
162
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
163
- | None =>
164
- switch thirdwebRangeRegExp->RegExp.exec(message) {
165
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
166
- | None =>
167
- switch blockpiRangeRegExp->RegExp.exec(message) {
168
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
169
- | None =>
170
- switch maxAllowedBlocksRegExp->RegExp.exec(message) {
171
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
172
- | None =>
173
- switch baseRangeRegExp->RegExp.exec(message) {
174
- | Some(_) => Some(2000, true)
175
- | None =>
176
- switch blastPaidRegExp->RegExp.exec(message) {
177
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
178
- | None =>
179
- switch chainstackRegExp->RegExp.exec(message) {
180
- | Some(_) => Some(10000, true)
181
- | None =>
182
- switch coinbaseRegExp->RegExp.exec(message) {
183
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
184
- | None =>
185
- switch publicNodeRegExp->RegExp.exec(message) {
186
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
187
- | None =>
188
- switch hyperliquidRegExp->RegExp.exec(message) {
189
- | Some(execResult) => extractBlockRange(execResult, ~isMaxRange=true)
190
- | None => None
191
- }
192
- }
193
- }
194
- }
195
- }
196
- }
100
+
101
+ // `EvmRpcClient.getNextPage` throws a napi error whose message is a JSON
102
+ // payload describing the retry decision:
103
+ // `{"kind":"Retry","attemptedToBlock":int,"errorMessage":string|null,
104
+ // "requestStats":[{"method":string,"seconds":float}],"retry":
105
+ // {"tag":"WithSuggestedToBlock","toBlock":int} |
106
+ // {"tag":"WithBackoff","message":string,"backoffMillis":int}}`.
107
+ let parseGetNextPageRetryError = (exn: exn): option<(
108
+ int,
109
+ Source.getItemsRetry,
110
+ array<Source.requestStat>,
111
+ )> =>
112
+ switch exn {
113
+ | JsExn(e) =>
114
+ switch e->JsExn.message {
115
+ | Some(msg) =>
116
+ switch msg->JSON.parseOrThrow->JSON.Decode.object {
117
+ | exception _ => None
118
+ | None => None
119
+ | Some(obj) =>
120
+ switch (obj->Dict.get("kind"), obj->Dict.get("attemptedToBlock"), obj->Dict.get("retry")) {
121
+ | (Some(String("Retry")), Some(Number(attemptedToBlock)), Some(Object(retryObj))) =>
122
+ let requestStats = switch obj->Dict.get("requestStats") {
123
+ | Some(Array(stats)) =>
124
+ stats->Array.filterMap(s =>
125
+ switch s->JSON.Decode.object {
126
+ | Some(o) =>
127
+ switch (o->Dict.get("method"), o->Dict.get("seconds")) {
128
+ | (Some(String(method)), Some(Number(seconds))) => Some({Source.method, seconds})
129
+ | _ => None
197
130
  }
131
+ | None => None
198
132
  }
133
+ )
134
+ | _ => []
135
+ }
136
+ let retry = switch retryObj->Dict.get("tag") {
137
+ | Some(String("WithSuggestedToBlock")) =>
138
+ switch retryObj->Dict.get("toBlock") {
139
+ | Some(Number(toBlock)) =>
140
+ Some(Source.WithSuggestedToBlock({toBlock: toBlock->Float.toInt}))
141
+ | _ => None
199
142
  }
143
+ | Some(String("WithBackoff")) =>
144
+ switch (retryObj->Dict.get("message"), retryObj->Dict.get("backoffMillis")) {
145
+ | (Some(String(message)), Some(Number(backoffMillis))) =>
146
+ Some(Source.WithBackoff({message, backoffMillis: backoffMillis->Float.toInt}))
147
+ | _ => None
148
+ }
149
+ | _ => None
200
150
  }
151
+ retry->Option.map(retry => (attemptedToBlock->Float.toInt, retry, requestStats))
152
+ | _ => None
201
153
  }
202
154
  }
155
+ | None => None
203
156
  }
157
+ | _ => None
204
158
  }
205
159
 
206
- (exn): option<(
207
- // The suggested block range
208
- int,
209
- // Whether it's the max range that the provider allows
210
- bool,
211
- )> =>
212
- switch exn {
213
- | Rpc.JsonRpcError({message}) => parseMessageForBlockRange(message)
214
- | JsExn(error) =>
215
- try {
216
- let message: string = (error->Obj.magic)["error"]["message"]
217
- message->S.assertOrThrow(S.string)
218
- parseMessageForBlockRange(message)
219
- } catch {
220
- | _ => None
221
- }
222
- | _ => None
223
- }
224
- }
225
-
226
- type eventBatchQuery = {
227
- logs: array<Rpc.GetLogs.log>,
228
- latestFetchedBlockInfo: blockInfo,
229
- }
230
-
231
- let maxSuggestedBlockIntervalKey = "max"
232
-
233
- let getNextPage = (
234
- ~fromBlock,
235
- ~toBlock,
236
- ~addresses,
237
- ~topicQuery,
238
- ~loadBlock,
239
- ~syncConfig as sc: Config.sourceSync,
240
- ~client,
241
- ~mutSuggestedBlockIntervals,
242
- ~partitionId,
243
- ~sourceName,
244
- ~chainId,
245
- ): promise<eventBatchQuery> => {
246
- //If the query hangs for longer than this, reject this promise to reduce the block interval
247
- let queryTimoutPromise =
248
- Time.resolvePromiseAfterDelay(~delayMilliseconds=sc.queryTimeoutMillis)->Promise.then(() =>
249
- Promise.reject(
250
- QueryTimout(`Query took longer than ${Int.toString(sc.queryTimeoutMillis / 1000)} seconds`),
251
- )
252
- )
253
-
254
- let latestFetchedBlockPromise = loadBlock(toBlock)
255
- Prometheus.SourceRequestCount.increment(~sourceName, ~chainId, ~method="eth_getLogs")
256
- let logsPromise = Rpc.getLogs(
257
- ~client,
258
- ~param={
259
- address: ?addresses,
260
- topics: topicQuery,
261
- fromBlock,
262
- toBlock,
263
- },
264
- )->Promise.then(async logs => {
265
- {
266
- logs,
267
- latestFetchedBlockInfo: await latestFetchedBlockPromise,
268
- }
269
- })
270
-
271
- [queryTimoutPromise, logsPromise]
272
- ->Promise.race
273
- ->Promise.catch(err => {
274
- switch getSuggestedBlockIntervalFromExn(err) {
275
- | Some((nextBlockIntervalTry, isMaxRange)) =>
276
- mutSuggestedBlockIntervals->Dict.set(
277
- isMaxRange ? maxSuggestedBlockIntervalKey : partitionId,
278
- nextBlockIntervalTry,
279
- )
280
- throw(
281
- Source.GetItemsError(
282
- FailedGettingItems({
283
- exn: err,
284
- attemptedToBlock: toBlock,
285
- retry: WithSuggestedToBlock({
286
- toBlock: fromBlock + nextBlockIntervalTry - 1,
287
- }),
288
- }),
289
- ),
290
- )
291
- | None =>
292
- let executedBlockInterval = toBlock - fromBlock + 1
293
- let nextBlockIntervalTry =
294
- (executedBlockInterval->Int.toFloat *. sc.backoffMultiplicative)->Float.toInt
295
- mutSuggestedBlockIntervals->Dict.set(partitionId, nextBlockIntervalTry)
296
- throw(
297
- Source.GetItemsError(
298
- Source.FailedGettingItems({
299
- exn: err,
300
- attemptedToBlock: toBlock,
301
- retry: WithBackoff({
302
- message: `Failed getting data for the block range. Will try smaller block range for the next attempt.`,
303
- backoffMillis: sc.backoffMillis,
304
- }),
305
- }),
306
- ),
307
- )
308
- }
309
- })
310
- }
311
-
312
- type logSelection = {
313
- addresses: option<array<Address.t>>,
314
- topicQuery: Rpc.GetLogs.topicQuery,
315
- }
316
-
317
- type selectionConfig = {
318
- getLogSelectionOrThrow: (~addressesByContractName: dict<array<Address.t>>) => logSelection,
319
- }
320
-
321
- let getSelectionConfig = (selection: FetchState.selection, ~chain) => {
322
- let staticTopicSelections = []
323
- let dynamicEventFilters = []
324
-
325
- selection.eventConfigs
326
- ->(Utils.magic: array<Internal.eventConfig> => array<Internal.evmEventConfig>)
327
- ->Array.forEach(({getEventFiltersOrThrow}) => {
328
- switch getEventFiltersOrThrow(chain) {
329
- | Static(s) => staticTopicSelections->Array.pushMany(s)->ignore
330
- | Dynamic(fn) => dynamicEventFilters->Array.push(fn)->ignore
331
- }
332
- })
333
-
334
- let getLogSelectionOrThrow = switch (
335
- staticTopicSelections->LogSelection.compressTopicSelections,
336
- dynamicEventFilters,
337
- ) {
338
- | ([], []) =>
339
- throw(
340
- Source.GetItemsError(
341
- UnsupportedSelection({
342
- message: "Invalid events configuration for the partition. Nothing to fetch. Please, report to the Envio team.",
343
- }),
344
- ),
345
- )
346
- | ([topicSelection], []) => {
347
- let topicQuery = topicSelection->Rpc.GetLogs.mapTopicQuery
348
- (~addressesByContractName) => {
349
- addresses: switch addressesByContractName->FetchState.addressesByContractNameGetAll {
350
- | [] => None
351
- | addresses => Some(addresses)
352
- },
353
- topicQuery,
354
- }
355
- }
356
- | ([], [dynamicEventFilter]) if selection.eventConfigs->Array.length === 1 =>
357
- let eventConfig = selection.eventConfigs->Utils.Array.firstUnsafe
358
-
359
- (~addressesByContractName) => {
360
- let addresses = addressesByContractName->FetchState.addressesByContractNameGetAll
361
- {
362
- addresses: eventConfig.isWildcard ? None : Some(addresses),
363
- topicQuery: switch dynamicEventFilter(addresses) {
364
- | [topicSelection] => topicSelection->Rpc.GetLogs.mapTopicQuery
365
- | _ =>
366
- throw(
367
- Source.GetItemsError(
368
- UnsupportedSelection({
369
- message: "RPC data-source currently doesn't support an array of event filters. Please, create a GitHub issue if it's a blocker for you.",
370
- }),
371
- ),
372
- )
373
- },
374
- }
375
- }
376
- | _ =>
377
- throw(
378
- Source.GetItemsError(
379
- UnsupportedSelection({
380
- message: "RPC data-source currently supports event filters only when there's a single wildcard event. Please, create a GitHub issue if it's a blocker for you.",
381
- }),
382
- ),
383
- )
384
- }
385
-
386
- {
387
- getLogSelectionOrThrow: getLogSelectionOrThrow,
388
- }
389
- }
390
-
391
- let memoGetSelectionConfig = (~chain) =>
392
- Utils.WeakMap.memoize(selection => selection->getSelectionConfig(~chain))
393
-
394
160
  // Type-erase a schema for storage in the field registry
395
161
  external toFieldSchema: S.t<'a> => S.t<JSON.t> = "%identity"
396
162
 
@@ -531,11 +297,9 @@ let makeThrowingGetEventBlock = (
531
297
  }
532
298
  }
533
299
 
534
- // `number`, `timestamp` and `hash` are always part of the selected block
535
- // fields, so they can be read from the assembled block at item construction.
300
+ // `number` is always part of the selected block fields, so it can be read
301
+ // from the assembled block for the item's own `blockNumber`.
536
302
  @get external getBlockNumber: Internal.eventBlock => int = "number"
537
- @get external getBlockTimestamp: Internal.eventBlock => int = "timestamp"
538
- @get external getBlockHash: Internal.eventBlock => string = "hash"
539
303
 
540
304
  // Field source classification for RPC calls
541
305
  type fieldSource = TransactionOnly | ReceiptOnly | Both
@@ -851,10 +615,11 @@ type options = {
851
615
  syncConfig: Config.sourceSync,
852
616
  url: string,
853
617
  chain: ChainMap.Chain.t,
854
- eventRouter: EventRouter.t<Internal.evmEventConfig>,
855
- allEventParams: array<HyperSyncClient.Decoder.eventParamsInput>,
618
+ // The chain's registrations, indexed by their sequential `index`.
619
+ onEventRegistrations: array<Internal.evmOnEventRegistration>,
856
620
  lowercaseAddresses: bool,
857
621
  ws?: string,
622
+ headers?: dict<string>,
858
623
  }
859
624
 
860
625
  let make = (
@@ -863,10 +628,10 @@ let make = (
863
628
  syncConfig,
864
629
  url,
865
630
  chain,
866
- eventRouter,
867
- allEventParams,
631
+ onEventRegistrations,
868
632
  lowercaseAddresses,
869
633
  ?ws,
634
+ ?headers,
870
635
  }: options,
871
636
  ): t => {
872
637
  let chainId = chain->ChainMap.Chain.toChainId
@@ -879,22 +644,48 @@ let make = (
879
644
  }
880
645
  let name = `RPC (${urlHost})`
881
646
 
882
- let getSelectionConfig = memoGetSelectionConfig(~chain)
883
-
884
- let mutSuggestedBlockIntervals = Dict.make()
647
+ let client = Rpc.makeClient(url, ~headers?)
648
+ let rpcClient = EvmRpcClient.make(
649
+ ~url,
650
+ ~eventRegistrations=HyperSyncClient.Registration.fromOnEventRegistrations(
651
+ onEventRegistrations,
652
+ ),
653
+ ~checksumAddresses=!lowercaseAddresses,
654
+ ~syncConfig,
655
+ ~headers?,
656
+ )
885
657
 
886
- let client = Rpc.makeClient(url)
887
- let rpcClient = EvmRpcClient.make(~url)
658
+ // Requests are made from shared, memoized loaders, so they can't be
659
+ // attributed to a single getItemsOrThrow/getHeightOrThrow/getBlockHashes
660
+ // call at its call site. Every actual request (cache/dedup hits never reach
661
+ // recordRequest) pushes here; each method drains whatever is pending when it
662
+ // returns. Since a push always lands in exactly one drain, per-source totals
663
+ // stay exact even with concurrent in-flight calls — which call happens to
664
+ // drain a given entry doesn't matter, since SourceManager aggregates by
665
+ // (source, method) regardless of which call returned it.
666
+ let pendingRequestStats: array<Source.requestStat> = []
667
+ let recordRequest = (~method, ~seconds) => {
668
+ pendingRequestStats->Array.push({Source.method, seconds})->ignore
669
+ }
670
+ let drainRequestStats = () => {
671
+ let stats = pendingRequestStats->Utils.Array.copy
672
+ pendingRequestStats->Utils.Array.clearInPlace
673
+ stats
674
+ }
888
675
 
889
676
  let makeTransactionLoader = () =>
890
677
  LazyLoader.make(
891
678
  ~loaderFn=transactionHash => {
892
- Prometheus.SourceRequestCount.increment(
893
- ~sourceName=name,
894
- ~chainId=chain->ChainMap.Chain.toChainId,
895
- ~method="eth_getTransactionByHash",
896
- )
897
- Rpc.GetTransactionByHash.rawRoute->Rest.fetch(transactionHash, ~client)
679
+ let timerRef = Performance.now()
680
+ Rpc.GetTransactionByHash.rawRoute
681
+ ->Rest.fetch(transactionHash, ~client)
682
+ ->Promise.thenResolve(res => {
683
+ recordRequest(
684
+ ~method="eth_getTransactionByHash",
685
+ ~seconds=timerRef->Performance.secondsSince,
686
+ )
687
+ res
688
+ })
898
689
  },
899
690
  ~onError=(am, ~exn) => {
900
691
  Logging.error({
@@ -922,6 +713,7 @@ let make = (
922
713
  ~chain,
923
714
  ~backoffMsOnFailure=1000,
924
715
  ~blockNumber,
716
+ ~recordRequest,
925
717
  )
926
718
  },
927
719
  ~onError=(am, ~exn) => {
@@ -944,12 +736,16 @@ let make = (
944
736
  let makeReceiptLoader = () =>
945
737
  LazyLoader.make(
946
738
  ~loaderFn=transactionHash => {
947
- Prometheus.SourceRequestCount.increment(
948
- ~sourceName=name,
949
- ~chainId=chain->ChainMap.Chain.toChainId,
950
- ~method="eth_getTransactionReceipt",
951
- )
952
- Rpc.GetTransactionReceipt.rawRoute->Rest.fetch(transactionHash, ~client)
739
+ let timerRef = Performance.now()
740
+ Rpc.GetTransactionReceipt.rawRoute
741
+ ->Rest.fetch(transactionHash, ~client)
742
+ ->Promise.thenResolve(res => {
743
+ recordRequest(
744
+ ~method="eth_getTransactionReceipt",
745
+ ~seconds=timerRef->Performance.secondsSince,
746
+ )
747
+ res
748
+ })
953
749
  },
954
750
  ~onError=(am, ~exn) => {
955
751
  Logging.error({
@@ -1000,58 +796,19 @@ let make = (
1000
796
  ~lowercaseAddresses,
1001
797
  )
1002
798
 
1003
- let convertLogToHyperSyncEvent = (log: Rpc.GetLogs.log): HyperSyncClient.ResponseTypes.event => {
1004
- let hyperSyncLog: HyperSyncClient.ResponseTypes.log = {
1005
- removed: log.removed,
1006
- index: log.logIndex,
1007
- transactionIndex: log.transactionIndex,
1008
- transactionHash: log.transactionHash,
1009
- blockHash: log.blockHash,
1010
- blockNumber: log.blockNumber,
1011
- address: log.address,
1012
- data: log.data,
1013
- topics: log.topics->(Utils.magic: array<string> => array<Nullable.t<EvmTypes.Hex.t>>),
1014
- }
1015
- {log: hyperSyncLog}
1016
- }
1017
-
1018
- let hscDecoder: ref<option<HyperSyncClient.Decoder.tWithParams>> = ref(None)
1019
- let getHscDecoder = () => {
1020
- switch hscDecoder.contents {
1021
- | Some(decoder) => decoder
1022
- | None => {
1023
- let decoder = HyperSyncClient.Decoder.fromParams(
1024
- allEventParams,
1025
- ~checksumAddresses=!lowercaseAddresses,
1026
- )
1027
- hscDecoder := Some(decoder)
1028
- decoder
1029
- }
1030
- }
1031
- }
1032
-
1033
799
  let getItemsOrThrow = async (
1034
800
  ~fromBlock,
1035
801
  ~toBlock,
1036
802
  ~addressesByContractName,
1037
- ~indexingAddresses,
803
+ ~contractNameByAddress as _,
1038
804
  ~knownHeight,
1039
805
  ~partitionId,
1040
806
  ~selection: FetchState.selection,
807
+ ~itemsTarget as _,
1041
808
  ~retry,
1042
809
  ~logger as _,
1043
810
  ) => {
1044
- let startFetchingBatchTimeRef = Hrtime.makeTimer()
1045
-
1046
- let suggestedBlockInterval = switch mutSuggestedBlockIntervals->Utils.Dict.dangerouslyGetNonOption(
1047
- maxSuggestedBlockIntervalKey,
1048
- ) {
1049
- | Some(maxSuggestedBlockInterval) => maxSuggestedBlockInterval
1050
- | None =>
1051
- mutSuggestedBlockIntervals
1052
- ->Utils.Dict.dangerouslyGetNonOption(partitionId)
1053
- ->Option.getOr(syncConfig.initialBlockInterval)
1054
- }
811
+ let startFetchingBatchTimeRef = Performance.now()
1055
812
 
1056
813
  // Always have a toBlock for an RPC worker
1057
814
  let toBlock = switch toBlock {
@@ -1059,10 +816,6 @@ let make = (
1059
816
  | None => knownHeight
1060
817
  }
1061
818
 
1062
- let suggestedToBlock = Pervasives.min(fromBlock + suggestedBlockInterval - 1, toBlock)
1063
- //Defensively ensure we never query a target block below fromBlock
1064
- ->Pervasives.max(fromBlock)
1065
-
1066
819
  let firstBlockParentPromise =
1067
820
  fromBlock > 0
1068
821
  ? blockLoader.contents
@@ -1070,97 +823,70 @@ let make = (
1070
823
  ->Promise.thenResolve(json => Some(parseBlockInfo(json)))
1071
824
  : Promise.resolve(None)
1072
825
 
1073
- let {getLogSelectionOrThrow} = getSelectionConfig(selection)
1074
- let {addresses, topicQuery} = getLogSelectionOrThrow(~addressesByContractName)
1075
-
1076
- let {logs, latestFetchedBlockInfo} = await getNextPage(
1077
- ~fromBlock,
1078
- ~toBlock=suggestedToBlock,
1079
- ~addresses,
1080
- ~topicQuery,
1081
- ~loadBlock=blockNumber =>
1082
- blockLoader.contents
1083
- ->LazyLoader.get(blockNumber)
1084
- ->Promise.thenResolve(parseBlockInfo),
1085
- ~syncConfig,
1086
- ~client,
1087
- ~mutSuggestedBlockIntervals,
1088
- ~partitionId,
1089
- ~sourceName=name,
1090
- ~chainId=chain->ChainMap.Chain.toChainId,
1091
- )
1092
-
1093
- let executedBlockInterval = suggestedToBlock - fromBlock + 1
1094
-
1095
- // Increase the suggested block interval only when it was actually applied
1096
- // and we didn't query to a hard toBlock
1097
- // We also don't care about it when we have a hard max block interval
1098
- if (
1099
- executedBlockInterval >= suggestedBlockInterval &&
1100
- !(mutSuggestedBlockIntervals->Dict.has(maxSuggestedBlockIntervalKey))
1101
- ) {
1102
- // Increase batch size going forward, but do not increase past a configured maximum
1103
- // See: https://en.wikipedia.org/wiki/Additive_increase/multiplicative_decrease
1104
- mutSuggestedBlockIntervals->Dict.set(
1105
- partitionId,
1106
- Pervasives.min(
1107
- executedBlockInterval + syncConfig.accelerationAdditive,
1108
- syncConfig.intervalCeiling,
1109
- ),
1110
- )
1111
- }
1112
-
1113
- // Convert RPC logs to HyperSync events
1114
- let hyperSyncEvents = logs->Array.map(convertLogToHyperSyncEvent)
1115
-
1116
- // Decode using HyperSyncClient decoder
1117
- let parsedEvents = try await getHscDecoder().decodeLogs(hyperSyncEvents) catch {
1118
- | exn =>
826
+ if selection.onEventRegistrations->Utils.Array.isEmpty {
1119
827
  throw(
1120
828
  Source.GetItemsError(
1121
- FailedGettingItems({
1122
- exn,
1123
- attemptedToBlock: toBlock,
1124
- retry: ImpossibleForTheQuery({
1125
- message: "Failed to parse events using hypersync client decoder. Please double-check your ABI.",
1126
- }),
829
+ UnsupportedSelection({
830
+ message: "Invalid events configuration for the partition. Nothing to fetch. Please, report to the Envio team.",
1127
831
  }),
1128
832
  ),
1129
833
  )
1130
834
  }
1131
835
 
1132
- let parsedQueueItems = await logs
1133
- ->Array.zip(parsedEvents)
1134
- ->Array.filterMap(((
1135
- log: Rpc.GetLogs.log,
1136
- maybeDecodedEvent: Nullable.t<dict<Internal.eventParams>>,
1137
- )) => {
1138
- let topic0 = log.topics[0]->Option.getOr("0x0")
1139
- let routedAddress = if lowercaseAddresses {
1140
- log.address->Address.Evm.fromAddressLowercaseOrThrow
1141
- } else {
1142
- log.address->Address.Evm.fromAddressOrThrow
836
+ let {items, toBlock: queriedToBlock, requestStats} = try await rpcClient.getNextPage({
837
+ fromBlock,
838
+ toBlockCeiling: toBlock,
839
+ partitionId,
840
+ registrationIndexes: selection.onEventRegistrations->Array.map(reg => reg.index),
841
+ addressesByContractName,
842
+ }) catch {
843
+ | exn =>
844
+ switch exn->parseGetNextPageRetryError {
845
+ | Some((attemptedToBlock, retry, requestStats)) =>
846
+ requestStats->Array.forEach(stat =>
847
+ recordRequest(~method=stat.method, ~seconds=stat.seconds)
848
+ )
849
+ throw(Source.GetItemsError(FailedGettingItems({exn, attemptedToBlock, retry})))
850
+ | None =>
851
+ throw(
852
+ Source.GetItemsError(
853
+ FailedGettingItems({
854
+ exn,
855
+ attemptedToBlock: toBlock,
856
+ retry: WithBackoff({
857
+ message: "Unexpected issue while fetching events from the RPC client. Attempt a retry.",
858
+ backoffMillis: switch retry {
859
+ | 0 => 500
860
+ | _ => 1000 * retry
861
+ },
862
+ }),
863
+ }),
864
+ ),
865
+ )
1143
866
  }
1144
-
1145
- switch eventRouter->EventRouter.get(
1146
- ~tag=EventRouter.getEvmEventId(~sighash=topic0, ~topicCount=log.topics->Array.length),
1147
- ~indexingAddresses,
1148
- ~contractAddress=routedAddress,
1149
- ~blockNumber=log.blockNumber,
1150
- ) {
1151
- | None => None
1152
- | Some(eventConfig) =>
1153
- switch maybeDecodedEvent
1154
- ->Nullable.toOption
1155
- ->Option.flatMap(Dict.get(_, eventConfig.contractName)) {
1156
- | Some(decoded) =>
1157
- Some(
1158
- (
1159
- async () => {
867
+ }
868
+ requestStats->Array.forEach(stat => recordRequest(~method=stat.method, ~seconds=stat.seconds))
869
+
870
+ let latestFetchedBlockInfo = await blockLoader.contents
871
+ ->LazyLoader.get(queriedToBlock)
872
+ ->Promise.thenResolve(parseBlockInfo)
873
+
874
+ let parsedQueueItems = await items
875
+ ->Array.map(({log, onEventRegistrationIndex, params: decoded}: EvmRpcClient.rpcEventItem) => {
876
+ // `log.address` comes back already normalized to the client's casing.
877
+ let onEventRegistration = onEventRegistrations->Array.getUnsafe(onEventRegistrationIndex)
878
+ let eventConfig =
879
+ onEventRegistration.eventConfig->(
880
+ Utils.magic: Internal.eventConfig => Internal.evmEventConfig
881
+ )
882
+ (
883
+ async () => {
1160
884
  let (block, transaction) = try await Promise.all2((
1161
885
  log->getEventBlockOrThrow(~selectedBlockFields=eventConfig.selectedBlockFields),
1162
886
  log->getEventTransactionOrThrow(
1163
- ~selectedTransactionFields=eventConfig.selectedTransactionFields,
887
+ ~selectedTransactionFields=eventConfig.selectedTransactionFields->(
888
+ Utils.magic: Utils.Set.t<string> => Utils.Set.t<Internal.evmTransactionField>
889
+ ),
1164
890
  ),
1165
891
  )) catch {
1166
892
  | TransactionDataNotFound({message}) =>
@@ -1194,35 +920,30 @@ let make = (
1194
920
  }
1195
921
 
1196
922
  Internal.Event({
1197
- eventConfig: (eventConfig :> Internal.eventConfig),
1198
- timestamp: block->getBlockTimestamp,
923
+ onEventRegistration: (onEventRegistration :> Internal.onEventRegistration),
1199
924
  blockNumber: block->getBlockNumber,
1200
- blockHash: block->getBlockHash,
1201
925
  chain,
1202
926
  logIndex: log.logIndex,
927
+ transactionIndex: log.transactionIndex,
1203
928
  payload: {
1204
929
  contractName: eventConfig.contractName,
1205
930
  eventName: eventConfig.name,
1206
931
  chainId: chain->ChainMap.Chain.toChainId,
1207
932
  params: decoded,
1208
- transaction,
1209
933
  block,
1210
- srcAddress: routedAddress,
934
+ transaction,
935
+ srcAddress: log.address,
1211
936
  logIndex: log.logIndex,
1212
937
  }->Evm.fromPayload,
1213
938
  })
1214
- }
1215
- )(),
1216
- )
1217
- | None => None
1218
939
  }
1219
- }
940
+ )()
1220
941
  })
1221
942
  ->Promise.all
1222
943
 
1223
944
  let optFirstBlockParent = await firstBlockParentPromise
1224
945
 
1225
- let totalTimeElapsed = startFetchingBatchTimeRef->Hrtime.timeSince->Hrtime.toSecondsFloat
946
+ let totalTimeElapsed = startFetchingBatchTimeRef->Performance.secondsSince
1226
947
 
1227
948
  // Every fetched block carries `hash` and `parentHash`, so each one yields
1228
949
  // two confirmed (number, hash) pairs for reorg detection at no extra cost.
@@ -1240,7 +961,7 @@ let make = (
1240
961
  | Some(b) => pushBlockInfo(b)
1241
962
  | None => ()
1242
963
  }
1243
- logs->Array.forEach(log =>
964
+ items->Array.forEach(({log}) =>
1244
965
  blockHashes
1245
966
  ->Array.push({ReorgDetection.blockNumber: log.blockNumber, blockHash: log.blockHash})
1246
967
  ->ignore
@@ -1250,12 +971,16 @@ let make = (
1250
971
  latestFetchedBlockTimestamp: latestFetchedBlockInfo.timestamp,
1251
972
  latestFetchedBlockNumber: latestFetchedBlockInfo.number,
1252
973
  parsedQueueItems,
974
+ // RPC keeps the transaction and block inline on the payload; no store pages.
975
+ transactionStore: None,
976
+ blockStore: None,
1253
977
  stats: {
1254
978
  totalTimeElapsed: totalTimeElapsed,
1255
979
  },
1256
980
  knownHeight,
1257
981
  blockHashes,
1258
982
  fromBlockQueried: fromBlock,
983
+ requestStats: drainRequestStats(),
1259
984
  }
1260
985
  }
1261
986
 
@@ -1272,21 +997,25 @@ let make = (
1272
997
  ->Array.map(blockNum => blockLoader.contents->LazyLoader.get(blockNum))
1273
998
  ->Promise.all
1274
999
  ->Promise.thenResolve(rawBlocks => {
1275
- rawBlocks
1276
- ->Array.map(json => {
1277
- let b = parseBlockInfo(json)
1000
+ let result =
1001
+ rawBlocks
1002
+ ->Array.map(json => {
1003
+ let b = parseBlockInfo(json)
1278
1004
 
1279
- (
1280
- {
1281
- blockNumber: b.number,
1282
- blockHash: b.hash,
1283
- blockTimestamp: b.timestamp,
1284
- }: ReorgDetection.blockDataWithTimestamp
1285
- )
1286
- })
1287
- ->Ok
1005
+ (
1006
+ {
1007
+ blockNumber: b.number,
1008
+ blockHash: b.hash,
1009
+ blockTimestamp: b.timestamp,
1010
+ }: ReorgDetection.blockDataWithTimestamp
1011
+ )
1012
+ })
1013
+ ->Ok
1014
+ {Source.result, requestStats: drainRequestStats()}
1288
1015
  })
1289
- ->Promise.catch(exn => exn->Error->Promise.resolve)
1016
+ ->Promise.catch(exn =>
1017
+ {Source.result: Error(exn), requestStats: drainRequestStats()}->Promise.resolve
1018
+ )
1290
1019
  }
1291
1020
 
1292
1021
  let createHeightSubscription =
@@ -1303,38 +1032,16 @@ let make = (
1303
1032
  getBlockHashes,
1304
1033
  onReorg,
1305
1034
  getHeightOrThrow: async () => {
1306
- let timerRef = Hrtime.makeTimer()
1035
+ let timerRef = Performance.now()
1307
1036
  let height = try {
1308
1037
  await rpcClient.getHeight()
1309
1038
  } catch {
1310
1039
  | exn =>
1311
- let seconds = timerRef->Hrtime.timeSince->Hrtime.toSecondsFloat
1312
- Prometheus.SourceRequestCount.increment(
1313
- ~sourceName=name,
1314
- ~chainId=chain->ChainMap.Chain.toChainId,
1315
- ~method="eth_blockNumber",
1316
- )
1317
- Prometheus.SourceRequestCount.addSeconds(
1318
- ~sourceName=name,
1319
- ~chainId=chain->ChainMap.Chain.toChainId,
1320
- ~method="eth_blockNumber",
1321
- ~seconds,
1322
- )
1040
+ recordRequest(~method="eth_blockNumber", ~seconds=timerRef->Performance.secondsSince)
1323
1041
  exn->throw
1324
1042
  }
1325
- let seconds = timerRef->Hrtime.timeSince->Hrtime.toSecondsFloat
1326
- Prometheus.SourceRequestCount.increment(
1327
- ~sourceName=name,
1328
- ~chainId=chain->ChainMap.Chain.toChainId,
1329
- ~method="eth_blockNumber",
1330
- )
1331
- Prometheus.SourceRequestCount.addSeconds(
1332
- ~sourceName=name,
1333
- ~chainId=chain->ChainMap.Chain.toChainId,
1334
- ~method="eth_blockNumber",
1335
- ~seconds,
1336
- )
1337
- height
1043
+ recordRequest(~method="eth_blockNumber", ~seconds=timerRef->Performance.secondsSince)
1044
+ {height, requestStats: drainRequestStats()}
1338
1045
  },
1339
1046
  getItemsOrThrow,
1340
1047
  ?createHeightSubscription,