envio 3.2.1 → 3.3.0-alpha.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.
Files changed (106) hide show
  1. package/package.json +6 -7
  2. package/src/Batch.res +12 -15
  3. package/src/Batch.res.mjs +4 -5
  4. package/src/BatchProcessing.res +199 -0
  5. package/src/BatchProcessing.res.mjs +125 -0
  6. package/src/ChainFetching.res +373 -0
  7. package/src/ChainFetching.res.mjs +206 -0
  8. package/src/ChainMetadata.res +27 -0
  9. package/src/ChainMetadata.res.mjs +27 -0
  10. package/src/ChainState.res +643 -0
  11. package/src/ChainState.res.mjs +476 -0
  12. package/src/ChainState.resi +87 -0
  13. package/src/Config.res +13 -4
  14. package/src/Config.res.mjs +8 -3
  15. package/src/ContractRegisterContext.res +106 -0
  16. package/src/ContractRegisterContext.res.mjs +76 -0
  17. package/src/Core.res +3 -0
  18. package/src/CrossChainState.res +294 -0
  19. package/src/CrossChainState.res.mjs +221 -0
  20. package/src/CrossChainState.resi +39 -0
  21. package/src/Ecosystem.res +58 -0
  22. package/src/Ecosystem.res.mjs +43 -1
  23. package/src/Env.res +0 -2
  24. package/src/Env.res.mjs +0 -3
  25. package/src/EventConfigBuilder.res +2 -2
  26. package/src/EventProcessing.res +60 -46
  27. package/src/EventProcessing.res.mjs +33 -32
  28. package/src/EventUtils.res +0 -4
  29. package/src/EventUtils.res.mjs +0 -4
  30. package/src/ExitOnCaughtUp.res +10 -0
  31. package/src/ExitOnCaughtUp.res.mjs +22 -0
  32. package/src/FetchState.res +205 -73
  33. package/src/FetchState.res.mjs +242 -103
  34. package/src/InMemoryStore.res +36 -451
  35. package/src/InMemoryStore.res.mjs +20 -369
  36. package/src/IndexerLoop.res +43 -0
  37. package/src/IndexerLoop.res.mjs +46 -0
  38. package/src/IndexerState.res +536 -0
  39. package/src/IndexerState.res.mjs +543 -0
  40. package/src/IndexerState.resi +128 -0
  41. package/src/Internal.res +29 -8
  42. package/src/LoadLayer.res +20 -18
  43. package/src/LoadLayer.res.mjs +14 -12
  44. package/src/LoadLayer.resi +6 -3
  45. package/src/Logging.res +11 -44
  46. package/src/Logging.res.mjs +10 -42
  47. package/src/Main.res +54 -79
  48. package/src/Main.res.mjs +44 -56
  49. package/src/PgStorage.res +8 -71
  50. package/src/PgStorage.res.mjs +3 -47
  51. package/src/Prometheus.res +1 -13
  52. package/src/Prometheus.res.mjs +84 -96
  53. package/src/PruneStaleHistory.res +45 -0
  54. package/src/PruneStaleHistory.res.mjs +46 -0
  55. package/src/RawEvent.res +73 -0
  56. package/src/RawEvent.res.mjs +51 -0
  57. package/src/Rollback.res +204 -0
  58. package/src/Rollback.res.mjs +118 -0
  59. package/src/SimulateItems.res +12 -10
  60. package/src/SimulateItems.res.mjs +1 -1
  61. package/src/UserContext.res +26 -114
  62. package/src/UserContext.res.mjs +15 -78
  63. package/src/Writing.res +242 -0
  64. package/src/Writing.res.mjs +215 -0
  65. package/src/db/InternalTable.res +14 -27
  66. package/src/sources/Evm.res +40 -1
  67. package/src/sources/Evm.res.mjs +94 -84
  68. package/src/sources/Fuel.res +40 -1
  69. package/src/sources/Fuel.res.mjs +36 -26
  70. package/src/sources/HyperFuel.res +41 -120
  71. package/src/sources/HyperFuel.res.mjs +42 -80
  72. package/src/sources/HyperFuel.resi +1 -24
  73. package/src/sources/HyperFuelClient.res +16 -297
  74. package/src/sources/HyperFuelClient.res.mjs +5 -4
  75. package/src/sources/HyperFuelSource.res +33 -8
  76. package/src/sources/HyperFuelSource.res.mjs +56 -10
  77. package/src/sources/HyperSyncSource.res +5 -3
  78. package/src/sources/HyperSyncSource.res.mjs +1 -1
  79. package/src/sources/RpcSource.res +2 -2
  80. package/src/sources/RpcSource.res.mjs +1 -1
  81. package/src/sources/SourceManager.res +14 -23
  82. package/src/sources/SourceManager.res.mjs +27 -29
  83. package/src/sources/SourceManager.resi +4 -2
  84. package/src/sources/Svm.res +18 -1
  85. package/src/sources/Svm.res.mjs +30 -22
  86. package/src/sources/SvmHyperSyncSource.res +1 -1
  87. package/src/sources/SvmHyperSyncSource.res.mjs +1 -1
  88. package/src/tui/Tui.res +38 -36
  89. package/src/tui/Tui.res.mjs +51 -51
  90. package/src/ChainFetcher.res +0 -519
  91. package/src/ChainFetcher.res.mjs +0 -314
  92. package/src/ChainManager.res +0 -358
  93. package/src/ChainManager.res.mjs +0 -291
  94. package/src/Ctx.res +0 -6
  95. package/src/Ctx.res.mjs +0 -2
  96. package/src/GlobalState.res +0 -1056
  97. package/src/GlobalState.res.mjs +0 -1086
  98. package/src/GlobalStateManager.res +0 -57
  99. package/src/GlobalStateManager.res.mjs +0 -68
  100. package/src/GlobalStateManager.resi +0 -7
  101. package/src/Ports.res +0 -5
  102. package/src/Ports.res.mjs +0 -9
  103. package/src/adapters/MarkBatchProcessedAdapter.res +0 -5
  104. package/src/adapters/MarkBatchProcessedAdapter.res.mjs +0 -14
  105. package/src/sources/EnvioApiClient.res +0 -15
  106. package/src/sources/EnvioApiClient.res.mjs +0 -24
