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.
- package/evm.schema.json +26 -0
- package/fuel.schema.json +26 -0
- package/index.d.ts +35 -3
- package/package.json +6 -7
- package/src/Batch.res +9 -0
- package/src/ChainState.res +35 -39
- package/src/ChainState.res.mjs +14 -11
- package/src/ChainState.resi +15 -7
- package/src/Config.res +52 -44
- package/src/Config.res.mjs +51 -17
- package/src/Core.res +4 -0
- package/src/CrossChainState.res +15 -23
- package/src/CrossChainState.res.mjs +10 -18
- package/src/CrossChainState.resi +4 -1
- package/src/Ecosystem.res +7 -2
- package/src/Envio.res +6 -4
- package/src/EventConfigBuilder.res +169 -26
- package/src/EventConfigBuilder.res.mjs +124 -15
- package/src/HandlerRegister.res +21 -4
- package/src/HandlerRegister.res.mjs +24 -5
- package/src/InMemoryStore.res +9 -7
- package/src/InMemoryStore.res.mjs +3 -3
- package/src/IndexerState.res +43 -6
- package/src/IndexerState.res.mjs +36 -7
- package/src/IndexerState.resi +4 -4
- package/src/Internal.res +16 -17
- package/src/Metrics.res +72 -6
- package/src/Metrics.res.mjs +30 -2
- package/src/Persistence.res +36 -30
- package/src/Persistence.res.mjs +1 -20
- package/src/PgStorage.res +104 -56
- package/src/PgStorage.res.mjs +57 -35
- package/src/PruneStaleHistory.res +85 -52
- package/src/PruneStaleHistory.res.mjs +63 -37
- package/src/Rollback.res +68 -45
- package/src/Rollback.res.mjs +38 -19
- package/src/RollbackCommit.res +7 -9
- package/src/RollbackCommit.res.mjs +5 -6
- package/src/SafeCheckpointTracking.res +11 -11
- package/src/SafeCheckpointTracking.res.mjs +7 -6
- package/src/SimulateItems.res +33 -18
- package/src/SimulateItems.res.mjs +26 -22
- package/src/Sink.res +38 -26
- package/src/Sink.res.mjs +21 -16
- package/src/TestIndexer.res +5 -5
- package/src/TestIndexer.res.mjs +1 -1
- package/src/Utils.res +170 -0
- package/src/Utils.res.mjs +153 -0
- package/src/Writing.res +2 -2
- package/src/Writing.res.mjs +1 -1
- package/src/bindings/ClickHouse.res +366 -698
- package/src/bindings/ClickHouse.res.mjs +371 -421
- package/src/bindings/ClickHouseSink.res +337 -0
- package/src/bindings/ClickHouseSink.res.mjs +246 -0
- package/src/bindings/EventSource.res +8 -2
- package/src/bindings/NodeJs.res +6 -0
- package/src/bindings/Vitest.res +23 -0
- package/src/bindings/Vitest.res.mjs +3 -0
- package/src/bindings/WebSocket.res +9 -10
- package/src/db/CheckpointBounds.res +53 -0
- package/src/db/CheckpointBounds.res.mjs +44 -0
- package/src/db/EntityFilter.res +36 -17
- package/src/db/EntityFilter.res.mjs +25 -14
- package/src/db/EntityHistory.res +47 -22
- package/src/db/EntityHistory.res.mjs +19 -12
- package/src/db/InternalTable.res +84 -35
- package/src/db/InternalTable.res.mjs +56 -23
- package/src/db/RollbackFloors.res +55 -0
- package/src/db/RollbackFloors.res.mjs +92 -0
- package/src/db/Table.res +22 -4
- package/src/db/Table.res.mjs +30 -9
- package/src/sources/Evm.res +2 -0
- package/src/sources/Evm.res.mjs +2 -0
- package/src/sources/EvmHyperSyncSource.res +3 -3
- package/src/sources/EvmHyperSyncSource.res.mjs +3 -3
- package/src/sources/EvmRpcWs.res +124 -0
- package/src/sources/EvmRpcWs.res.mjs +117 -0
- package/src/sources/Fuel.res +2 -0
- package/src/sources/Fuel.res.mjs +2 -0
- package/src/sources/FuelHyperSyncSource.res +1 -1
- package/src/sources/FuelHyperSyncSource.res.mjs +2 -1
- package/src/sources/HeightFeed.res +568 -0
- package/src/sources/HeightFeed.res.mjs +452 -0
- package/src/sources/HeightStream.res +257 -0
- package/src/sources/HeightStream.res.mjs +194 -0
- package/src/sources/HyperSync.res +5 -0
- package/src/sources/HyperSync.res.mjs +3 -0
- package/src/sources/HyperSync.resi +4 -0
- package/src/sources/HyperSyncSSE.res +49 -0
- package/src/sources/HyperSyncSSE.res.mjs +79 -0
- package/src/sources/RpcSource.res +1 -1
- package/src/sources/RpcSource.res.mjs +2 -2
- package/src/sources/Source.res +54 -1
- package/src/sources/Source.res.mjs +25 -0
- package/src/sources/SourceManager.res +220 -227
- package/src/sources/SourceManager.res.mjs +139 -164
- package/src/sources/SourceManager.resi +8 -0
- package/src/sources/Svm.res +2 -0
- package/src/sources/Svm.res.mjs +2 -0
- package/src/sources/SvmHyperSyncClient.res +70 -127
- package/src/sources/SvmHyperSyncClient.res.mjs +51 -25
- package/src/sources/SvmHyperSyncSource.res +18 -15
- package/src/sources/SvmHyperSyncSource.res.mjs +5 -4
- package/svm.schema.json +1 -75
- package/src/MemoryStorage.res +0 -729
- package/src/MemoryStorage.res.mjs +0 -586
- package/src/sources/HyperSyncHeightStream.res +0 -129
- package/src/sources/HyperSyncHeightStream.res.mjs +0 -110
- package/src/sources/RpcWebSocketHeightStream.res +0 -175
- package/src/sources/RpcWebSocketHeightStream.res.mjs +0 -180
package/src/MemoryStorage.res
DELETED
|
@@ -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
|
-
)
|