envio 3.3.0-alpha.6 → 3.3.0-alpha.8

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 (93) hide show
  1. package/index.d.ts +23 -5
  2. package/package.json +6 -6
  3. package/src/Address.res +5 -2
  4. package/src/Address.res.mjs +3 -1
  5. package/src/Api.res +1 -1
  6. package/src/Api.res.mjs +1 -1
  7. package/src/ChainFetching.res +34 -34
  8. package/src/ChainFetching.res.mjs +29 -29
  9. package/src/ChainState.res +328 -165
  10. package/src/ChainState.res.mjs +205 -100
  11. package/src/ChainState.resi +16 -7
  12. package/src/Config.res +55 -27
  13. package/src/Config.res.mjs +46 -7
  14. package/src/ContractRegisterContext.res +2 -12
  15. package/src/ContractRegisterContext.res.mjs +3 -5
  16. package/src/Core.res +7 -0
  17. package/src/CrossChainState.res +54 -79
  18. package/src/CrossChainState.res.mjs +45 -60
  19. package/src/CrossChainState.resi +1 -1
  20. package/src/Ecosystem.res +3 -3
  21. package/src/Ecosystem.res.mjs +3 -3
  22. package/src/Envio.res +14 -9
  23. package/src/EventConfigBuilder.res +105 -56
  24. package/src/EventConfigBuilder.res.mjs +69 -33
  25. package/src/EventProcessing.res +19 -15
  26. package/src/EventProcessing.res.mjs +13 -12
  27. package/src/FetchState.res +383 -182
  28. package/src/FetchState.res.mjs +260 -264
  29. package/src/HandlerLoader.res +7 -112
  30. package/src/HandlerLoader.res.mjs +1 -87
  31. package/src/HandlerRegister.res +209 -6
  32. package/src/HandlerRegister.res.mjs +90 -5
  33. package/src/HandlerRegister.resi +13 -2
  34. package/src/IndexerState.res +6 -3
  35. package/src/IndexerState.res.mjs +3 -3
  36. package/src/IndexerState.resi +1 -1
  37. package/src/IndexingAddresses.res +10 -7
  38. package/src/IndexingAddresses.res.mjs +8 -7
  39. package/src/IndexingAddresses.resi +3 -1
  40. package/src/Internal.res +82 -36
  41. package/src/Internal.res.mjs +11 -1
  42. package/src/Main.res +22 -23
  43. package/src/Main.res.mjs +15 -17
  44. package/src/Metrics.res +43 -2
  45. package/src/Metrics.res.mjs +39 -3
  46. package/src/Prometheus.res +0 -35
  47. package/src/Prometheus.res.mjs +33 -68
  48. package/src/RawEvent.res +7 -2
  49. package/src/RawEvent.res.mjs +4 -4
  50. package/src/SimulateItems.res +26 -8
  51. package/src/SimulateItems.res.mjs +11 -14
  52. package/src/TestIndexer.res +1 -1
  53. package/src/TestIndexer.res.mjs +1 -1
  54. package/src/sources/BlockStore.res +46 -0
  55. package/src/sources/BlockStore.res.mjs +24 -0
  56. package/src/sources/EventRouter.res +19 -12
  57. package/src/sources/EventRouter.res.mjs +7 -5
  58. package/src/sources/Evm.res +54 -4
  59. package/src/sources/Evm.res.mjs +43 -4
  60. package/src/sources/EvmChain.res +14 -18
  61. package/src/sources/EvmChain.res.mjs +12 -13
  62. package/src/sources/FieldMask.res +39 -0
  63. package/src/sources/FieldMask.res.mjs +42 -0
  64. package/src/sources/Fuel.res +5 -2
  65. package/src/sources/Fuel.res.mjs +4 -3
  66. package/src/sources/HyperFuelSource.res +32 -40
  67. package/src/sources/HyperFuelSource.res.mjs +53 -45
  68. package/src/sources/HyperSync.res +35 -9
  69. package/src/sources/HyperSync.res.mjs +52 -35
  70. package/src/sources/HyperSync.resi +11 -3
  71. package/src/sources/HyperSyncClient.res +15 -4
  72. package/src/sources/HyperSyncHeightStream.res +1 -8
  73. package/src/sources/HyperSyncHeightStream.res.mjs +0 -2
  74. package/src/sources/HyperSyncSource.res +39 -58
  75. package/src/sources/HyperSyncSource.res.mjs +44 -44
  76. package/src/sources/RpcSource.res +114 -92
  77. package/src/sources/RpcSource.res.mjs +77 -46
  78. package/src/sources/RpcWebSocketHeightStream.res +0 -5
  79. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -2
  80. package/src/sources/SimulateSource.res +6 -3
  81. package/src/sources/SimulateSource.res.mjs +13 -5
  82. package/src/sources/Source.res +25 -5
  83. package/src/sources/SourceManager.res +66 -3
  84. package/src/sources/SourceManager.res.mjs +55 -7
  85. package/src/sources/SourceManager.resi +10 -0
  86. package/src/sources/Svm.res +15 -10
  87. package/src/sources/Svm.res.mjs +24 -7
  88. package/src/sources/SvmHyperSyncClient.res +5 -6
  89. package/src/sources/SvmHyperSyncSource.res +84 -41
  90. package/src/sources/SvmHyperSyncSource.res.mjs +90 -43
  91. package/src/sources/TransactionStore.res +6 -107
  92. package/src/sources/TransactionStore.res.mjs +5 -86
  93. package/svm.schema.json +19 -0
