envio 3.6.1 → 3.7.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.
- package/index.d.ts +197 -9
- package/package.json +6 -6
- package/src/Batch.res +64 -25
- package/src/Batch.res.mjs +69 -46
- package/src/ChainFetching.res +13 -17
- package/src/ChainFetching.res.mjs +9 -14
- package/src/ChainState.res +160 -119
- package/src/ChainState.res.mjs +75 -46
- package/src/ChainState.resi +15 -12
- package/src/Config.res +9 -2
- package/src/Config.res.mjs +4 -4
- package/src/Core.res +5 -0
- package/src/Ecosystem.res +0 -1
- package/src/Envio.res +16 -3
- package/src/EventConfigBuilder.res +155 -40
- package/src/EventConfigBuilder.res.mjs +76 -24
- package/src/EventProcessing.res +4 -8
- package/src/EventProcessing.res.mjs +4 -4
- package/src/EventUtils.res +3 -47
- package/src/FetchState.res +124 -104
- package/src/FetchState.res.mjs +116 -100
- package/src/HandlerRegister.res +108 -2
- package/src/HandlerRegister.res.mjs +59 -10
- package/src/HandlerRegister.resi +4 -0
- package/src/Internal.res +84 -24
- package/src/Internal.res.mjs +33 -2
- package/src/LazyLoader.res +12 -11
- package/src/LazyLoader.res.mjs +13 -7
- package/src/Main.res +22 -9
- package/src/Main.res.mjs +18 -5
- package/src/MemoryStorage.res +724 -0
- package/src/MemoryStorage.res.mjs +601 -0
- package/src/PgStorage.res +18 -7
- package/src/PgStorage.res.mjs +14 -11
- package/src/ReorgDetection.res +0 -222
- package/src/ReorgDetection.res.mjs +0 -163
- package/src/Rollback.res +14 -15
- package/src/Rollback.res.mjs +4 -5
- package/src/SimulateItems.res +2 -2
- package/src/Utils.res +3 -0
- package/src/bindings/ClickHouse.res +16 -11
- package/src/bindings/ClickHouse.res.mjs +10 -10
- package/src/bindings/Vitest.res +1 -1
- package/src/sources/BlockStore.res +115 -5
- package/src/sources/BlockStore.res.mjs +25 -0
- package/src/sources/Evm.res +0 -1
- package/src/sources/Evm.res.mjs +0 -1
- package/src/sources/EvmHyperSyncSource.res +19 -84
- package/src/sources/EvmHyperSyncSource.res.mjs +24 -60
- package/src/sources/Fuel.res +24 -4
- package/src/sources/Fuel.res.mjs +23 -3
- package/src/sources/FuelHyperSync.res +8 -3
- package/src/sources/FuelHyperSync.res.mjs +5 -4
- package/src/sources/FuelHyperSync.resi +3 -1
- package/src/sources/FuelHyperSyncClient.res +7 -10
- package/src/sources/FuelHyperSyncSource.res +18 -45
- package/src/sources/FuelHyperSyncSource.res.mjs +18 -35
- package/src/sources/HyperSync.res +49 -229
- package/src/sources/HyperSync.res.mjs +74 -191
- package/src/sources/HyperSync.resi +11 -35
- package/src/sources/HyperSyncClient.res +9 -79
- package/src/sources/HyperSyncClient.res.mjs +2 -6
- package/src/sources/RequestStat.res +5 -0
- package/src/sources/RequestStat.res.mjs +2 -0
- package/src/sources/RpcSource.res +149 -43
- package/src/sources/RpcSource.res.mjs +104 -41
- package/src/sources/SimulateSource.res +8 -5
- package/src/sources/SimulateSource.res.mjs +6 -6
- package/src/sources/Source.res +89 -16
- package/src/sources/Source.res.mjs +50 -1
- package/src/sources/SourceManager.res +255 -50
- package/src/sources/SourceManager.res.mjs +149 -55
- package/src/sources/SourceManager.resi +2 -6
- package/src/sources/Svm.res +0 -7
- package/src/sources/Svm.res.mjs +0 -8
- package/src/sources/SvmHyperSyncClient.res +21 -16
- package/src/sources/SvmHyperSyncClient.res.mjs +3 -4
- package/src/sources/SvmHyperSyncSource.res +25 -117
- package/src/sources/SvmHyperSyncSource.res.mjs +8 -121
- package/svm.schema.json +3 -2
|
@@ -0,0 +1,724 @@
|
|
|
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
|
+
// Loads for non-entity tables (e.g. effect caches `envio_effect_<name>`) reach
|
|
236
|
+
// here too. Nothing persists those, so there's nothing to return — an empty
|
|
237
|
+
// result makes the effect recompute instead of crashing on a missing config.
|
|
238
|
+
switch state.entityConfigs->Dict.get(tableName) {
|
|
239
|
+
| None => []
|
|
240
|
+
| Some(entityConfig) =>
|
|
241
|
+
let entityDict = state.entities->Dict.get(entityConfig.name)->Option.getOr(Dict.make())
|
|
242
|
+
let matched =
|
|
243
|
+
entityDict
|
|
244
|
+
->Dict.valuesToArray
|
|
245
|
+
->Array.filter(entity => {
|
|
246
|
+
// The store holds decoded entities and the filter carries decoded values,
|
|
247
|
+
// so compare directly (same approach as InMemoryTable) — no JSON round-trip.
|
|
248
|
+
let entityAsDict = entity->(Utils.magic: Internal.entity => dict<EntityFilter.FieldValue.t>)
|
|
249
|
+
filter->EntityFilter.matches(~entity=entityAsDict)
|
|
250
|
+
})
|
|
251
|
+
// The chain is already fixed by the scope the load ran for, so the loaded
|
|
252
|
+
// entity is handed back in the shape the handlers see.
|
|
253
|
+
switch entityConfig.table->Table.getChainIdField {
|
|
254
|
+
| None => matched
|
|
255
|
+
| Some(field) =>
|
|
256
|
+
matched->Array.map(entity => {
|
|
257
|
+
let copy = entity->(Utils.magic: Internal.entity => dict<unknown>)->Utils.Dict.shallowCopy
|
|
258
|
+
copy->Utils.Dict.deleteInPlace(field.fieldName)
|
|
259
|
+
copy->(Utils.magic: dict<unknown> => Internal.entity)
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Postgres backfills a history row for an entity that predates history being
|
|
266
|
+
// kept, so a rollback can restore it. Same here: the current row is recorded as
|
|
267
|
+
// a SET at the initial checkpoint the first time the entity gains history.
|
|
268
|
+
let backfillHistory = (
|
|
269
|
+
state: t,
|
|
270
|
+
~entityConfig: Internal.entityConfig,
|
|
271
|
+
~scope,
|
|
272
|
+
~entityId,
|
|
273
|
+
~rows: array<historyRow>,
|
|
274
|
+
) => {
|
|
275
|
+
let key = historyKey(~scope, ~entityId)
|
|
276
|
+
if !(rows->Array.some(row => historyKey(~scope=row.scope, ~entityId=row.entityId) === key)) {
|
|
277
|
+
switch state.entities
|
|
278
|
+
->Dict.get(entityConfig.name)
|
|
279
|
+
->Option.flatMap(dict => dict->Dict.get(rowKey(~scope, ~entityId))) {
|
|
280
|
+
| Some(entity) =>
|
|
281
|
+
rows
|
|
282
|
+
->Array.push({
|
|
283
|
+
entityId,
|
|
284
|
+
scope,
|
|
285
|
+
checkpointId: InternalTable.Checkpoints.initialCheckpointId,
|
|
286
|
+
action: EntityHistory.RowAction.SET,
|
|
287
|
+
entity: Some(entity),
|
|
288
|
+
})
|
|
289
|
+
->ignore
|
|
290
|
+
| None => ()
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let applyRollback = (state: t, ~targetCheckpointId) => {
|
|
296
|
+
state.checkpoints = state.checkpoints->Array.filter(cp => cp.id <= targetCheckpointId)
|
|
297
|
+
state.history
|
|
298
|
+
->Dict.toArray
|
|
299
|
+
->Array.forEach(((name, rows)) =>
|
|
300
|
+
state.history->Dict.set(name, rows->Array.filter(row => row.checkpointId <= targetCheckpointId))
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let writeBatch = (
|
|
305
|
+
state: t,
|
|
306
|
+
~batch: Batch.t,
|
|
307
|
+
~rollback: option<Persistence.rollback>,
|
|
308
|
+
~isInReorgThreshold,
|
|
309
|
+
~config: Config.t,
|
|
310
|
+
~updatedEntities: array<Persistence.updatedEntity>,
|
|
311
|
+
~updatedEffectsCache: array<Persistence.updatedEffectCache>,
|
|
312
|
+
~chainMetaData: option<dict<InternalTable.Chains.metaFields>>,
|
|
313
|
+
) => {
|
|
314
|
+
let shouldSaveHistory = config->Config.shouldSaveHistory(~isInReorgThreshold)
|
|
315
|
+
|
|
316
|
+
// Rollback first, exactly like the Postgres transaction: the batch being
|
|
317
|
+
// written is the reprocessed one, so its rows must land on the reverted state.
|
|
318
|
+
switch rollback {
|
|
319
|
+
| Some({targetCheckpointId}) => state->applyRollback(~targetCheckpointId)
|
|
320
|
+
| None => ()
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
updatedEntities->Array.forEach(({entityConfig, scope, changes}: Persistence.updatedEntity) => {
|
|
324
|
+
let entityDict = state->getEntityDict(~name=entityConfig.name)
|
|
325
|
+
let historyRows = state->getHistory(~name=entityConfig.name)
|
|
326
|
+
// The scope is what makes a per-chain row identifiable, so it's stamped
|
|
327
|
+
// onto the stored entity the same way the Postgres write path does.
|
|
328
|
+
let chainIdField = entityConfig.table->Table.getChainIdField
|
|
329
|
+
|
|
330
|
+
changes->Array.forEach(change => {
|
|
331
|
+
let entityId = change->Change.getEntityId
|
|
332
|
+
if shouldSaveHistory {
|
|
333
|
+
state->backfillHistory(~entityConfig, ~scope, ~entityId, ~rows=historyRows)
|
|
334
|
+
}
|
|
335
|
+
switch change {
|
|
336
|
+
| Set({entity, checkpointId}) =>
|
|
337
|
+
let storedEntity = switch (chainIdField, scope->Internal.chainScopeChainId) {
|
|
338
|
+
| (Some(field), Some(chainId)) =>
|
|
339
|
+
entity->Internal.stampChainId(~fieldName=field.fieldName, ~chainId)
|
|
340
|
+
| _ => entity
|
|
341
|
+
}
|
|
342
|
+
entityDict->Dict.set(rowKey(~scope, ~entityId), storedEntity)
|
|
343
|
+
if shouldSaveHistory {
|
|
344
|
+
historyRows
|
|
345
|
+
->Array.push({
|
|
346
|
+
entityId,
|
|
347
|
+
scope,
|
|
348
|
+
checkpointId,
|
|
349
|
+
action: EntityHistory.RowAction.SET,
|
|
350
|
+
entity: Some(storedEntity),
|
|
351
|
+
})
|
|
352
|
+
->ignore
|
|
353
|
+
}
|
|
354
|
+
| Delete({checkpointId}) =>
|
|
355
|
+
entityDict->Utils.Dict.deleteInPlace(rowKey(~scope, ~entityId))
|
|
356
|
+
if shouldSaveHistory {
|
|
357
|
+
historyRows
|
|
358
|
+
->Array.push({
|
|
359
|
+
entityId,
|
|
360
|
+
scope,
|
|
361
|
+
checkpointId,
|
|
362
|
+
action: EntityHistory.RowAction.DELETE,
|
|
363
|
+
entity: None,
|
|
364
|
+
})
|
|
365
|
+
->ignore
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
batch.progressedChainsById
|
|
372
|
+
->Dict.valuesToArray
|
|
373
|
+
->Array.forEach(chainAfterBatch => {
|
|
374
|
+
let key = chainAfterBatch.fetchState.chainId->ChainId.toString
|
|
375
|
+
switch state.chains->Dict.get(key) {
|
|
376
|
+
| Some(chain) =>
|
|
377
|
+
chain.progressBlockNumber = chainAfterBatch.progressBlockNumber
|
|
378
|
+
chain.sourceBlockNumber = chainAfterBatch.sourceBlockNumber
|
|
379
|
+
chain.numEventsProcessed = chainAfterBatch.totalEventsProcessed
|
|
380
|
+
| None => ()
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
switch chainMetaData {
|
|
385
|
+
| Some(chainsData) =>
|
|
386
|
+
chainsData
|
|
387
|
+
->Dict.toArray
|
|
388
|
+
->Array.forEach(((key, meta)) =>
|
|
389
|
+
switch state.chains->Dict.get(key) {
|
|
390
|
+
| Some(chain) =>
|
|
391
|
+
chain.firstEventBlockNumber = meta.firstEventBlockNumber->Null.toOption
|
|
392
|
+
chain.latestFetchedBlockNumber = meta.latestFetchedBlockNumber
|
|
393
|
+
chain.timestampCaughtUpToHeadOrEndblock =
|
|
394
|
+
meta.timestampCaughtUpToHeadOrEndblock->Null.toOption
|
|
395
|
+
| None => ()
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
| None => ()
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if shouldSaveHistory {
|
|
402
|
+
for i in 0 to batch.checkpointIds->Array.length - 1 {
|
|
403
|
+
state.checkpoints
|
|
404
|
+
->Array.push({
|
|
405
|
+
id: batch.checkpointIds->Array.getUnsafe(i),
|
|
406
|
+
chainId: batch.checkpointChainIds->Array.getUnsafe(i),
|
|
407
|
+
blockNumber: batch.checkpointBlockNumbers->Array.getUnsafe(i),
|
|
408
|
+
blockHash: batch.checkpointBlockHashes->Array.getUnsafe(i)->Null.toOption,
|
|
409
|
+
eventsProcessed: batch.checkpointEventsProcessed->Array.getUnsafe(i),
|
|
410
|
+
})
|
|
411
|
+
->ignore
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
updatedEffectsCache->Array.forEach(({table, items}: Persistence.updatedEffectCache) => {
|
|
416
|
+
let cacheDict = switch state.effectCache->Dict.get(table.tableName) {
|
|
417
|
+
| Some(dict) => dict
|
|
418
|
+
| None =>
|
|
419
|
+
let dict = Dict.make()
|
|
420
|
+
state.effectCache->Dict.set(table.tableName, dict)
|
|
421
|
+
dict
|
|
422
|
+
}
|
|
423
|
+
items->Array.forEach(item => cacheDict->Dict.set(item.id, item))
|
|
424
|
+
switch state.cache->Dict.get(table.tableName) {
|
|
425
|
+
| Some(record) => record.count = cacheDict->Dict.keysToArray->Array.length
|
|
426
|
+
| None => ()
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// The latest history row at or before the target, for keys changed after it —
|
|
432
|
+
// the memory equivalent of `makeGetRollbackPreTargetRowsQuery`.
|
|
433
|
+
let getRollbackData = (
|
|
434
|
+
state: t,
|
|
435
|
+
~entityConfig: Internal.entityConfig,
|
|
436
|
+
~rollbackTargetCheckpointId,
|
|
437
|
+
) => {
|
|
438
|
+
let rows = state.history->Dict.get(entityConfig.name)->Option.getOr([])
|
|
439
|
+
|
|
440
|
+
let changedKeys = Dict.make()
|
|
441
|
+
rows->Array.forEach(row =>
|
|
442
|
+
if row.checkpointId > rollbackTargetCheckpointId {
|
|
443
|
+
changedKeys->Dict.set(historyKey(~scope=row.scope, ~entityId=row.entityId), row)
|
|
444
|
+
}
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
let removals = []
|
|
448
|
+
let restored = []
|
|
449
|
+
|
|
450
|
+
changedKeys
|
|
451
|
+
->Dict.toArray
|
|
452
|
+
->Array.forEach(((key, changedRow)) => {
|
|
453
|
+
let preTarget = rows->Array.reduce(None, (latest, row) =>
|
|
454
|
+
if (
|
|
455
|
+
historyKey(~scope=row.scope, ~entityId=row.entityId) === key &&
|
|
456
|
+
row.checkpointId <= rollbackTargetCheckpointId
|
|
457
|
+
) {
|
|
458
|
+
switch latest {
|
|
459
|
+
| Some(current: historyRow) if current.checkpointId >= row.checkpointId => latest
|
|
460
|
+
| _ => Some(row)
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
latest
|
|
464
|
+
}
|
|
465
|
+
)
|
|
466
|
+
switch preTarget {
|
|
467
|
+
// Nothing before the target: the entity only ever existed after it.
|
|
468
|
+
| None =>
|
|
469
|
+
removals
|
|
470
|
+
->Array.push({Persistence.entityId: changedRow.entityId, scope: changedRow.scope})
|
|
471
|
+
->ignore
|
|
472
|
+
| Some({action: DELETE, entityId, scope}) =>
|
|
473
|
+
removals->Array.push({Persistence.entityId, scope})->ignore
|
|
474
|
+
| Some({action: SET, entity: Some(entity)}) =>
|
|
475
|
+
restored->Array.push(entity->(Utils.magic: Internal.entity => unknown))->ignore
|
|
476
|
+
| Some({action: SET, entity: None}) => ()
|
|
477
|
+
}
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
(removals, restored)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
type progressDiff = {
|
|
484
|
+
chainId: ChainId.t,
|
|
485
|
+
mutable eventsProcessed: int,
|
|
486
|
+
mutable newProgressBlockNumber: int,
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
let getRollbackProgressDiff = (state: t, ~rollbackTargetCheckpointId) => {
|
|
490
|
+
let byChain = Dict.make()
|
|
491
|
+
state.checkpoints->Array.forEach(cp =>
|
|
492
|
+
if cp.id > rollbackTargetCheckpointId {
|
|
493
|
+
let key = cp.chainId->ChainId.toString
|
|
494
|
+
switch byChain->Dict.get(key) {
|
|
495
|
+
| Some(acc) =>
|
|
496
|
+
acc.eventsProcessed = acc.eventsProcessed + cp.eventsProcessed
|
|
497
|
+
acc.newProgressBlockNumber = Math.Int.min(acc.newProgressBlockNumber, cp.blockNumber - 1)
|
|
498
|
+
| None =>
|
|
499
|
+
byChain->Dict.set(
|
|
500
|
+
key,
|
|
501
|
+
{
|
|
502
|
+
chainId: cp.chainId,
|
|
503
|
+
eventsProcessed: cp.eventsProcessed,
|
|
504
|
+
newProgressBlockNumber: cp.blockNumber - 1,
|
|
505
|
+
},
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
)
|
|
510
|
+
byChain
|
|
511
|
+
->Dict.valuesToArray
|
|
512
|
+
->Array.map(acc =>
|
|
513
|
+
{
|
|
514
|
+
"chain_id": acc.chainId,
|
|
515
|
+
"events_processed_diff": acc.eventsProcessed->Int.toString,
|
|
516
|
+
"new_progress_block_number": acc.newProgressBlockNumber,
|
|
517
|
+
}
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
let toStorage = (state: t, ~config: Config.t): Persistence.storage => {
|
|
522
|
+
name: "memory",
|
|
523
|
+
isInitialized: async () => state.isInitialized,
|
|
524
|
+
initialize: async (~chainConfigs=[], ~entities=[], ~enums as _=[], ~envioInfo) => {
|
|
525
|
+
state->registerEntities(~entities)
|
|
526
|
+
state->seedIndexingAddresses(~chainConfigs)
|
|
527
|
+
chainConfigs->Array.forEach(chainConfig =>
|
|
528
|
+
state.chains->Dict.set(
|
|
529
|
+
chainConfig.id->ChainId.toString,
|
|
530
|
+
{
|
|
531
|
+
id: chainConfig.id,
|
|
532
|
+
startBlock: chainConfig.startBlock,
|
|
533
|
+
endBlock: chainConfig.endBlock,
|
|
534
|
+
maxReorgDepth: chainConfig.maxReorgDepth,
|
|
535
|
+
progressBlockNumber: -1,
|
|
536
|
+
sourceBlockNumber: 0,
|
|
537
|
+
numEventsProcessed: 0.,
|
|
538
|
+
firstEventBlockNumber: None,
|
|
539
|
+
latestFetchedBlockNumber: 0,
|
|
540
|
+
timestampCaughtUpToHeadOrEndblock: None,
|
|
541
|
+
},
|
|
542
|
+
)
|
|
543
|
+
)
|
|
544
|
+
state.envioInfo = Some(envioInfo)
|
|
545
|
+
state.isInitialized = true
|
|
546
|
+
state->toInitialState(~cleanRun=true)
|
|
547
|
+
},
|
|
548
|
+
resumeInitialState: async () => state->toInitialState(~cleanRun=false),
|
|
549
|
+
loadOrThrow: async (~filter, ~table: Table.table) =>
|
|
550
|
+
state
|
|
551
|
+
->handleLoad(~tableName=table.tableName, ~filter)
|
|
552
|
+
->(Utils.magic: array<Internal.entity> => array<unknown>),
|
|
553
|
+
// Nothing to index, and the store is always queryable.
|
|
554
|
+
ensureQueryIndexes: async (~table as _, ~filters as _) => (),
|
|
555
|
+
ensureSchemaIndexes: async (~entities as _) => (),
|
|
556
|
+
finalizeBackfill: async (~entities as _, ~chainIds, ~readyAt) =>
|
|
557
|
+
chainIds->Array.forEach(chainId =>
|
|
558
|
+
switch state.chains->Dict.get(chainId->ChainId.toString) {
|
|
559
|
+
| Some(chain) => chain.timestampCaughtUpToHeadOrEndblock = Some(readyAt)
|
|
560
|
+
| None => ()
|
|
561
|
+
}
|
|
562
|
+
),
|
|
563
|
+
dumpEffectCache: async () => (),
|
|
564
|
+
reset: async () => {
|
|
565
|
+
let clear = dict =>
|
|
566
|
+
dict->Dict.keysToArray->Array.forEach(key => dict->Utils.Dict.deleteInPlace(key))
|
|
567
|
+
state.entities->clear
|
|
568
|
+
state.history->clear
|
|
569
|
+
state.chains->clear
|
|
570
|
+
state.effectCache->clear
|
|
571
|
+
state.cache->clear
|
|
572
|
+
state.checkpoints = []
|
|
573
|
+
state.envioInfo = None
|
|
574
|
+
state.isInitialized = false
|
|
575
|
+
},
|
|
576
|
+
setChainMeta: async chainsData => {
|
|
577
|
+
chainsData
|
|
578
|
+
->Dict.toArray
|
|
579
|
+
->Array.forEach(((key, meta)) =>
|
|
580
|
+
switch state.chains->Dict.get(key) {
|
|
581
|
+
| Some(chain) =>
|
|
582
|
+
chain.firstEventBlockNumber = meta.firstEventBlockNumber->Null.toOption
|
|
583
|
+
chain.latestFetchedBlockNumber = meta.latestFetchedBlockNumber
|
|
584
|
+
chain.timestampCaughtUpToHeadOrEndblock =
|
|
585
|
+
meta.timestampCaughtUpToHeadOrEndblock->Null.toOption
|
|
586
|
+
| None => ()
|
|
587
|
+
}
|
|
588
|
+
)
|
|
589
|
+
%raw(`undefined`)
|
|
590
|
+
},
|
|
591
|
+
pruneStaleCheckpoints: async (~safeCheckpointId) => {
|
|
592
|
+
state.checkpoints = state.checkpoints->Array.filter(cp => cp.id >= safeCheckpointId)
|
|
593
|
+
},
|
|
594
|
+
pruneStaleEntityHistory: async (
|
|
595
|
+
~entityName,
|
|
596
|
+
~entityIndex as _,
|
|
597
|
+
~chainIdColumn as _,
|
|
598
|
+
~safeCheckpointId,
|
|
599
|
+
) => {
|
|
600
|
+
switch state.history->Dict.get(entityName) {
|
|
601
|
+
| None => ()
|
|
602
|
+
| Some(rows) =>
|
|
603
|
+
// Keep the newest row below the safe point per key: it's what a rollback
|
|
604
|
+
// to the safe checkpoint restores to.
|
|
605
|
+
let newestBelow = Dict.make()
|
|
606
|
+
rows->Array.forEach(row =>
|
|
607
|
+
if row.checkpointId < safeCheckpointId {
|
|
608
|
+
let key = historyKey(~scope=row.scope, ~entityId=row.entityId)
|
|
609
|
+
switch newestBelow->Dict.get(key) {
|
|
610
|
+
| Some(current: historyRow) if current.checkpointId >= row.checkpointId => ()
|
|
611
|
+
| _ => newestBelow->Dict.set(key, row)
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
)
|
|
615
|
+
state.history->Dict.set(
|
|
616
|
+
entityName,
|
|
617
|
+
rows->Array.filter(row => {
|
|
618
|
+
let key = historyKey(~scope=row.scope, ~entityId=row.entityId)
|
|
619
|
+
row.checkpointId >= safeCheckpointId ||
|
|
620
|
+
switch newestBelow->Dict.get(key) {
|
|
621
|
+
| Some(kept) => kept === row
|
|
622
|
+
| None => false
|
|
623
|
+
}
|
|
624
|
+
}),
|
|
625
|
+
)
|
|
626
|
+
}
|
|
627
|
+
},
|
|
628
|
+
getRollbackTargetCheckpoint: async (~reorgChainId, ~lastKnownValidBlockNumber) =>
|
|
629
|
+
state.checkpoints->Array.reduce(None, (target, cp) =>
|
|
630
|
+
if cp.chainId == reorgChainId && cp.blockNumber <= lastKnownValidBlockNumber {
|
|
631
|
+
switch target {
|
|
632
|
+
| Some(current) if current >= cp.id => target
|
|
633
|
+
| _ => Some(cp.id)
|
|
634
|
+
}
|
|
635
|
+
} else {
|
|
636
|
+
target
|
|
637
|
+
}
|
|
638
|
+
),
|
|
639
|
+
getRollbackProgressDiff: async (~rollbackTargetCheckpointId) =>
|
|
640
|
+
state->getRollbackProgressDiff(~rollbackTargetCheckpointId),
|
|
641
|
+
getRollbackData: async (~entityConfig, ~rollbackTargetCheckpointId) =>
|
|
642
|
+
state->getRollbackData(~entityConfig, ~rollbackTargetCheckpointId),
|
|
643
|
+
writeBatch: async (
|
|
644
|
+
~batch,
|
|
645
|
+
~rollback,
|
|
646
|
+
~isInReorgThreshold,
|
|
647
|
+
~config as _,
|
|
648
|
+
~allEntities as _,
|
|
649
|
+
~updatedEffectsCache,
|
|
650
|
+
~updatedEntities,
|
|
651
|
+
~chainMetaData,
|
|
652
|
+
~onWrite as _,
|
|
653
|
+
) =>
|
|
654
|
+
state->writeBatch(
|
|
655
|
+
~batch,
|
|
656
|
+
~rollback,
|
|
657
|
+
~isInReorgThreshold,
|
|
658
|
+
~config,
|
|
659
|
+
~updatedEntities,
|
|
660
|
+
~updatedEffectsCache,
|
|
661
|
+
~chainMetaData,
|
|
662
|
+
),
|
|
663
|
+
close: async () => (),
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Read helpers for tests. They mirror what the Postgres-backed equivalents
|
|
667
|
+
// return, so one assertion can run against either backend.
|
|
668
|
+
let currentRows = (state: t, ~entityConfig: Internal.entityConfig): array<Internal.entity> =>
|
|
669
|
+
state.entities->Dict.get(entityConfig.name)->Option.getOr(Dict.make())->Dict.valuesToArray
|
|
670
|
+
|
|
671
|
+
let historyChanges = (state: t, ~entityConfig: Internal.entityConfig): array<
|
|
672
|
+
Change.t<Internal.entity>,
|
|
673
|
+
> =>
|
|
674
|
+
state.history
|
|
675
|
+
->Dict.get(entityConfig.name)
|
|
676
|
+
->Option.getOr([])
|
|
677
|
+
->Array.map(row =>
|
|
678
|
+
switch row.action {
|
|
679
|
+
| SET =>
|
|
680
|
+
Change.Set({
|
|
681
|
+
entityId: row.entityId,
|
|
682
|
+
checkpointId: row.checkpointId,
|
|
683
|
+
entity: row.entity->Option.getOr(%raw(`{}`)),
|
|
684
|
+
})
|
|
685
|
+
| DELETE => Change.Delete({entityId: row.entityId, checkpointId: row.checkpointId})
|
|
686
|
+
}
|
|
687
|
+
)
|
|
688
|
+
->Array.toSorted((a, b) =>
|
|
689
|
+
switch String.compare(
|
|
690
|
+
a->Change.getEntityId->EntityId.toKey,
|
|
691
|
+
b->Change.getEntityId->EntityId.toKey,
|
|
692
|
+
) {
|
|
693
|
+
| 0. =>
|
|
694
|
+
Float.compare(
|
|
695
|
+
a->Change.getCheckpointId->BigInt.toFloat,
|
|
696
|
+
b->Change.getCheckpointId->BigInt.toFloat,
|
|
697
|
+
)
|
|
698
|
+
| order => order
|
|
699
|
+
}
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
let checkpointRows = (state: t): array<InternalTable.Checkpoints.t> =>
|
|
703
|
+
state.checkpoints->Array.map(cp => {
|
|
704
|
+
InternalTable.Checkpoints.id: cp.id,
|
|
705
|
+
chainId: cp.chainId,
|
|
706
|
+
blockNumber: cp.blockNumber,
|
|
707
|
+
blockHash: cp.blockHash->Null.fromOption,
|
|
708
|
+
eventsProcessed: cp.eventsProcessed,
|
|
709
|
+
})
|
|
710
|
+
|
|
711
|
+
let effectCacheRows = (state: t, ~tableName): array<{
|
|
712
|
+
"id": string,
|
|
713
|
+
"output": JSON.t,
|
|
714
|
+
}> =>
|
|
715
|
+
state.effectCache
|
|
716
|
+
->Dict.get(tableName)
|
|
717
|
+
->Option.getOr(Dict.make())
|
|
718
|
+
->Dict.valuesToArray
|
|
719
|
+
->Array.map(item =>
|
|
720
|
+
{
|
|
721
|
+
"id": item.id,
|
|
722
|
+
"output": item.output->(Utils.magic: Internal.effectOutput => JSON.t),
|
|
723
|
+
}
|
|
724
|
+
)
|