envio 3.9.0 → 3.10.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 (110) hide show
  1. package/evm.schema.json +26 -0
  2. package/fuel.schema.json +26 -0
  3. package/index.d.ts +35 -3
  4. package/package.json +6 -7
  5. package/src/Batch.res +9 -0
  6. package/src/ChainState.res +35 -39
  7. package/src/ChainState.res.mjs +14 -11
  8. package/src/ChainState.resi +15 -7
  9. package/src/Config.res +52 -44
  10. package/src/Config.res.mjs +51 -17
  11. package/src/Core.res +4 -0
  12. package/src/CrossChainState.res +15 -23
  13. package/src/CrossChainState.res.mjs +10 -18
  14. package/src/CrossChainState.resi +4 -1
  15. package/src/Ecosystem.res +7 -2
  16. package/src/Envio.res +6 -4
  17. package/src/EventConfigBuilder.res +169 -26
  18. package/src/EventConfigBuilder.res.mjs +124 -15
  19. package/src/HandlerRegister.res +21 -4
  20. package/src/HandlerRegister.res.mjs +24 -5
  21. package/src/InMemoryStore.res +9 -7
  22. package/src/InMemoryStore.res.mjs +3 -3
  23. package/src/IndexerState.res +43 -6
  24. package/src/IndexerState.res.mjs +36 -7
  25. package/src/IndexerState.resi +4 -4
  26. package/src/Internal.res +16 -17
  27. package/src/Metrics.res +72 -6
  28. package/src/Metrics.res.mjs +30 -2
  29. package/src/Persistence.res +36 -30
  30. package/src/Persistence.res.mjs +1 -20
  31. package/src/PgStorage.res +104 -56
  32. package/src/PgStorage.res.mjs +57 -35
  33. package/src/PruneStaleHistory.res +85 -52
  34. package/src/PruneStaleHistory.res.mjs +63 -37
  35. package/src/Rollback.res +68 -45
  36. package/src/Rollback.res.mjs +38 -19
  37. package/src/RollbackCommit.res +7 -9
  38. package/src/RollbackCommit.res.mjs +5 -6
  39. package/src/SafeCheckpointTracking.res +11 -11
  40. package/src/SafeCheckpointTracking.res.mjs +7 -6
  41. package/src/SimulateItems.res +33 -18
  42. package/src/SimulateItems.res.mjs +26 -22
  43. package/src/Sink.res +38 -26
  44. package/src/Sink.res.mjs +21 -16
  45. package/src/TestIndexer.res +5 -5
  46. package/src/TestIndexer.res.mjs +1 -1
  47. package/src/Utils.res +170 -0
  48. package/src/Utils.res.mjs +153 -0
  49. package/src/Writing.res +2 -2
  50. package/src/Writing.res.mjs +1 -1
  51. package/src/bindings/ClickHouse.res +366 -698
  52. package/src/bindings/ClickHouse.res.mjs +371 -421
  53. package/src/bindings/ClickHouseSink.res +337 -0
  54. package/src/bindings/ClickHouseSink.res.mjs +246 -0
  55. package/src/bindings/EventSource.res +8 -2
  56. package/src/bindings/NodeJs.res +6 -0
  57. package/src/bindings/Vitest.res +23 -0
  58. package/src/bindings/Vitest.res.mjs +3 -0
  59. package/src/bindings/WebSocket.res +9 -10
  60. package/src/db/CheckpointBounds.res +53 -0
  61. package/src/db/CheckpointBounds.res.mjs +44 -0
  62. package/src/db/EntityFilter.res +36 -17
  63. package/src/db/EntityFilter.res.mjs +25 -14
  64. package/src/db/EntityHistory.res +47 -22
  65. package/src/db/EntityHistory.res.mjs +19 -12
  66. package/src/db/InternalTable.res +84 -35
  67. package/src/db/InternalTable.res.mjs +56 -23
  68. package/src/db/RollbackFloors.res +55 -0
  69. package/src/db/RollbackFloors.res.mjs +92 -0
  70. package/src/db/Table.res +22 -4
  71. package/src/db/Table.res.mjs +30 -9
  72. package/src/sources/Evm.res +2 -0
  73. package/src/sources/Evm.res.mjs +2 -0
  74. package/src/sources/EvmHyperSyncSource.res +3 -3
  75. package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
  76. package/src/sources/EvmRpcWs.res +124 -0
  77. package/src/sources/EvmRpcWs.res.mjs +117 -0
  78. package/src/sources/Fuel.res +2 -0
  79. package/src/sources/Fuel.res.mjs +2 -0
  80. package/src/sources/FuelHyperSyncSource.res +1 -1
  81. package/src/sources/FuelHyperSyncSource.res.mjs +2 -1
  82. package/src/sources/HeightFeed.res +568 -0
  83. package/src/sources/HeightFeed.res.mjs +452 -0
  84. package/src/sources/HeightStream.res +257 -0
  85. package/src/sources/HeightStream.res.mjs +194 -0
  86. package/src/sources/HyperSync.res +5 -0
  87. package/src/sources/HyperSync.res.mjs +3 -0
  88. package/src/sources/HyperSync.resi +4 -0
  89. package/src/sources/HyperSyncSSE.res +49 -0
  90. package/src/sources/HyperSyncSSE.res.mjs +79 -0
  91. package/src/sources/RpcSource.res +1 -1
  92. package/src/sources/RpcSource.res.mjs +2 -2
  93. package/src/sources/Source.res +54 -1
  94. package/src/sources/Source.res.mjs +25 -0
  95. package/src/sources/SourceManager.res +220 -227
  96. package/src/sources/SourceManager.res.mjs +139 -164
  97. package/src/sources/SourceManager.resi +8 -0
  98. package/src/sources/Svm.res +2 -0
  99. package/src/sources/Svm.res.mjs +2 -0
  100. package/src/sources/SvmHyperSyncClient.res +70 -127
  101. package/src/sources/SvmHyperSyncClient.res.mjs +51 -25
  102. package/src/sources/SvmHyperSyncSource.res +18 -15
  103. package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
  104. package/svm.schema.json +1 -75
  105. package/src/MemoryStorage.res +0 -729
  106. package/src/MemoryStorage.res.mjs +0 -586
  107. package/src/sources/HyperSyncHeightStream.res +0 -129
  108. package/src/sources/HyperSyncHeightStream.res.mjs +0 -110
  109. package/src/sources/RpcWebSocketHeightStream.res +0 -175
  110. package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -180