@@ -14,16 +14,24 @@ type t = {
14
14
  mutable timestampCaughtUpToHeadOrEndblock: option<Date.t>,
15
15
  mutable committedProgressBlockNumber: int,
16
16
  mutable numEventsProcessed: float,
17
- // Running sum of in-flight queries' estResponseSize, kept here so the
17
+ // Running sum of in-flight queries' itemsTarget, kept here so the
18
18
  // scheduler doesn't re-sum pending queries on every tick. Incremented when
19
19
  // queries are dispatched, decremented as their responses land.
20
20
  mutable pendingBudget: float,
21
+ // Chain-wide events/block, used to turn a chain's item budget into a target
22
+ // block for query sizing. Seeded from cumulative progress on construction,
23
+ // then smoothed with an EMA on every batch (see applyBatchProgress). None
24
+ // until the chain has processed at least one event.
25
+ mutable chainDensity: option<float>,
21
26
  mutable reorgDetection: ReorgDetection.t,
22
27
  mutable safeCheckpointTracking: option<SafeCheckpointTracking.t>,
23
28
  // Holds this chain's transactions (kept in Rust) keyed by (blockNumber,
24
29
  // transactionIndex). Fetch responses merge their page in; entries are pruned
25
30
  // as the chain progresses and dropped above the target on rollback.
26
31
  transactionStore: TransactionStore.t,
32
+ // Holds this chain's blocks (kept in Rust) keyed by block number. Same merge /
33
+ // prune / rollback lifecycle as the transaction store.
34
+ blockStore: BlockStore.t,
27
35
  }
28
36
 
29
37
  // Per-chain shape returned by the status API.
