envio 3.2.0 → 3.3.0-alpha.0

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