@@ -1,729 +0,0 @@
1
- // A `Persistence.storage` that keeps everything in process memory, including
2
- // entity history and the rollback queries a reorg needs. It backs
3
- // `createTestIndexer` and the scenario suite's in-memory leg, so a test that
4
- // passes here means the same thing it means against Postgres.
5
- //
6
- // Entities are stored decoded — there's no worker boundary to serialize across —
7
- // so loads and history comparisons work on real bigint/BigDecimal values.
8
-
9
- type checkpointRow = {
10
- id: Internal.checkpointId,
11
- chainId: ChainId.t,
12
- blockNumber: int,
13
- blockHash: option<string>,
14
- eventsProcessed: int,
15
- }
16
-
17
- // One entity change, kept the way the Postgres history table keeps it: the row
18
- // as of the change, tagged with the checkpoint that produced it. A DELETE
19
- // carries no entity — only the key it removed.
20
- type historyRow = {
21
- entityId: EntityId.t,
22
- scope: Internal.chainScope,
23
- checkpointId: Internal.checkpointId,
24
- action: EntityHistory.RowAction.t,
25
- entity: option<Internal.entity>,
26
- }
27
-
28
- type chainRow = {
29
- id: ChainId.t,
30
- startBlock: int,
31
- endBlock: option<int>,
32
- maxReorgDepth: int,
33
- mutable progressBlockNumber: int,
34
- mutable sourceBlockNumber: int,
35
- mutable numEventsProcessed: float,
36
- mutable firstEventBlockNumber: option<int>,
37
- mutable latestFetchedBlockNumber: int,
38
- mutable timestampCaughtUpToHeadOrEndblock: option<Date.t>,
39
- }
40
-
41
- type t = {
42
- // tableName -> row key -> entity, the current state a load reads.
43
- entities: dict<dict<Internal.entity>>,
44
- entityConfigs: dict<Internal.entityConfig>,
45
- // entityName -> changes in write order.
46
- history: dict<array<historyRow>>,
47
- mutable checkpoints: array<checkpointRow>,
48
- chains: dict<chainRow>,
49
- cache: dict<Persistence.effectCacheRecord>,
50
- effectCache: dict<dict<Internal.effectCacheItem>>,
51
- mutable envioInfo: option<JSON.t>,
52
- addresses: AddressRows.Table.t,
53
- mutable isInitialized: bool,
54
- }
55
-
56
- let make = (): t => {
57
- entities: Dict.make(),
58
- entityConfigs: Dict.make(),
59
- history: Dict.make(),
60
- checkpoints: [],
61
- chains: Dict.make(),
62
- cache: Dict.make(),
63
- effectCache: Dict.make(),
64
- envioInfo: None,
65
- addresses: AddressRows.Table.make(),
66
- isInitialized: false,
67
- }
68
-
69
- // Rows of a per-chain entity are keyed per (chain, id): the same id exists
70
- // independently on every chain.
71
- let rowKey = (~scope: Internal.chainScope, ~entityId: EntityId.t) =>
72
- switch scope {
73
- | CrossChain => entityId->EntityId.toKey
74
- | Chain(chainId) => `${chainId->ChainId.toString}|${entityId->EntityId.toKey}`
75
- }
76
-
77
- let historyKey = (~scope: Internal.chainScope, ~entityId: EntityId.t) => rowKey(~scope, ~entityId)
78
-
79
- let readChainId = (entity: Internal.entity, ~field: Table.field): option<ChainId.t> =>
80
- entity
81
- ->(Utils.magic: Internal.entity => dict<ChainId.t>)
82
- ->Utils.Dict.dangerouslyGetNonOption(field.fieldName)
83
-
84
- // The store owns its entities. Copy on the boundary with user code — both when
85
- // handing one out and when taking one in — so a user mutating a returned
86
- // entity, or an object they passed to `set`, can't corrupt the store. The copy
87
- // is shallow (matching InMemoryTable): scalar fields are immutable, but
88
- // array-valued fields still share the backing array.
89
- let copyEntity = (entity: Internal.entity): Internal.entity =>
90
- entity
91
- ->(Utils.magic: Internal.entity => dict<unknown>)
92
- ->Utils.Dict.shallowCopy
93
- ->(Utils.magic: dict<unknown> => Internal.entity)
94
-
95
- let getEntityDict = (state: t, ~name) =>
96
- switch state.entities->Dict.get(name) {
97
- | Some(dict) => dict
98
- | None =>
99
- let dict = Dict.make()
100
- state.entities->Dict.set(name, dict)
101
- dict
102
- }
103
-
104
- let getHistory = (state: t, ~name) =>
105
- switch state.history->Dict.get(name) {
106
- | Some(rows) => rows
107
- | None =>
108
- let rows = []
109
- state.history->Dict.set(name, rows)
110
- rows
111
- }
112
-
113
- let registerEntities = (state: t, ~entities: array<Internal.entityConfig>) =>
114
- entities->Array.forEach(entityConfig => {
115
- state.entityConfigs->Dict.set(entityConfig.name, entityConfig)
116
- state.entityConfigs->Dict.set(entityConfig.table.tableName, entityConfig)
117
- let _ = state->getEntityDict(~name=entityConfig.name)
118
- })
119
-
120
- // Seeds the config's contract addresses, mirroring what PgStorage.initialize
121
- // writes into `envio_addresses`.
122
- let seedConfigAddresses = (
123
- state: t,
124
- ~chainConfigs: array<Config.chain>,
125
- ~contractMapping,
126
- ~ecosystem,
127
- ) => {
128
- // Initialize starts from an empty schema — the Postgres DDL drops and
129
- // recreates one — so a re-initialize must not stack a second copy of the
130
- // config's addresses on what a previous one left behind.
131
- state.addresses->AddressRows.Table.clear
132
- state.addresses->ChainState.seedConfigAddresses(~chainConfigs, ~ecosystem, ~contractMapping)
133
- }
134
-
135
- let addressRowsByChain = (state: t) => state.addresses->AddressRows.Table.groupByChain
136
-
137
- let toInitialChainStates = (state: t): array<Persistence.initialChainState> => {
138
- let addressesByChain = state->addressRowsByChain
139
- state.chains
140
- ->Dict.valuesToArray
141
- ->Array.map((chain): Persistence.initialChainState => {
142
- id: chain.id,
143
- startBlock: chain.startBlock,
144
- endBlock: chain.endBlock,
145
- maxReorgDepth: chain.maxReorgDepth,
146
- progressBlockNumber: chain.progressBlockNumber,
147
- numEventsProcessed: chain.numEventsProcessed,
148
- firstEventBlockNumber: chain.firstEventBlockNumber,
149
- timestampCaughtUpToHeadOrEndblock: chain.timestampCaughtUpToHeadOrEndblock,
150
- addressRows: addressesByChain
151
- ->Utils.Dict.dangerouslyGetNonOption(chain.id->ChainId.toString)
152
- ->Option.getOr(AddressRows.emptySeedRows()),
153
- sourceBlockNumber: chain.sourceBlockNumber,
154
- })
155
- }
156
-
157
- let committedCheckpointId = (state: t) =>
158
- state.checkpoints->Array.reduce(InternalTable.Checkpoints.initialCheckpointId, (max, cp) =>
159
- cp.id > max ? cp.id : max
160
- )
161
-
162
- // Mirrors `makeGetReorgCheckpointsQuery`: the hashed checkpoints still inside
163
- // a reorg-capable chain's threshold, which is what reorg detection resumes from.
164
- let reorgCheckpoints = (state: t): array<Internal.reorgCheckpoint> =>
165
- state.checkpoints->Array.filterMap(cp =>
166
- switch (cp.blockHash, state.chains->Dict.get(cp.chainId->ChainId.toString)) {
167
- | (Some(blockHash), Some(chain)) =>
168
- let safeBlock = chain.sourceBlockNumber - chain.maxReorgDepth
169
- if (
170
- chain.maxReorgDepth > 0 &&
171
- chain.progressBlockNumber > safeBlock &&
172
- cp.blockNumber >= safeBlock
173
- ) {
174
- Some({
175
- Internal.checkpointId: cp.id,
176
- chainId: cp.chainId,
177
- blockNumber: cp.blockNumber,
178
- blockHash,
179
- })
180
- } else {
181
- None
182
- }
183
- | _ => None
184
- }
185
- )
186
-
187
- let toInitialState = (state: t, ~cleanRun, ~contractMapping): Persistence.initialState => {
188
- cleanRun,
189
- contractMapping,
190
- envioInfo: state.envioInfo,
191
- cache: state.cache,
192
- chains: state->toInitialChainStates,
193
- checkpointId: state->committedCheckpointId,
194
- reorgCheckpoints: state->reorgCheckpoints,
195
- }
196
-
197
- let handleLoad = (state: t, ~tableName: string, ~filter: EntityFilter.t): array<
198
- Internal.entity,
199
- > => {
200
- // Effect caches (`envio_effect_<name>`) are loaded through the same call, and
201
- // they have no entity config — they're served from the cache `writeBatch`
202
- // filled, so a resumed indexer reuses cached outputs instead of recomputing
203
- // them the way Postgres would.
204
- switch state.entityConfigs->Dict.get(tableName) {
205
- | None =>
206
- switch state.effectCache->Dict.get(tableName) {
207
- | None => []
208
- | Some(cacheDict) =>
209
- cacheDict
210
- ->Dict.valuesToArray
211
- ->Array.filter(item =>
212
- filter->EntityFilter.matches(
213
- ~entity=item->(Utils.magic: Internal.effectCacheItem => dict<EntityFilter.FieldValue.t>),
214
- )
215
- )
216
- ->(Utils.magic: array<Internal.effectCacheItem> => array<Internal.entity>)
217
- }
218
- | Some(entityConfig) =>
219
- let entityDict = state.entities->Dict.get(entityConfig.name)->Option.getOr(Dict.make())
220
- let matched =
221
- entityDict
222
- ->Dict.valuesToArray
223
- ->Array.filter(entity => {
224
- // The store holds decoded entities and the filter carries decoded values,
225
- // so compare directly (same approach as InMemoryTable) — no JSON round-trip.
226
- let entityAsDict = entity->(Utils.magic: Internal.entity => dict<EntityFilter.FieldValue.t>)
227
- filter->EntityFilter.matches(~entity=entityAsDict)
228
- })
229
- // The chain is already fixed by the scope the load ran for, so the loaded
230
- // entity is handed back in the shape the handlers see.
231
- switch entityConfig.table->Table.getChainIdField {
232
- | None => matched
233
- | Some(field) =>
234
- matched->Array.map(entity => {
235
- let copy = entity->(Utils.magic: Internal.entity => dict<unknown>)->Utils.Dict.shallowCopy
236
- copy->Utils.Dict.deleteInPlace(field.fieldName)
237
- copy->(Utils.magic: dict<unknown> => Internal.entity)
238
- })
239
- }
240
- }
241
- }
242
-
243
- // Postgres backfills a history row for an entity that predates history being
244
- // kept, so a rollback can restore it. Same here: the current row is recorded as
245
- // a SET at the initial checkpoint the first time the entity gains history.
246
- let backfillHistory = (
247
- state: t,
248
- ~entityConfig: Internal.entityConfig,
249
- ~scope,
250
- ~entityId,
251
- ~rows: array<historyRow>,
252
- ) => {
253
- let key = historyKey(~scope, ~entityId)
254
- if !(rows->Array.some(row => historyKey(~scope=row.scope, ~entityId=row.entityId) === key)) {
255
- switch state.entities
256
- ->Dict.get(entityConfig.name)
257
- ->Option.flatMap(dict => dict->Dict.get(rowKey(~scope, ~entityId))) {
258
- | Some(entity) =>
259
- rows
260
- ->Array.push({
261
- entityId,
262
- scope,
263
- checkpointId: InternalTable.Checkpoints.initialCheckpointId,
264
- action: EntityHistory.RowAction.SET,
265
- entity: Some(entity),
266
- })
267
- ->ignore
268
- | None => ()
269
- }
270
- }
271
- }
272
-
273
- let applyRollback = (state: t, ~targetCheckpointId, ~rolledBackAddresses) => {
274
- state.checkpoints = state.checkpoints->Array.filter(cp => cp.id <= targetCheckpointId)
275
- // Addresses are removed by primary key, exactly like the Postgres delete.
276
- rolledBackAddresses->Array.forEach(key => state.addresses->AddressRows.Table.delete(key))
277
- state.history
278
- ->Dict.toArray
279
- ->Array.forEach(((name, rows)) =>
280
- state.history->Dict.set(name, rows->Array.filter(row => row.checkpointId <= targetCheckpointId))
281
- )
282
- }
283
-
284
- let writeBatch = (
285
- state: t,
286
- ~batch: Batch.t,
287
- ~rollback: option<Persistence.rollback>,
288
- ~isInReorgThreshold,
289
- ~config: Config.t,
290
- ~updatedEntities: array<Persistence.updatedEntity>,
291
- ~registeredAddresses: array<AddressRows.staged>,
292
- ~updatedEffectsCache: array<Persistence.updatedEffectCache>,
293
- ~chainMetaData: option<dict<InternalTable.Chains.metaFields>>,
294
- ) => {
295
- let shouldSaveHistory = config->Config.shouldSaveHistory(~isInReorgThreshold)
296
-
297
- // Rollback first, exactly like the Postgres transaction: the batch being
298
- // written is the reprocessed one, so its rows must land on the reverted state.
299
- switch rollback {
300
- | Some({targetCheckpointId, rolledBackAddresses}) =>
301
- state->applyRollback(~targetCheckpointId, ~rolledBackAddresses)
302
- | None => ()
303
- }
304
-
305
- registeredAddresses->Array.forEach(({row}) => state.addresses->AddressRows.Table.insert(row))
306
-
307
- // The rollback diff restates what the reverted state already is, so it is not
308
- // a change history should record — and an id it touches needs no backfill
309
- // either, because the diff already carries its reverted value. Postgres draws
310
- // the same line, keyed on the diff's checkpoint id.
311
- let diffCheckpointId = rollback->Option.map(({diffCheckpointId}) => diffCheckpointId)
312
- let isDiff = (change: Change.t<Internal.entity>) =>
313
- switch diffCheckpointId {
314
- | Some(diffCheckpointId) => change->Change.getCheckpointId === diffCheckpointId
315
- | None => false
316
- }
317
-
318
- updatedEntities->Array.forEach(({entityConfig, scope, changes}: Persistence.updatedEntity) => {
319
- let entityDict = state->getEntityDict(~name=entityConfig.name)
320
- let historyRows = state->getHistory(~name=entityConfig.name)
321
- // The scope is what makes a per-chain row identifiable, so it's stamped
322
- // onto the stored entity the same way the Postgres write path does.
323
- let chainIdField = entityConfig.table->Table.getChainIdField
324
-
325
- let idsWithDiff = Utils.Set.make()
326
- changes->Array.forEach(change =>
327
- if isDiff(change) {
328
- idsWithDiff->Utils.Set.add(change->Change.getEntityId->EntityId.toKey)->ignore
329
- }
330
- )
331
-
332
- changes->Array.forEach(change => {
333
- let entityId = change->Change.getEntityId
334
- let shouldSaveChangeHistory = shouldSaveHistory && !isDiff(change)
335
- if shouldSaveHistory && !(idsWithDiff->Utils.Set.has(entityId->EntityId.toKey)) {
336
- state->backfillHistory(~entityConfig, ~scope, ~entityId, ~rows=historyRows)
337
- }
338
- switch change {
339
- | Set({entity, checkpointId}) =>
340
- let storedEntity = switch (chainIdField, scope->Internal.chainScopeChainId) {
341
- | (Some(field), Some(chainId)) =>
342
- entity->Internal.stampChainId(~fieldName=field.fieldName, ~chainId)
343
- | _ => entity
344
- }
345
- entityDict->Dict.set(rowKey(~scope, ~entityId), storedEntity)
346
- if shouldSaveChangeHistory {
347
- historyRows
348
- ->Array.push({
349
- entityId,
350
- scope,
351
- checkpointId,
352
- action: EntityHistory.RowAction.SET,
353
- entity: Some(storedEntity),
354
- })
355
- ->ignore
356
- }
357
- | Delete({checkpointId}) =>
358
- entityDict->Utils.Dict.deleteInPlace(rowKey(~scope, ~entityId))
359
- if shouldSaveChangeHistory {
360
- historyRows
361
- ->Array.push({
362
- entityId,
363
- scope,
364
- checkpointId,
365
- action: EntityHistory.RowAction.DELETE,
366
- entity: None,
367
- })
368
- ->ignore
369
- }
370
- }
371
- })
372
- })
373
-
374
- batch.progressedChainsById
375
- ->Dict.valuesToArray
376
- ->Array.forEach(chainAfterBatch => {
377
- let key = chainAfterBatch.fetchState.chainId->ChainId.toString
378
- switch state.chains->Dict.get(key) {
379
- | Some(chain) =>
380
- chain.progressBlockNumber = chainAfterBatch.progressBlockNumber
381
- chain.sourceBlockNumber = chainAfterBatch.sourceBlockNumber
382
- chain.numEventsProcessed = chainAfterBatch.totalEventsProcessed
383
- | None => ()
384
- }
385
- })
386
-
387
- switch chainMetaData {
388
- | Some(chainsData) =>
389
- chainsData
390
- ->Dict.toArray
391
- ->Array.forEach(((key, meta)) =>
392
- switch state.chains->Dict.get(key) {
393
- | Some(chain) =>
394
- chain.firstEventBlockNumber = meta.firstEventBlockNumber->Null.toOption
395
- chain.latestFetchedBlockNumber = meta.latestFetchedBlockNumber
396
- chain.timestampCaughtUpToHeadOrEndblock =
397
- meta.timestampCaughtUpToHeadOrEndblock->Null.toOption
398
- | None => ()
399
- }
400
- )
401
- | None => ()
402
- }
403
-
404
- if shouldSaveHistory {
405
- for i in 0 to batch.checkpointIds->Array.length - 1 {
406
- state.checkpoints
407
- ->Array.push({
408
- id: batch.checkpointIds->Array.getUnsafe(i),
409
- chainId: batch.checkpointChainIds->Array.getUnsafe(i),
410
- blockNumber: batch.checkpointBlockNumbers->Array.getUnsafe(i),
411
- blockHash: batch.checkpointBlockHashes->Array.getUnsafe(i)->Null.toOption,
412
- eventsProcessed: batch.checkpointEventsProcessed->Array.getUnsafe(i),
413
- })
414
- ->ignore
415
- }
416
- }
417
-
418
- updatedEffectsCache->Array.forEach(({table, items}: Persistence.updatedEffectCache) => {
419
- let cacheDict = switch state.effectCache->Dict.get(table.tableName) {
420
- | Some(dict) => dict
421
- | None =>
422
- let dict = Dict.make()
423
- state.effectCache->Dict.set(table.tableName, dict)
424
- dict
425
- }
426
- items->Array.forEach(item => cacheDict->Dict.set(item.id, item))
427
- switch state.cache->Dict.get(table.tableName) {
428
- | Some(record) => record.count = cacheDict->Dict.keysToArray->Array.length
429
- | None => ()
430
- }
431
- })
432
- }
433
-
434
- // The latest history row at or before the target, for keys changed after it —
435
- // the memory equivalent of `makeGetRollbackPreTargetRowsQuery`.
436
- let getRollbackData = (
437
- state: t,
438
- ~entityConfig: Internal.entityConfig,
439
- ~rollbackTargetCheckpointId,
440
- ) => {
441
- let rows = state.history->Dict.get(entityConfig.name)->Option.getOr([])
442
-
443
- let changedKeys = Dict.make()
444
- rows->Array.forEach(row =>
445
- if row.checkpointId > rollbackTargetCheckpointId {
446
- changedKeys->Dict.set(historyKey(~scope=row.scope, ~entityId=row.entityId), row)
447
- }
448
- )
449
-
450
- let removals = []
451
- let restored = []
452
-
453
- changedKeys
454
- ->Dict.toArray
455
- ->Array.forEach(((key, changedRow)) => {
456
- let preTarget = rows->Array.reduce(None, (latest, row) =>
457
- if (
458
- historyKey(~scope=row.scope, ~entityId=row.entityId) === key &&
459
- row.checkpointId <= rollbackTargetCheckpointId
460
- ) {
461
- switch latest {
462
- | Some(current: historyRow) if current.checkpointId >= row.checkpointId => latest
463
- | _ => Some(row)
464
- }
465
- } else {
466
- latest
467
- }
468
- )
469
- switch preTarget {
470
- // Nothing before the target: the entity only ever existed after it.
471
- | None =>
472
- removals
473
- ->Array.push({Persistence.entityId: changedRow.entityId, scope: changedRow.scope})
474
- ->ignore
475
- | Some({action: DELETE, entityId, scope}) =>
476
- removals->Array.push({Persistence.entityId, scope})->ignore
477
- | Some({action: SET, entity: Some(entity)}) => restored->Array.push(entity)->ignore
478
- | Some({action: SET, entity: None}) => ()
479
- }
480
- })
481
-
482
- (removals, restored)
483
- }
484
-
485
- type progressDiff = {
486
- chainId: ChainId.t,
487
- mutable eventsProcessed: int,
488
- mutable newProgressBlockNumber: int,
489
- }
490
-
491
- let getRollbackProgressDiff = (state: t, ~rollbackTargetCheckpointId) => {
492
- let byChain = Dict.make()
493
- state.checkpoints->Array.forEach(cp =>
494
- if cp.id > rollbackTargetCheckpointId {
495
- let key = cp.chainId->ChainId.toString
496
- switch byChain->Dict.get(key) {
497
- | Some(acc) =>
498
- acc.eventsProcessed = acc.eventsProcessed + cp.eventsProcessed
499
- acc.newProgressBlockNumber = Math.Int.min(acc.newProgressBlockNumber, cp.blockNumber - 1)
500
- | None =>
501
- byChain->Dict.set(
502
- key,
503
- {
504
- chainId: cp.chainId,
505
- eventsProcessed: cp.eventsProcessed,
506
- newProgressBlockNumber: cp.blockNumber - 1,
507
- },
508
- )
509
- }
510
- }
511
- )
512
- byChain
513
- ->Dict.valuesToArray
514
- ->Array.map(acc =>
515
- {
516
- "chain_id": acc.chainId,
517
- "events_processed_diff": acc.eventsProcessed->Int.toString,
518
- "new_progress_block_number": acc.newProgressBlockNumber,
519
- }
520
- )
521
- }
522
-
523
- let toStorage = (state: t, ~config: Config.t): Persistence.storage => {
524
- name: "memory",
525
- isInitialized: async () => state.isInitialized,
526
- initialize: async (~chainConfigs=[], ~entities=[], ~enums as _=[], ~contractMapping, ~envioInfo) => {
527
- state->registerEntities(~entities)
528
- state->seedConfigAddresses(~chainConfigs, ~contractMapping, ~ecosystem=config.ecosystem.name)
529
- chainConfigs->Array.forEach(chainConfig =>
530
- state.chains->Dict.set(
531
- chainConfig.id->ChainId.toString,
532
- {
533
- id: chainConfig.id,
534
- startBlock: chainConfig.startBlock,
535
- endBlock: chainConfig.endBlock,
536
- maxReorgDepth: chainConfig.maxReorgDepth,
537
- progressBlockNumber: -1,
538
- sourceBlockNumber: 0,
539
- numEventsProcessed: 0.,
540
- firstEventBlockNumber: None,
541
- latestFetchedBlockNumber: 0,
542
- timestampCaughtUpToHeadOrEndblock: None,
543
- },
544
- )
545
- )
546
- state.envioInfo = Some(envioInfo)
547
- state.isInitialized = true
548
- state->toInitialState(~cleanRun=true, ~contractMapping)
549
- },
550
- resumeInitialState: async () =>
551
- state->toInitialState(~cleanRun=false, ~contractMapping=config.contractMapping),
552
- loadOrThrow: async (~filter, ~table: Table.table) =>
553
- state
554
- ->handleLoad(~tableName=table.tableName, ~filter)
555
- ->(Utils.magic: array<Internal.entity> => array<unknown>),
556
- // Nothing to index, and the store is always queryable.
557
- ensureQueryIndexes: async (~table as _, ~filters as _) => (),
558
- ensureSchemaIndexes: async (~entities as _) => (),
559
- finalizeBackfill: async (~entities as _, ~chainIds, ~readyAt) =>
560
- chainIds->Array.forEach(chainId =>
561
- switch state.chains->Dict.get(chainId->ChainId.toString) {
562
- | Some(chain) => chain.timestampCaughtUpToHeadOrEndblock = Some(readyAt)
563
- | None => ()
564
- }
565
- ),
566
- dumpEffectCache: async () => (),
567
- reset: async () => {
568
- let clear = Utils.Dict.clearInPlace
569
- state.entities->clear
570
- state.history->clear
571
- state.chains->clear
572
- state.effectCache->clear
573
- state.cache->clear
574
- state.checkpoints = []
575
- state.addresses->AddressRows.Table.clear
576
- state.envioInfo = None
577
- state.isInitialized = false
578
- },
579
- setChainMeta: async chainsData => {
580
- chainsData
581
- ->Dict.toArray
582
- ->Array.forEach(((key, meta)) =>
583
- switch state.chains->Dict.get(key) {
584
- | Some(chain) =>
585
- chain.firstEventBlockNumber = meta.firstEventBlockNumber->Null.toOption
586
- chain.latestFetchedBlockNumber = meta.latestFetchedBlockNumber
587
- chain.timestampCaughtUpToHeadOrEndblock =
588
- meta.timestampCaughtUpToHeadOrEndblock->Null.toOption
589
- | None => ()
590
- }
591
- )
592
- %raw(`undefined`)
593
- },
594
- pruneStaleCheckpoints: async (~safeCheckpointId) => {
595
- state.checkpoints = state.checkpoints->Array.filter(cp => cp.id >= safeCheckpointId)
596
- },
597
- pruneStaleEntityHistory: async (
598
- ~entityName,
599
- ~entityIndex as _,
600
- ~chainIdColumn as _,
601
- ~safeCheckpointId,
602
- ) => {
603
- switch state.history->Dict.get(entityName) {
604
- | None => ()
605
- | Some(rows) =>
606
- // Keep the newest row below the safe point per key: it's what a rollback
607
- // to the safe checkpoint restores to.
608
- let newestBelow = Dict.make()
609
- rows->Array.forEach(row =>
610
- if row.checkpointId < safeCheckpointId {
611
- let key = historyKey(~scope=row.scope, ~entityId=row.entityId)
612
- switch newestBelow->Dict.get(key) {
613
- | Some(current: historyRow) if current.checkpointId >= row.checkpointId => ()
614
- | _ => newestBelow->Dict.set(key, row)
615
- }
616
- }
617
- )
618
- state.history->Dict.set(
619
- entityName,
620
- rows->Array.filter(row => {
621
- let key = historyKey(~scope=row.scope, ~entityId=row.entityId)
622
- row.checkpointId >= safeCheckpointId ||
623
- switch newestBelow->Dict.get(key) {
624
- | Some(kept) => kept === row
625
- | None => false
626
- }
627
- }),
628
- )
629
- }
630
- },
631
- getRollbackTargetCheckpoint: async (~reorgChainId, ~lastKnownValidBlockNumber) =>
632
- state.checkpoints->Array.reduce(None, (target, cp) =>
633
- if cp.chainId == reorgChainId && cp.blockNumber <= lastKnownValidBlockNumber {
634
- switch target {
635
- | Some(current) if current >= cp.id => target
636
- | _ => Some(cp.id)
637
- }
638
- } else {
639
- target
640
- }
641
- ),
642
- getRollbackProgressDiff: async (~rollbackTargetCheckpointId) =>
643
- state->getRollbackProgressDiff(~rollbackTargetCheckpointId),
644
- getRollbackData: async (~entityConfig, ~rollbackTargetCheckpointId) =>
645
- state->getRollbackData(~entityConfig, ~rollbackTargetCheckpointId),
646
- writeBatch: async (
647
- ~batch,
648
- ~rollback,
649
- ~isInReorgThreshold,
650
- ~config as _,
651
- ~allEntities as _,
652
- ~updatedEffectsCache,
653
- ~updatedEntities,
654
- ~registeredAddresses,
655
- ~chainMetaData,
656
- ~onWrite as _,
657
- ) =>
658
- state->writeBatch(
659
- ~batch,
660
- ~rollback,
661
- ~isInReorgThreshold,
662
- ~config,
663
- ~updatedEntities,
664
- ~registeredAddresses,
665
- ~updatedEffectsCache,
666
- ~chainMetaData,
667
- ),
668
- close: async () => (),
669
- }
670
-
671
- // Read helpers for tests. They mirror what the Postgres-backed equivalents
672
- // return, so one assertion can run against either backend.
673
- let currentRows = (state: t, ~entityConfig: Internal.entityConfig): array<Internal.entity> =>
674
- state.entities->Dict.get(entityConfig.name)->Option.getOr(Dict.make())->Dict.valuesToArray
675
-
676
- let historyChanges = (state: t, ~entityConfig: Internal.entityConfig): array<
677
- Change.t<Internal.entity>,
678
- > =>
679
- state.history
680
- ->Dict.get(entityConfig.name)
681
- ->Option.getOr([])
682
- ->Array.map(row =>
683
- switch row.action {
684
- | SET =>
685
- Change.Set({
686
- entityId: row.entityId,
687
- checkpointId: row.checkpointId,
688
- entity: row.entity->Option.getOr(%raw(`{}`)),
689
- })
690
- | DELETE => Change.Delete({entityId: row.entityId, checkpointId: row.checkpointId})
691
- }
692
- )
693
- ->Array.toSorted((a, b) =>
694
- switch String.compare(
695
- a->Change.getEntityId->EntityId.toKey,
696
- b->Change.getEntityId->EntityId.toKey,
697
- ) {
698
- | 0. =>
699
- Float.compare(
700
- a->Change.getCheckpointId->BigInt.toFloat,
701
- b->Change.getCheckpointId->BigInt.toFloat,
702
- )
703
- | order => order
704
- }
705
- )
706
-
707
- let checkpointRows = (state: t): array<InternalTable.Checkpoints.t> =>
708
- state.checkpoints->Array.map(cp => {
709
- InternalTable.Checkpoints.id: cp.id,
710
- chainId: cp.chainId,
711
- blockNumber: cp.blockNumber,
712
- blockHash: cp.blockHash->Null.fromOption,
713
- eventsProcessed: cp.eventsProcessed,
714
- })
715
-
716
- let effectCacheRows = (state: t, ~tableName): array<{
717
- "id": string,
718
- "output": JSON.t,
719
- }> =>
720
- state.effectCache
721
- ->Dict.get(tableName)
722
- ->Option.getOr(Dict.make())
723
- ->Dict.valuesToArray
724
- ->Array.map(item =>
725
- {
726
- "id": item.id,
727
- "output": item.output->(Utils.magic: Internal.effectOutput => JSON.t),
728
- }
729
- )