envio 3.6.1 → 3.7.0-subgraph

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