@@ -0,0 +1,536 @@
1
+ type chain = ChainMap.Chain.t
2
+ type rollbackState =
3
+ | NoRollback
4
+ | ReorgDetected({chain: chain, blockNumber: int})
5
+ | FindingReorgDepth
6
+ | FoundReorgDepth({chain: chain, rollbackTargetBlockNumber: int})
7
+ | RollbackReady({eventsProcessedDiffByChain: dict<float>})
8
+
9
+ module WriteThrottlers = {
10
+ type t = {pruneStaleEntityHistory: Throttler.t}
11
+ let make = (): t => {
12
+ let pruneStaleEntityHistory = {
13
+ let intervalMillis = Env.ThrottleWrites.pruneStaleDataIntervalMillis
14
+ let logger = Logging.createChild(
15
+ ~params={
16
+ "context": "Throttler for pruning stale entity history data",
17
+ "intervalMillis": intervalMillis,
18
+ },
19
+ )
20
+ Throttler.make(~intervalMillis, ~logger)
21
+ }
22
+ {pruneStaleEntityHistory: pruneStaleEntityHistory}
23
+ }
24
+ }
25
+
26
+ module EntityTables = {
27
+ type t = dict<InMemoryTable.Entity.t>
28
+ exception UndefinedEntity({entityName: string})
29
+ let make = (entities: array<Internal.entityConfig>): t => {
30
+ let init = Dict.make()
31
+ entities->Array.forEach(entityConfig => {
32
+ init->Dict.set((entityConfig.name :> string), InMemoryTable.Entity.make())
33
+ })
34
+ init
35
+ }
36
+
37
+ let get = (self: t, ~entityName: string) => {
38
+ switch self->Utils.Dict.dangerouslyGetNonOption(entityName) {
39
+ | Some(table) => table
40
+ | None =>
41
+ UndefinedEntity({entityName: entityName})->ErrorHandling.mkLogAndRaise(
42
+ ~msg="Unexpected, entity InMemoryTable is undefined",
43
+ )
44
+ }
45
+ }
46
+ }
47
+
48
+ type effectCacheInMemTable = {
49
+ // Cache keys whose handler output is persisted on the next write. Drained
50
+ // each write; eviction is driven by the per-entry checkpointId instead.
51
+ mutable idsToStore: array<string>,
52
+ mutable invalidationsCount: int,
53
+ // Each entry is stamped with the checkpoint that referenced it (or
54
+ // loadedFromDbCheckpointId for db reads), so committed entries can be
55
+ // dropped once persisted/re-derivable, mirroring entity changes.
56
+ mutable dict: dict<Change.t<Internal.effectOutput>>,
57
+ mutable changesCount: float,
58
+ effect: Internal.effect,
59
+ }
60
+
61
+ type t = {
62
+ config: Config.t,
63
+ persistence: Persistence.t,
64
+ // --- In-memory store: entity/effect tables and the pending-write queue. ---
65
+ allEntities: array<Internal.entityConfig>,
66
+ mutable entities: EntityTables.t,
67
+ mutable effects: dict<effectCacheInMemTable>,
68
+ mutable rollback: option<Persistence.rollback>,
69
+ // Last checkpoint persisted to the db.
70
+ mutable committedCheckpointId: Internal.checkpointId,
71
+ // Processing frontier; runs ahead of committedCheckpointId while writes lag.
72
+ mutable processedCheckpointId: Internal.checkpointId,
73
+ // Processed but unwritten. The cycle drains them, splitting each write at a
74
+ // change in isInReorgThreshold so it never mixes history-saving modes.
75
+ mutable processedBatches: array<Batch.t>,
76
+ // Count of processed batches; version-independent progress counter.
77
+ mutable processedBatchesCount: int,
78
+ // The single in-flight write loop, None when idle.
79
+ mutable writeFiber: option<promise<unit>>,
80
+ // Set once a write throws, to stop the loop. The error itself goes to onError.
81
+ mutable hasFailedWrite: bool,
82
+ // Resolved after every commit so capacity/flush waiters can re-evaluate.
83
+ mutable commitWaiters: array<unit => unit>,
84
+ // Latest metadata staged per chain; used to skip unchanged restages.
85
+ mutable chainMeta: dict<InternalTable.Chains.metaFields>,
86
+ // Set on a real change. Folded into a batch write, else flushed on the throttle.
87
+ mutable chainMetaDirty: bool,
88
+ // Throttles metadata-only writes when no batches flow.
89
+ chainMetaThrottler: Throttler.t,
90
+ // True while a batch is being processed; guards ProcessEventBatch re-entry.
91
+ mutable isProcessing: bool,
92
+ // Whole-indexer view over every chain's runtime state plus the run-wide flags
93
+ // derived from it. Mutated in place through CrossChainState.
94
+ crossChainState: CrossChainState.t,
95
+ mutable rollbackState: rollbackState,
96
+ indexerStartTime: Date.t,
97
+ writeThrottlers: WriteThrottlers.t,
98
+ loadManager: LoadManager.t,
99
+ keepProcessAlive: bool,
100
+ exitAfterFirstEventBlock: bool,
101
+ // The single fatal-error handler.
102
+ onError: ErrorHandling.t => unit,
103
+ // Set once on any fatal error. Every loop checks it to stop iterating and
104
+ // every launch skips when it's set, so a single failure quiesces the indexer.
105
+ mutable isStopped: bool,
106
+ // Bumped when in-flight fetch work must be invalidated: on a reorg (responses
107
+ // requested against pre-reorg state) and on the realtime transition (the
108
+ // waitForNewBlock waiter is bound to the old, pre-realtime source). A fetch
109
+ // response or waiter carrying an older epoch than this is discarded.
110
+ mutable epoch: int,
111
+ }
112
+
113
+ let make = (
114
+ ~config: Config.t,
115
+ ~persistence: Persistence.t,
116
+ ~chainStates: dict<ChainState.t>,
117
+ ~isInReorgThreshold: bool,
118
+ ~isRealtime: bool,
119
+ ~targetBufferSize=CrossChainState.calculateTargetBufferSize(),
120
+ ~committedCheckpointId=Internal.initialCheckpointId,
121
+ ~isDevelopmentMode=false,
122
+ ~shouldUseTui=false,
123
+ ~exitAfterFirstEventBlock=false,
124
+ ~onError: ErrorHandling.t => unit,
125
+ ) => {
126
+ let chainMetaThrottler = {
127
+ let intervalMillis = Env.ThrottleWrites.chainMetadataIntervalMillis
128
+ Throttler.make(
129
+ ~intervalMillis,
130
+ ~logger=Logging.createChild(
131
+ ~params={
132
+ "context": "Throttler for chain metadata writes",
133
+ "intervalMillis": intervalMillis,
134
+ },
135
+ ),
136
+ )
137
+ }
138
+
139
+ {
140
+ config,
141
+ persistence,
142
+ allEntities: persistence.allEntities,
143
+ entities: EntityTables.make(persistence.allEntities),
144
+ effects: Dict.make(),
145
+ rollback: None,
146
+ committedCheckpointId,
147
+ processedCheckpointId: committedCheckpointId,
148
+ processedBatches: [],
149
+ processedBatchesCount: 0,
150
+ writeFiber: None,
151
+ hasFailedWrite: false,
152
+ commitWaiters: [],
153
+ chainMeta: Dict.make(),
154
+ chainMetaDirty: false,
155
+ chainMetaThrottler,
156
+ isProcessing: false,
157
+ crossChainState: CrossChainState.make(
158
+ ~chainStates,
159
+ ~isInReorgThreshold,
160
+ ~isRealtime,
161
+ ~targetBufferSize,
162
+ ),
163
+ indexerStartTime: Date.make(),
164
+ rollbackState: NoRollback,
165
+ writeThrottlers: WriteThrottlers.make(),
166
+ loadManager: LoadManager.make(),
167
+ keepProcessAlive: isDevelopmentMode || shouldUseTui,
168
+ exitAfterFirstEventBlock,
169
+ onError,
170
+ isStopped: false,
171
+ epoch: 0,
172
+ }
173
+ }
174
+
175
+ // Check if progress is past the reorg threshold (safe block).
176
+ // A chain is in reorg threshold when progressBlockNumber > sourceBlockNumber - maxReorgDepth.
177
+ // This matches the logic in InternalTable.Checkpoints.makeGetReorgCheckpointsQuery.
178
+ let isProgressInReorgThreshold = (~progressBlockNumber, ~sourceBlockNumber, ~maxReorgDepth) => {
179
+ maxReorgDepth > 0 &&
180
+ sourceBlockNumber > 0 &&
181
+ progressBlockNumber > sourceBlockNumber - maxReorgDepth
182
+ }
183
+
184
+ let makeFromDbState = (
185
+ ~config: Config.t,
186
+ ~persistence: Persistence.t,
187
+ ~initialState: Persistence.initialState,
188
+ ~registrations,
189
+ ~isDevelopmentMode=false,
190
+ ~shouldUseTui=false,
191
+ ~exitAfterFirstEventBlock=false,
192
+ ~reducedPollingInterval=?,
193
+ ~targetBufferSize=CrossChainState.calculateTargetBufferSize(),
194
+ ~onError,
195
+ ) => {
196
+ let isInReorgThreshold = if initialState.cleanRun {
197
+ false
198
+ } else {
199
+ // Check if any chain is in reorg threshold by comparing progress with sourceBlock - maxReorgDepth.
200
+ initialState.chains->Array.some(chain =>
201
+ isProgressInReorgThreshold(
202
+ ~progressBlockNumber=chain.progressBlockNumber,
203
+ ~sourceBlockNumber=chain.sourceBlockNumber,
204
+ ~maxReorgDepth=chain.maxReorgDepth,
205
+ )
206
+ )
207
+ }
208
+
209
+ Prometheus.ProcessingMaxBatchSize.set(~maxBatchSize=config.batchSize)
210
+ Prometheus.ReorgThreshold.set(~isInReorgThreshold)
211
+ initialState.cache->Utils.Dict.forEach(({effectName, count}) => {
212
+ Prometheus.EffectCacheCount.set(~count, ~effectName)
213
+ })
214
+
215
+ // updateSyncTimeOnRestart wipes the saved timestamp so a restart re-enters
216
+ // backfill mode for all chains.
217
+ let isRealtime =
218
+ !Env.updateSyncTimeOnRestart &&
219
+ initialState.chains->Array.length > 0 &&
220
+ initialState.chains->Array.every(c => c.timestampCaughtUpToHeadOrEndblock->Option.isSome)
221
+
222
+ let chainStates = Dict.make()
223
+ initialState.chains->Array.forEach((resumedChainState: Persistence.initialChainState) => {
224
+ let chain = Config.getChain(config, ~chainId=resumedChainState.id)
225
+ let chainConfig = config.chainMap->ChainMap.get(chain)
226
+ chainStates->Utils.Dict.setByInt(
227
+ resumedChainState.id,
228
+ chainConfig->ChainState.makeFromDbState(
229
+ ~resumedChainState,
230
+ ~reorgCheckpoints=initialState.reorgCheckpoints,
231
+ ~isInReorgThreshold,
232
+ ~isRealtime,
233
+ ~config,
234
+ ~registrations,
235
+ ~reducedPollingInterval?,
236
+ ),
237
+ )
238
+ })
239
+
240
+ // Set initial progress metrics from DB state so dashboards reflect
241
+ // the persisted state immediately on restart
242
+ let allChainsReady = ref(initialState.chains->Array.length > 0)
243
+ chainStates->Utils.Dict.forEach(cs => {
244
+ let chainId = (cs->ChainState.chainConfig).id
245
+ Prometheus.ProgressBlockNumber.set(
246
+ ~blockNumber=cs->ChainState.committedProgressBlockNumber,
247
+ ~chainId,
248
+ )
249
+ Prometheus.ProgressReady.init(~chainId)
250
+ if cs->ChainState.isReady {
251
+ Prometheus.ProgressReady.set(~chainId)
252
+ } else {
253
+ allChainsReady := false
254
+ }
255
+ })
256
+ if allChainsReady.contents {
257
+ Prometheus.ProgressReady.setAllReady()
258
+ }
259
+
260
+ make(
261
+ ~config,
262
+ ~persistence,
263
+ ~chainStates,
264
+ ~isInReorgThreshold,
265
+ ~isRealtime,
266
+ ~targetBufferSize,
267
+ ~committedCheckpointId=initialState.checkpointId,
268
+ ~isDevelopmentMode,
269
+ ~shouldUseTui,
270
+ ~exitAfterFirstEventBlock,
271
+ ~onError,
272
+ )
273
+ }
274
+
275
+ // A fetch response or new-block waiter is stale once the indexer stopped or the
276
+ // epoch moved on (reorg / realtime transition) since the work was scheduled.
277
+ @inline
278
+ let isStale = (state: t, ~stateId) => state.isStopped || stateId !== state.epoch
279
+
280
+ // True from when a reorg is detected until its rollback target is resolved.
281
+ // Fetching and batch processing pause while it holds so they don't act on chain
282
+ // state that's about to be rolled back. Once RollbackReady, processing resumes to
283
+ // apply the diff, so this reads false there.
284
+ let isResolvingReorg = (state: t) =>
285
+ switch state.rollbackState {
286
+ | ReorgDetected(_) | FindingReorgDepth | FoundReorgDepth(_) => true
287
+ | NoRollback | RollbackReady(_) => false
288
+ }
289
+
290
+ // The single fatal-error handler. Stops every loop before reporting, and only
291
+ // reports the first error so redundant handlers (eg an error caught in two
292
+ // nested scopes) don't double-report.
293
+ @inline
294
+ let errorExit = (state: t, errHandler) =>
295
+ if !state.isStopped {
296
+ state.isStopped = true
297
+ state.onError(errHandler)
298
+ }
299
+
300
+ let unexpectedErrorMsg = "Indexer has failed with an unexpected error"
301
+
302
+ // Halt the loops without reporting an error, eg to hand the shared db over to a
303
+ // resumed indexer in tests.
304
+ let stop = (state: t) => state.isStopped = true
305
+
306
+ let getChainState = (state: t, ~chain: chain): ChainState.t =>
307
+ switch state.crossChainState
308
+ ->CrossChainState.chainStates
309
+ ->Utils.Dict.dangerouslyGetByIntNonOption(chain->ChainMap.Chain.toChainId) {
310
+ | Some(cs) => cs
311
+ | None =>
312
+ // Should be unreachable, since we validate on Chain.t creation
313
+ JsError.throwWithMessage(
314
+ "No chain with id " ++ chain->ChainMap.Chain.toString ++ " found in chain states",
315
+ )
316
+ }
317
+
318
+ let getSafeCheckpointId = (state: t) => state.crossChainState->CrossChainState.getSafeCheckpointId
319
+
320
+ let createBatch = (
321
+ state: t,
322
+ ~processedCheckpointId,
323
+ ~batchSizeTarget: int,
324
+ ~isRollback: bool,
325
+ ): Batch.t =>
326
+ state.crossChainState->CrossChainState.createBatch(
327
+ ~processedCheckpointId,
328
+ ~batchSizeTarget,
329
+ ~isRollback,
330
+ )
331
+
332
+ let enterReorgThreshold = (state: t) => state.crossChainState->CrossChainState.enterReorgThreshold
333
+
334
+ // Begin a reorg rollback. Invalidates in-flight fetches and enters the
335
+ // ReorgDetected state as one step, so the epoch bump can never be left out. The
336
+ // caller has already mutated the chain states (restored counters, reset pending
337
+ // queries). isResolvingReorg derives from rollbackState.
338
+ let beginReorg = (state: t, ~chain, ~blockNumber) => {
339
+ state.epoch = state.epoch + 1
340
+ state.rollbackState = ReorgDetected({chain, blockNumber})
341
+ }
342
+
343
+ let enterFindingReorgDepth = (state: t) => state.rollbackState = FindingReorgDepth
344
+
345
+ let foundReorgDepth = (state: t, ~chain, ~rollbackTargetBlockNumber) =>
346
+ state.rollbackState = FoundReorgDepth({chain, rollbackTargetBlockNumber})
347
+
348
+ // Finish a rollback. The caller has already rolled the chain states back in
349
+ // place; this leaves the diff ready for the next batch to consume.
350
+ // RollbackReady makes isResolvingReorg false, so processing resumes to apply it.
351
+ let completeRollback = (state: t, ~eventsProcessedDiffByChain) => {
352
+ state.rollbackState = RollbackReady({eventsProcessedDiffByChain: eventsProcessedDiffByChain})
353
+ }
354
+
355
+ let clearRollback = (state: t) => state.rollbackState = NoRollback
356
+
357
+ // Invalidate in-flight fetches/waiters without starting a rollback, eg on the
358
+ // realtime transition where the parked waiter is bound to the pre-realtime source.
359
+ let invalidateInflight = (state: t) => state.epoch = state.epoch + 1
360
+
361
+ let applyBatchProgress = (state: t, ~batch: Batch.t) =>
362
+ state.crossChainState->CrossChainState.applyBatchProgress(~batch)
363
+
364
+ // Processing-loop mutex. Guards ProcessEventBatch re-entry so only one
365
+ // processing loop runs at a time.
366
+ let isProcessing = (state: t) => state.isProcessing
367
+ let beginProcessing = (state: t) => state.isProcessing = true
368
+ let endProcessing = (state: t) => state.isProcessing = false
369
+
370
+ let recordProcessedBatch = (state: t) =>
371
+ state.processedBatchesCount = state.processedBatchesCount + 1
372
+
373
+ // --- Read accessors. The type is abstract in the interface; modules read state
374
+ // through these and change it only through the transitions above and the domain
375
+ // operations below. Accessors returning a mutable dict/array let callers mutate
376
+ // the container in place (eg insert an entity table). ---
377
+
378
+ let config = (state: t) => state.config
379
+ let persistence = (state: t) => state.persistence
380
+ let allEntities = (state: t) => state.allEntities
381
+ let entities = (state: t) => state.entities
382
+ let effects = (state: t) => state.effects
383
+ let committedCheckpointId = (state: t) => state.committedCheckpointId
384
+ let processedCheckpointId = (state: t) => state.processedCheckpointId
385
+ let processedBatches = (state: t) => state.processedBatches
386
+ let processedBatchesCount = (state: t) => state.processedBatchesCount
387
+ let writeFiber = (state: t) => state.writeFiber
388
+ let hasFailedWrite = (state: t) => state.hasFailedWrite
389
+ let chainMetaDirty = (state: t) => state.chainMetaDirty
390
+ let chainMetaThrottler = (state: t) => state.chainMetaThrottler
391
+ let crossChainState = (state: t) => state.crossChainState
392
+ let chainStates = (state: t) => state.crossChainState->CrossChainState.chainStates
393
+ let isInReorgThreshold = (state: t) => state.crossChainState->CrossChainState.isInReorgThreshold
394
+ let isRealtime = (state: t) => state.crossChainState->CrossChainState.isRealtime
395
+ let rollbackState = (state: t) => state.rollbackState
396
+ let indexerStartTime = (state: t) => state.indexerStartTime
397
+ let loadManager = (state: t) => state.loadManager
398
+ let keepProcessAlive = (state: t) => state.keepProcessAlive
399
+ let exitAfterFirstEventBlock = (state: t) => state.exitAfterFirstEventBlock
400
+ let isStopped = (state: t) => state.isStopped
401
+ let epoch = (state: t) => state.epoch
402
+ let pruneStaleEntityHistoryThrottler = (state: t) => state.writeThrottlers.pruneStaleEntityHistory
403
+
404
+ // --- Store domain operations. ---
405
+
406
+ // Queue a processed batch for writing and advance the processing frontier.
407
+ let queueProcessedBatch = (state: t, ~batch: Batch.t) => {
408
+ state.processedBatches->Array.push(batch)->ignore
409
+ switch batch.checkpointIds->Utils.Array.last {
410
+ | Some(checkpointId) => state.processedCheckpointId = checkpointId
411
+ | None => ()
412
+ }
413
+ }
414
+
415
+ // Take the leading run of queued batches sharing isInReorgThreshold as one merged
416
+ // batch, leaving the rest queued for the next write. Caller guarantees the queue
417
+ // is non-empty.
418
+ let drainBatchRun = (state: t): Batch.t => {
419
+ let all = state.processedBatches
420
+ let isInReorgThreshold = (all->Array.getUnsafe(0)).isInReorgThreshold
421
+
422
+ let rest = []
423
+ let progressedChainsById = Dict.make()
424
+ let totalBatchSize = ref(0)
425
+ let items = []
426
+ let checkpointIds = []
427
+ let checkpointChainIds = []
428
+ let checkpointBlockNumbers = []
429
+ let checkpointBlockHashes = []
430
+ let checkpointEventsProcessed = []
431
+ all->Array.forEach(batch => {
432
+ // Once one batch lands in rest, all later ones follow it, preserving order.
433
+ if rest->Utils.Array.isEmpty && batch.isInReorgThreshold == isInReorgThreshold {
434
+ batch.progressedChainsById->Utils.Dict.forEachWithKey((chainAfterBatch, key) =>
435
+ progressedChainsById->Dict.set(key, chainAfterBatch)
436
+ )
437
+ totalBatchSize := totalBatchSize.contents + batch.totalBatchSize
438
+ items->Array.pushMany(batch.items)
439
+ checkpointIds->Array.pushMany(batch.checkpointIds)
440
+ checkpointChainIds->Array.pushMany(batch.checkpointChainIds)
441
+ checkpointBlockNumbers->Array.pushMany(batch.checkpointBlockNumbers)
442
+ checkpointBlockHashes->Array.pushMany(batch.checkpointBlockHashes)
443
+ checkpointEventsProcessed->Array.pushMany(batch.checkpointEventsProcessed)
444
+ } else {
445
+ rest->Array.push(batch)
446
+ }
447
+ })
448
+ state.processedBatches = rest
449
+
450
+ {
451
+ totalBatchSize: totalBatchSize.contents,
452
+ items,
453
+ progressedChainsById,
454
+ isInReorgThreshold,
455
+ checkpointIds,
456
+ checkpointChainIds,
457
+ checkpointBlockNumbers,
458
+ checkpointBlockHashes,
459
+ checkpointEventsProcessed,
460
+ }
461
+ }
462
+
463
+ // Take the pending rollback diff to write, clearing it from the store.
464
+ let takeRollback = (state: t): option<Persistence.rollback> => {
465
+ let rollback = state.rollback
466
+ state.rollback = None
467
+ rollback
468
+ }
469
+
470
+ // Advance the committed (durably persisted) frontier after a successful write.
471
+ let markCommitted = (state: t, ~upToCheckpointId) => state.committedCheckpointId = upToCheckpointId
472
+
473
+ // Reset the in-memory tables and arm the rollback diff that the next write commits.
474
+ let beginRollbackDiff = (
475
+ state: t,
476
+ ~targetCheckpointId,
477
+ ~diffCheckpointId,
478
+ ~progressBlockNumberByChainId,
479
+ ) => {
480
+ state.entities = EntityTables.make(state.allEntities)
481
+ state.effects = Dict.make()
482
+ state.rollback = Some({
483
+ targetCheckpointId,
484
+ diffCheckpointId,
485
+ progressBlockNumberByChainId,
486
+ })
487
+ }
488
+
489
+ // Stop the write loop and surface the failure; the error itself goes to onError.
490
+ let recordWriteFailure = (state: t, exn) => {
491
+ state.hasFailedWrite = true
492
+ state.onError(exn->ErrorHandling.make(~msg="Failed writing batch to the database"))
493
+ }
494
+
495
+ let beginWriteFiber = (state: t, fiber) => state.writeFiber = Some(fiber)
496
+ let endWriteFiber = (state: t) => state.writeFiber = None
497
+
498
+ // Resolve and clear everyone waiting on a commit so they can re-evaluate.
499
+ let wakeCommitWaiters = (state: t) => {
500
+ let waiters = state.commitWaiters
501
+ state.commitWaiters = []
502
+ waiters->Array.forEach(resolve => resolve())
503
+ }
504
+
505
+ let addCommitWaiter = (state: t, resolve) => state.commitWaiters->Array.push(resolve)->ignore
506
+
507
+ let metaFieldsEqual = (a: InternalTable.Chains.metaFields, b: InternalTable.Chains.metaFields) =>
508
+ a.firstEventBlockNumber == b.firstEventBlockNumber &&
509
+ a.latestFetchedBlockNumber == b.latestFetchedBlockNumber &&
510
+ a.isHyperSync == b.isHyperSync &&
511
+ // Date is boxed; compare epoch ms.
512
+ a.timestampCaughtUpToHeadOrEndblock->Null.toOption->Option.map(Date.getTime) ==
513
+ b.timestampCaughtUpToHeadOrEndblock->Null.toOption->Option.map(Date.getTime)
514
+
515
+ // Stage per-chain metadata, dirtying only on a real change so restages are no-ops.
516
+ let stageChainMeta = (state: t, chainsData: dict<InternalTable.Chains.metaFields>) =>
517
+ chainsData->Utils.Dict.forEachWithKey((meta, chainId) => {
518
+ let changed = switch state.chainMeta->Utils.Dict.dangerouslyGetNonOption(chainId) {
519
+ | Some(prev) => !metaFieldsEqual(meta, prev)
520
+ | None => true
521
+ }
522
+ if changed {
523
+ state.chainMeta->Dict.set(chainId, meta)
524
+ state.chainMetaDirty = true
525
+ }
526
+ })
527
+
528
+ // Take a snapshot of staged metadata to write, clearing the dirty flag. A restage
529
+ // during the in-flight write re-dirties it and is rewritten next iteration.
530
+ let takeChainMetaSnapshot = (state: t): option<dict<InternalTable.Chains.metaFields>> =>
531
+ if state.chainMetaDirty {
532
+ state.chainMetaDirty = false
533
+ Some(state.chainMeta->Utils.Dict.shallowCopy)
534
+ } else {
535
+ None
536
+ }