@@ -70,6 +78,8 @@ let make = (
70
78
  ~timestampCaughtUpToHeadOrEndblock=None,
71
79
  ~isProgressAtHead=false,
72
80
  ~transactionStore=TransactionStore.make(~ecosystem=Ecosystem.Evm, ~shouldChecksum=false),
81
+ ~chainDensity=None,
82
+ ~blockStore=BlockStore.make(~ecosystem=Ecosystem.Evm, ~shouldChecksum=false),
73
83
  ~logger: Pino.t,
74
84
  ): t => {
75
85
  logger,
@@ -82,9 +92,11 @@ let make = (
82
92
  committedProgressBlockNumber,
83
93
  numEventsProcessed,
84
94
  pendingBudget: 0.,
95
+ chainDensity,
85
96
  reorgDetection,
86
97
  safeCheckpointTracking,
87
98
  transactionStore,
99
+ blockStore,
88
100
  }
89
101
 
90
102
  let makeInternal = (
@@ -95,7 +107,7 @@ let makeInternal = (
95
107
  ~firstEventBlock=None,
96
108
  ~progressBlockNumber,
97
109
  ~config: Config.t,
98
- ~registrations: HandlerRegister.registrations,
110
+ ~registrationsByChainId: HandlerRegister.registrationsByChainId,
99
111
  ~logger,
100
112
  ~timestampCaughtUpToHeadOrEndblock,
101
113
  ~numEventsProcessed,
@@ -106,123 +118,44 @@ let makeInternal = (
106
118
  ~knownHeight=0,
107
119
  ~reducedPollingInterval=?,
108
120
  ): t => {
109
- // We don't need the router itself, but only validation logic,
110
- // since now event router is created for selection of events
111
- // and validation doesn't work correctly in routers.
112
- // Ideally to split it into two different parts.
113
- let eventRouter = EventRouter.empty()
114
-
115
- // Aggregate events we want to fetch
116
- let eventConfigs: array<Internal.eventConfig> = []
117
-
118
- let notRegisteredEvents = []
121
+ // Handler binding + `where`-derived fetch state, and onBlock registrations,
122
+ // are already collected by `HandlerRegister.finishRegistration`, keyed by
123
+ // chain - this just looks up this chain's slice.
124
+ let {onEventRegistrations, onBlockRegistrations} =
125
+ registrationsByChainId
126
+ ->Utils.Dict.dangerouslyGetNonOption(chainConfig.id->Int.toString)
127
+ ->Option.getOr({onEventRegistrations: [], onBlockRegistrations: []})
119
128
 
120
129
  chainConfig.contracts->Array.forEach(contract => {
121
- let contractName = contract.name
122
-
123
- contract.events->Array.forEach(eventConfig => {
124
- let {isWildcard} = eventConfig
125
- let hasContractRegister = eventConfig.contractRegister->Option.isSome
126
-
127
- // Should validate the events
128
- eventRouter->EventRouter.addOrThrow(
129
- eventConfig.id,
130
- (),
131
- ~contractName,
132
- ~chain=ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id),
133
- ~eventName=eventConfig.name,
134
- ~isWildcard,
135
- )
136
-
137
- // Filter out non-preRegistration events on preRegistration phase
138
- // so we don't care about it in fetch state and workers anymore
139
- let shouldBeIncluded = if config.enableRawEvents {
140
- true
141
- } else {
142
- let isRegistered = hasContractRegister || eventConfig.handler->Option.isSome
143
- if !isRegistered {
144
- notRegisteredEvents->Array.push(eventConfig)
145
- }
146
- isRegistered
147
- }
148
-
149
- // Check if event has Static([]) filters (from a dynamic where
150
- // callback returning `false` / SkipAll for this chain).
151
- // If so, skip it entirely - it should never be fetched
152
- let shouldSkip = try {
153
- let getEventFiltersOrThrow = (
154
- eventConfig->(Utils.magic: Internal.eventConfig => Internal.evmEventConfig)
155
- ).getEventFiltersOrThrow
156
-
157
- // Check for non-evm chains
158
- if (
159
- getEventFiltersOrThrow->(Utils.magic: (ChainMap.Chain.t => Internal.eventFilters) => bool)
160
- ) {
161
- switch getEventFiltersOrThrow(ChainMap.Chain.makeUnsafe(~chainId=chainConfig.id)) {
162
- | Static([]) => true
163
- | _ => false
164
- }
165
- } else {
166
- false
167
- }
168
- } catch {
169
- // Can throw when filter is invalid
170
- // Don't skip an event in this case. Let it throw in a better place - source code
171
- | _ => false
172
- }
173
-
174
- if shouldBeIncluded && !shouldSkip {
175
- eventConfigs->Array.push(eventConfig)
176
- }
177
- })
178
-
179
130
  switch contract.startBlock {
180
131
  | Some(startBlock) if startBlock < chainConfig.startBlock =>
181
132
  JsError.throwWithMessage(
182
- `The start block for contract "${contractName}" is less than the chain start block. This is not supported yet.`,
133
+ `The start block for contract "${contract.name}" is less than the chain start block. This is not supported yet.`,
183
134
  )
184
135
  | _ => ()
185
136
  }
186
137
  })
187
138
 
188
- if notRegisteredEvents->Utils.Array.notEmpty {
189
- logger->Logging.childInfo(
190
- `The event${if notRegisteredEvents->Array.length > 1 {
191
- "s"
192
- } else {
193
- ""
194
- }} ${notRegisteredEvents
195
- ->Array.map(eventConfig => `${eventConfig.contractName}.${eventConfig.name}`)
196
- ->Array.joinUnsafe(", ")} don't have an event handler and skipped for indexing.`,
197
- )
198
- }
199
-
200
- let onBlockConfigs =
201
- registrations.onBlockByChainId->Utils.Dict.dangerouslyGetNonOption(chainConfig.id->Int.toString)
202
- switch onBlockConfigs {
203
- | Some(onBlockConfigs) =>
204
- // TODO: Move it to the HandlerRegister module
205
- // so the error is thrown with better stack trace
206
- onBlockConfigs->Array.forEach(onBlockConfig => {
207
- if onBlockConfig.startBlock->Option.getOr(startBlock) < startBlock {
139
+ // TODO: Move it to the HandlerRegister module
140
+ // so the error is thrown with better stack trace
141
+ onBlockRegistrations->Array.forEach(onBlockRegistration => {
142
+ if onBlockRegistration.startBlock->Option.getOr(startBlock) < startBlock {
143
+ JsError.throwWithMessage(
144
+ `The start block for onBlock handler "${onBlockRegistration.name}" is less than the chain start block (${startBlock->Int.toString}). This is not supported yet.`,
145
+ )
146
+ }
147
+ switch endBlock {
148
+ | Some(chainEndBlock) =>
149
+ if onBlockRegistration.endBlock->Option.getOr(chainEndBlock) > chainEndBlock {
208
150
  JsError.throwWithMessage(
209
- `The start block for onBlock handler "${onBlockConfig.name}" is less than the chain start block (${startBlock->Int.toString}). This is not supported yet.`,
151
+ `The end block for onBlock handler "${onBlockRegistration.name}" is greater than the chain end block (${chainEndBlock->Int.toString}). This is not supported yet.`,
210
152
  )
211
153
  }
212
- switch endBlock {
213
- | Some(chainEndBlock) =>
214
- if onBlockConfig.endBlock->Option.getOr(chainEndBlock) > chainEndBlock {
215
- JsError.throwWithMessage(
216
- `The end block for onBlock handler "${onBlockConfig.name}" is greater than the chain end block (${chainEndBlock->Int.toString}). This is not supported yet.`,
217
- )
218
- }
219
- | None => ()
220
- }
221
- })
222
- | None => ()
223
- }
154
+ | None => ()
155
+ }
156
+ })
224
157
 
225
- let contractConfigs = IndexingAddresses.makeContractConfigs(~eventConfigs)
158
+ let contractConfigs = IndexingAddresses.makeContractConfigs(~onEventRegistrations)
226
159
  let indexingAddressIndex = IndexingAddresses.make(~contractConfigs, ~addresses=indexingAddresses)
227
160
 
228
161
  let fetchState = FetchState.make(
@@ -232,7 +165,7 @@ let makeInternal = (
232
165
  ~progressBlockNumber,
233
166
  ~startBlock,
234
167
  ~endBlock,
235
- ~eventConfigs,
168
+ ~onEventRegistrations,
236
169
  ~maxOnBlockBufferSize=2 * config.batchSize,
237
170
  ~knownHeight,
238
171
  ~chainId=chainConfig.id,
@@ -241,7 +174,7 @@ let makeInternal = (
241
174
  !config.shouldRollbackOnReorg || isInReorgThreshold ? 0 : chainConfig.maxReorgDepth,
242
175
  chainConfig.blockLag,
243
176
  ),
244
- ~onBlockConfigs?,
177
+ ~onBlockRegistrations,
245
178
  ~firstEventBlock,
246
179
  )
247
180
 
@@ -258,16 +191,6 @@ let makeInternal = (
258
191
  let lowercaseAddresses = config.lowercaseAddresses
259
192
  let sources = switch chainConfig.sourceConfig {
260
193
  | Config.EvmSourceConfig({hypersync, rpcs}) =>
261
- // Build Internal.evmContractConfig from contracts for EvmChain.makeSources
262
- let evmContracts: array<Internal.evmContractConfig> = chainConfig.contracts->Array.map((
263
- contract
264
- ): Internal.evmContractConfig => {
265
- name: contract.name,
266
- abi: contract.abi,
267
- events: contract.events->(
268
- Utils.magic: array<Internal.eventConfig> => array<Internal.evmEventConfig>
269
- ),
270
- })
271
194
  let evmRpcs: array<EvmChain.rpc> = rpcs->Array.map((rpc): EvmChain.rpc => {
272
195
  let syncConfig = rpc.syncConfig
273
196
  let ws = rpc.ws
@@ -282,7 +205,9 @@ let makeInternal = (
282
205
  })
283
206
  EvmChain.makeSources(
284
207
  ~chain,
285
- ~contracts=evmContracts,
208
+ ~onEventRegistrations=onEventRegistrations->(
209
+ Utils.magic: array<Internal.onEventRegistration> => array<Internal.evmOnEventRegistration>
210
+ ),
286
211
  ~hyperSync=hypersync,
287
212
  ~rpcs=evmRpcs,
288
213
  ~lowercaseAddresses,
@@ -300,17 +225,13 @@ let makeInternal = (
300
225
  | (Some(hypersyncUrl), _) =>
301
226
  // HyperSync drives instruction sync. A configured RPC is ignored for now
302
227
  // (RPC fallback isn't wired up yet).
303
- let svmEventConfigs =
304
- chainConfig.contracts
305
- ->Array.flatMap(contract => contract.events)
306
- ->(Utils.magic: array<Internal.eventConfig> => array<Internal.svmInstructionEventConfig>)
307
228
  let apiToken = Env.envioApiToken
308
229
  [
309
230
  SvmHyperSyncSource.make({
310
231
  chain,
311
232
  endpointUrl: hypersyncUrl,
312
233
  apiToken,
313
- eventConfigs: svmEventConfigs,
234
+ onEventRegistrations,
314
235
  clientTimeoutMillis: Env.hyperSyncClientTimeoutMillis,
315
236
  }),
316
237
  ]
@@ -319,6 +240,14 @@ let makeInternal = (
319
240
  | Config.CustomSources(sources) => sources
320
241
  }
321
242
 
243
+ // Seed chain density from whatever progress this chain already has (from a
244
+ // resumed DB state, or 0 on a fresh chain) — refined per-batch afterwards.
245
+ let chainDensity = switch fetchState.firstEventBlock {
246
+ | Some(firstEventBlock) if progressBlockNumber > firstEventBlock && numEventsProcessed > 0. =>
247
+ Some(numEventsProcessed /. (progressBlockNumber - firstEventBlock)->Int.toFloat)
248
+ | _ => None
249
+ }
250
+
322
251
  make(
323
252
  ~chainConfig,
324
253
  ~fetchState,
@@ -341,17 +270,27 @@ let makeInternal = (
341
270
  ~ecosystem=config.ecosystem.name,
342
271
  ~shouldChecksum=!lowercaseAddresses,
343
272
  ),
273
+ ~chainDensity,
274
+ ~blockStore=BlockStore.make(
275
+ ~ecosystem=config.ecosystem.name,
276
+ ~shouldChecksum=!lowercaseAddresses,
277
+ ),
344
278
  ~logger,
345
279
  )
346
280
  }
347
281
 
348
- let makeFromConfig = (chainConfig: Config.chain, ~config, ~registrations, ~knownHeight) => {
282
+ let makeFromConfig = (
283
+ chainConfig: Config.chain,
284
+ ~config,
285
+ ~registrationsByChainId,
286
+ ~knownHeight,
287
+ ) => {
349
288
  let logger = Logging.createChild(~params={"chainId": chainConfig.id})
350
289
 
351
290
  makeInternal(
352
291
  ~chainConfig,
353
292
  ~config,
354
- ~registrations,
293
+ ~registrationsByChainId,
355
294
  ~startBlock=chainConfig.startBlock,
356
295
  ~endBlock=chainConfig.endBlock,
357
296
  ~reorgCheckpoints=[],
@@ -377,7 +316,7 @@ let makeFromDbState = (
377
316
  ~isInReorgThreshold,
378
317
  ~isRealtime,
379
318
  ~config,
380
- ~registrations,
319
+ ~registrationsByChainId,
381
320
  ~reducedPollingInterval=?,
382
321
  ) => {
383
322
  let chainId = chainConfig.id
@@ -397,7 +336,7 @@ let makeFromDbState = (
397
336
  ~startBlock=resumedChainState.startBlock,
398
337
  ~endBlock=resumedChainState.endBlock,
399
338
  ~config,
400
- ~registrations,
339
+ ~registrationsByChainId,
401
340
  ~reorgCheckpoints,
402
341
  ~maxReorgDepth=resumedChainState.maxReorgDepth,
403
342
  ~firstEventBlock=resumedChainState.firstEventBlockNumber,
@@ -435,8 +374,7 @@ let contractAddresses = (cs: t, ~contractName) =>
435
374
  let bufferSize = (cs: t) => cs.fetchState->FetchState.bufferSize
436
375
  let bufferReadyCount = (cs: t) => cs.fetchState->FetchState.bufferReadyCount
437
376
  let getProgressPercentage = (cs: t) => cs.fetchState->FetchState.getProgressPercentage
438
- let getProgressPercentageAt = (cs: t, ~blockNumber) =>
439
- cs.fetchState->FetchState.getProgressPercentageAt(~blockNumber)
377
+ let chainDensity = (cs: t) => cs.chainDensity
440
378
  let hasReadyItem = (cs: t) =>
441
379
  cs.fetchState->FetchState.isActivelyIndexing && cs.fetchState->FetchState.hasReadyItem
442
380
  let isReadyToEnterReorgThreshold = (cs: t) => cs.fetchState->FetchState.isReadyToEnterReorgThreshold
@@ -447,7 +385,7 @@ let isReadyToEnterReorgThreshold = (cs: t) => cs.fetchState->FetchState.isReadyT
447
385
  let startFetchingQueries = (cs: t, ~queries: array<FetchState.query>) => {
448
386
  cs.fetchState->FetchState.startFetchingQueries(~queries)
449
387
  cs.pendingBudget =
450
- cs.pendingBudget +. queries->Array.reduce(0., (acc, query) => acc +. query.estResponseSize)
388
+ cs.pendingBudget +. queries->Array.reduce(0., (acc, query) => acc +. query.itemsTarget)
451
389
  }
452
390
 
453
391
  // Drop every in-flight query and release their reservations together, keeping
@@ -457,10 +395,25 @@ let resetPendingQueries = (cs: t) => {
457
395
  cs.pendingBudget = 0.
458
396
  }
459
397
 
460
- // Propose the chain's candidate queries against the shared buffer budget,
461
- // accounting for this chain's own in-flight reservations.
462
- let getNextQuery = (cs: t, ~budget) =>
463
- cs.fetchState->FetchState.getNextQuery(~budget, ~chainPendingBudget=cs.pendingBudget)
398
+ // Turn this chain's share of the indexer-wide buffer budget into a soft
399
+ // target block (via chain density, or knownHeight when density isn't known
400
+ // yet), then propose queries sized against it. Called by CrossChainState's
401
+ // waterfall, furthest-behind chain first, with chainTargetItems set to
402
+ // whatever budget remains at that point in the waterfall.
403
+ let getNextQuery = (cs: t, ~chainTargetItems: float) => {
404
+ let fetchState = cs.fetchState
405
+ let knownHeight = fetchState.knownHeight
406
+ let bufferBlockNumber = fetchState->FetchState.bufferBlockNumber
407
+ let chainTargetBlock = switch cs.chainDensity {
408
+ | Some(density) if density > 0. =>
409
+ Pervasives.min(
410
+ knownHeight,
411
+ bufferBlockNumber + Math.ceil(chainTargetItems /. density)->Float.toInt,
412
+ )
413
+ | _ => knownHeight
414
+ }
415
+ fetchState->FetchState.getNextQuery(~chainTargetBlock, ~chainTargetItems)
416
+ }
464
417
 
465
418
  // Run a fetch tick for this chain against its sources, feeding the owned fetch
466
419
  // frontier to the source manager.
@@ -511,21 +464,189 @@ let isAtHeadWithoutEndBlock = (cs: t) =>
511
464
  // --- State transitions. The chain state is mutated only through these; each
512
465
  // owns a cohesive update so callers don't juggle individual fields. ---
513
466
 
514
- // Apply a fetch response: register any new dynamic contracts, append the items
515
- // to the buffer and advance the known head.
516
- // Materialise the chain store's selected transaction fields onto a batch's
517
- // items at batch prep (the persistent-store path).
518
- let materializeBatchItems = (cs: t, ~items: array<Internal.item>) =>
519
- cs.transactionStore->TransactionStore.materializeItems(~items)
520
-
521
- // Materialise a fetch-response page's transactions onto its items before
522
- // contract-register handlers read them. `None` pages (RPC/Fuel/Simulate keep the
523
- // transaction inline) are a no-op.
524
- let materializePageItems = (~items: array<Internal.item>, ~page: option<TransactionStore.t>) =>
525
- switch page {
526
- | Some(store) => store->TransactionStore.materializeItems(~items)
527
- | None => Promise.resolve()
467
+ // Per-store grouping data a single pass over `items` produces (see
468
+ // `groupBatchItems`), fed into `TransactionStore`/`BlockStore`'s `materialize`.
469
+ type transactionGroups = {
470
+ txBlockNumbers: array<int>,
471
+ transactionIndices: array<int>,
472
+ transactionMasks: array<float>,
473
+ payloadGroups: array<array<Internal.eventPayload>>,
474
+ anyTransactionFieldSelected: bool,
475
+ }
476
+ type blockGroups = {
477
+ blockBlockNumbers: array<int>,
478
+ blockMasks: array<float>,
479
+ blockItemGroups: array<array<Internal.eventItem>>,
480
+ }
481
+
482
+ // Walk `items` once to build both the transaction-store and block-store
483
+ // grouping data, instead of `TransactionStore`/`BlockStore` each re-walking the
484
+ // same batch independently. Items already arrive in (blockNumber, logIndex)
485
+ // order, so a (blockNumber, transactionIndex) run and a blockNumber-only run
486
+ // each stay adjacent; every item is checked against both, in the same pass.
487
+ // `includeBlocks` skips the block side entirely for Fuel, which carries the
488
+ // block inline and has no store.
489
+ let groupBatchItems = (items: array<Internal.item>, ~includeBlocks: bool): (
490
+ transactionGroups,
491
+ blockGroups,
492
+ ) => {
493
+ let txBlockNumbers = []
494
+ let transactionIndices = []
495
+ let transactionMasks = []
496
+ let payloadGroups = []
497
+ let anyTransactionFieldSelected = ref(false)
498
+
499
+ let blockBlockNumbers = []
500
+ let blockMasks = []
501
+ let blockItemGroups = []
502
+
503
+ items->Array.forEach(item =>
504
+ switch item {
505
+ | Internal.Event(_) =>
506
+ let eventItem = item->Internal.castUnsafeEventItem
507
+ let {blockNumber} = eventItem
508
+
509
+ switch eventItem.payload->Internal.getPayloadTransaction->Nullable.toOption {
510
+ | Some(_) => () // RPC/simulate/Fuel carry the transaction inline.
511
+ | None =>
512
+ let {transactionIndex} = eventItem
513
+ let mask = eventItem.onEventRegistration.eventConfig.transactionFieldMask
514
+ if mask != 0. {
515
+ anyTransactionFieldSelected := true
516
+ }
517
+ let last = payloadGroups->Array.length - 1
518
+ if (
519
+ last >= 0 &&
520
+ txBlockNumbers->Array.getUnsafe(last) == blockNumber &&
521
+ transactionIndices->Array.getUnsafe(last) == transactionIndex
522
+ ) {
523
+ payloadGroups->Array.getUnsafe(last)->Array.push(eventItem.payload)
524
+ transactionMasks->Array.setUnsafe(
525
+ last,
526
+ FieldMask.orMask(transactionMasks->Array.getUnsafe(last), mask),
527
+ )
528
+ } else {
529
+ txBlockNumbers->Array.push(blockNumber)
530
+ transactionIndices->Array.push(transactionIndex)
531
+ transactionMasks->Array.push(mask)
532
+ payloadGroups->Array.push([eventItem.payload])
533
+ }
534
+ }
535
+
536
+ if includeBlocks {
537
+ switch eventItem.payload->Internal.getPayloadBlock->Nullable.toOption {
538
+ | Some(_) => () // RPC/simulate/Fuel carry the block inline.
539
+ | None =>
540
+ let mask = eventItem.onEventRegistration.eventConfig.blockFieldMask
541
+ let last = blockItemGroups->Array.length - 1
542
+ if last >= 0 && blockBlockNumbers->Array.getUnsafe(last) == blockNumber {
543
+ blockItemGroups->Array.getUnsafe(last)->Array.push(eventItem)
544
+ blockMasks->Array.setUnsafe(
545
+ last,
546
+ FieldMask.orMask(blockMasks->Array.getUnsafe(last), mask),
547
+ )
548
+ } else {
549
+ blockBlockNumbers->Array.push(blockNumber)
550
+ blockMasks->Array.push(mask)
551
+ blockItemGroups->Array.push([eventItem])
552
+ }
553
+ }
554
+ }
555
+ | Internal.Block(_) => ()
556
+ }
557
+ )
558
+
559
+ (
560
+ {
561
+ txBlockNumbers,
562
+ transactionIndices,
563
+ transactionMasks,
564
+ payloadGroups,
565
+ anyTransactionFieldSelected: anyTransactionFieldSelected.contents,
566
+ },
567
+ {blockBlockNumbers, blockMasks, blockItemGroups},
568
+ )
569
+ }
570
+
571
+ // Materialise a `TransactionStore` against precomputed groups (see
572
+ // `groupBatchItems`). Store-backed items always get a transaction object — the
573
+ // selected fields, or `{}` when nothing was selected — so `event.transaction`
574
+ // is never `undefined` (matching the inline sources).
575
+ let applyTransactionGroups = async (store: TransactionStore.t, g: transactionGroups) => {
576
+ if g.payloadGroups->Utils.Array.notEmpty {
577
+ if g.anyTransactionFieldSelected {
578
+ let txs = await store->TransactionStore.materialize(
579
+ ~blockNumbers=g.txBlockNumbers,
580
+ ~transactionIndices=g.transactionIndices,
581
+ ~masks=g.transactionMasks,
582
+ )
583
+ g.payloadGroups->Array.forEachWithIndex((payloads, i) => {
584
+ let tx = txs->Array.getUnsafe(i)
585
+ payloads->Array.forEach(payload => payload->Internal.setPayloadTransaction(tx))
586
+ })
587
+ } else {
588
+ g.payloadGroups->Array.forEach(payloads =>
589
+ payloads->Array.forEach(payload => payload->Internal.setPayloadTransaction(%raw(`{}`)))
590
+ )
591
+ }
528
592
  }
593
+ }
594
+
595
+ // Materialise a `BlockStore` against precomputed groups (see `groupBatchItems`).
596
+ let applyBlockGroups = async (store: BlockStore.t, g: blockGroups) => {
597
+ if g.blockItemGroups->Utils.Array.notEmpty {
598
+ let blocks = await store->BlockStore.materialize(
599
+ ~blockNumbers=g.blockBlockNumbers,
600
+ ~masks=g.blockMasks,
601
+ )
602
+ g.blockItemGroups->Array.forEachWithIndex((group, i) => {
603
+ let block = blocks->Array.getUnsafe(i)
604
+ group->Array.forEach(ei => ei.payload->Internal.setPayloadBlock(block))
605
+ })
606
+ }
607
+ }
608
+
609
+ let includeBlocksForEcosystem = (ecosystem: Ecosystem.name) =>
610
+ switch ecosystem {
611
+ | Evm | Svm => true
612
+ | Fuel => false
613
+ }
614
+
615
+ // Materialise the chain stores' selected transaction and block fields onto a
616
+ // batch's items at batch prep (the persistent-store path). A single pass over
617
+ // `items` (`groupBatchItems`) builds both stores' selection masks before the
618
+ // two independent materialize calls run concurrently.
619
+ let materializeBatchItems = async (cs: t, ~items: array<Internal.item>, ~ecosystem) => {
620
+ let (txGroups, blockGroups) =
621
+ items->groupBatchItems(~includeBlocks=ecosystem->includeBlocksForEcosystem)
622
+ let _ = await Promise.all2((
623
+ cs.transactionStore->applyTransactionGroups(txGroups),
624
+ cs.blockStore->applyBlockGroups(blockGroups),
625
+ ))
626
+ }
627
+
628
+ // Materialise a fetch-response page's transactions and blocks onto its items
629
+ // before contract-register handlers read them. `None` pages (RPC/Fuel/Simulate
630
+ // keep them inline) are a no-op.
631
+ let materializePageItems = async (
632
+ ~items: array<Internal.item>,
633
+ ~transactionStore: option<TransactionStore.t>,
634
+ ~blockStore: option<BlockStore.t>,
635
+ ~ecosystem,
636
+ ) => {
637
+ let (txGroups, blockGroups) =
638
+ items->groupBatchItems(~includeBlocks=ecosystem->includeBlocksForEcosystem)
639
+ let _ = await Promise.all2((
640
+ switch transactionStore {
641
+ | Some(store) => store->applyTransactionGroups(txGroups)
642
+ | None => Promise.resolve()
643
+ },
644
+ switch blockStore {
645
+ | Some(store) => store->applyBlockGroups(blockGroups)
646
+ | None => Promise.resolve()
647
+ },
648
+ ))
649
+ }
529
650
 
530
651
  let handleQueryResult = (
531
652
  cs: t,
@@ -534,14 +655,19 @@ let handleQueryResult = (
534
655
  ~newItemsWithDcs,
535
656
  ~latestFetchedBlock,
536
657
  ~knownHeight,
537
- ~transactionStore as page: option<TransactionStore.t>,
658
+ ~transactionStore as txPage: option<TransactionStore.t>,
659
+ ~blockStore as blockPage: option<BlockStore.t>,
538
660
  ) => {
539
- // Merge this response's page into the chain store in lockstep with appending
540
- // its items to the buffer. Inline-transaction sources contribute no page.
541
- switch page {
661
+ // Merge this response's pages into the chain stores in lockstep with appending
662
+ // its items to the buffer. Inline sources contribute no page.
663
+ switch txPage {
542
664
  | Some(page) => cs.transactionStore->TransactionStore.merge(page)
543
665
  | None => ()
544
666
  }
667
+ switch blockPage {
668
+ | Some(page) => cs.blockStore->BlockStore.merge(page)
669
+ | None => ()
670
+ }
545
671
 
546
672
  let fs = switch newItemsWithDcs {
547
673
  | [] => cs.fetchState
@@ -563,7 +689,7 @@ let handleQueryResult = (
563
689
  ->FetchState.updateKnownHeight(~knownHeight)
564
690
 
565
691
  // The query is no longer in flight, so release its reservation.
566
- cs.pendingBudget = Pervasives.max(0., cs.pendingBudget -. query.estResponseSize)
692
+ cs.pendingBudget = Pervasives.max(0., cs.pendingBudget -. query.itemsTarget)
567
693
  }
568
694
 
569
695
  // Run reorg detection against a fetch response and commit the updated guard.
@@ -670,8 +796,9 @@ let advanceAfterBatch = (cs: t, ~batch: Batch.t, ~enteringReorgThreshold) =>
670
796
  // Commit a processed batch's progress for this chain (progress block, events
671
797
  // processed, head/safe-checkpoint tracking, first event block). Emits the
672
798
  // per-chain progress metrics. Readiness is decided by CrossChainState once every
673
- // chain is caught up (see markReady).
674
- let applyBatchProgress = (cs: t, ~batch: Batch.t) => {
799
+ // chain is caught up (see markReady). `blockTimestampName` is the ecosystem's
800
+ // block-timestamp field, read off the payload block for the latency metric.
801
+ let applyBatchProgress = (cs: t, ~batch: Batch.t, ~blockTimestampName: string) => {
675
802
  let chainId = cs.chainConfig.id
676
803
 
677
804
  switch batch.progressedChainsById->Utils.Dict.dangerouslyGetByIntNonOption(chainId) {
@@ -689,15 +816,27 @@ let applyBatchProgress = (cs: t, ~batch: Batch.t) => {
689
816
  )
690
817
  }
691
818
 
692
- // Calculate and set latency metrics
693
- switch batch->Batch.findLastEventItem(~chainId) {
694
- | Some(eventItem) => {
695
- let blockTimestampMs = eventItem.timestamp * 1000
696
- Prometheus.ProgressLatency.set(
697
- ~latencyMs=Date.now()->Float.toInt - blockTimestampMs,
698
- ~chainId,
699
- )
700
- }
819
+ // Calculate and set latency metrics. The payload block is materialised or
820
+ // inline by processing time; its timestamp may still be absent (e.g. an
821
+ // SVM slot with no block row) the metric is skipped then.
822
+ switch batch
823
+ ->Batch.findLastEventItem(~chainId)
824
+ ->Option.flatMap(eventItem =>
825
+ eventItem.payload
826
+ ->Internal.getPayloadBlock
827
+ ->Nullable.toOption
828
+ )
829
+ ->Option.flatMap(block =>
830
+ block
831
+ ->(Utils.magic: Internal.eventBlock => dict<unknown>)
832
+ ->Utils.Dict.dangerouslyGetNonOption(blockTimestampName)
833
+ ) {
834
+ | Some(blockTimestamp) =>
835
+ let blockTimestampMs = blockTimestamp->(Utils.magic: unknown => int) * 1000
836
+ Prometheus.ProgressLatency.set(
837
+ ~latencyMs=Date.now()->Float.toInt - blockTimestampMs,
838
+ ~chainId,
839
+ )
701
840
  | None => ()
702
841
  }
703
842
 
@@ -712,10 +851,32 @@ let applyBatchProgress = (cs: t, ~batch: Batch.t) => {
712
851
  }
713
852
  }
714
853
 
854
+ // Chain-wide density update: seed with the batch's own events/block on
855
+ // the first update, then smooth incrementally so a run of a few
856
+ // sparse/dense blocks doesn't swing the target block estimate.
857
+ let deltaBlocks = chainAfterBatch.progressBlockNumber - cs.committedProgressBlockNumber
858
+ if deltaBlocks > 0 {
859
+ let deltaEvents = chainAfterBatch.totalEventsProcessed -. cs.numEventsProcessed
860
+ // Don't seed a density before the first event is seen — a progress-only
861
+ // batch would otherwise set it to 0, matching the resume-seed guard.
862
+ switch (cs.chainDensity, deltaEvents > 0.) {
863
+ | (None, false) => ()
864
+ | _ =>
865
+ let batchDensity = deltaEvents /. deltaBlocks->Int.toFloat
866
+ cs.chainDensity = Some(
867
+ switch cs.chainDensity {
868
+ | None => batchDensity
869
+ | Some(oldDensity) => (oldDensity +. batchDensity) /. 2.
870
+ },
871
+ )
872
+ }
873
+ }
874
+
715
875
  cs.committedProgressBlockNumber = chainAfterBatch.progressBlockNumber
716
876
  cs.numEventsProcessed = chainAfterBatch.totalEventsProcessed
717
- // Processed blocks' transactions are no longer needed.
877
+ // Processed blocks' transactions and blocks are no longer needed.
718
878
  cs.transactionStore->TransactionStore.prune(chainAfterBatch.progressBlockNumber)
879
+ cs.blockStore->BlockStore.prune(chainAfterBatch.progressBlockNumber)
719
880
  cs.isProgressAtHead = cs.isProgressAtHead || chainAfterBatch.isProgressAtHeadWhenBatchCreated
720
881
  switch cs.safeCheckpointTracking {
721
882
  | Some(safeCheckpointTracking) =>
@@ -794,6 +955,7 @@ let rollback = (
794
955
  ~targetBlockNumber=newProgressBlockNumber,
795
956
  )
796
957
  cs.transactionStore->TransactionStore.rollback(newProgressBlockNumber)
958
+ cs.blockStore->BlockStore.rollback(newProgressBlockNumber)
797
959
  cs.committedProgressBlockNumber = newProgressBlockNumber
798
960
  cs.numEventsProcessed = newTotalEventsProcessed
799
961
  | None =>
@@ -808,6 +970,7 @@ let rollback = (
808
970
  ~targetBlockNumber=rollbackTargetBlockNumber,
809
971
  )
810
972
  cs.transactionStore->TransactionStore.rollback(rollbackTargetBlockNumber)
973
+ cs.blockStore->BlockStore.rollback(rollbackTargetBlockNumber)
811
974
  cs.committedProgressBlockNumber = Pervasives.min(
812
975
  cs.committedProgressBlockNumber,
813
976
  rollbackTargetBlockNumber,