envio 3.3.2 → 3.5.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/Api.res +1 -8
- package/src/Api.res.mjs +1 -5
- package/src/Bin.res +2 -2
- package/src/ChainState.res +6 -1
- package/src/ChainState.res.mjs +4 -3
- package/src/Config.res +38 -5
- package/src/Config.res.mjs +35 -6
- package/src/Core.res +65 -20
- package/src/Core.res.mjs +36 -6
- package/src/EnvioGlobal.res +0 -2
- package/src/EnvioGlobal.res.mjs +0 -1
- package/src/ExitOnCaughtUp.res +6 -2
- package/src/ExitOnCaughtUp.res.mjs +4 -0
- package/src/FetchState.res +3 -3
- package/src/HandlerRegister.res +357 -434
- package/src/HandlerRegister.res.mjs +202 -242
- package/src/HandlerRegister.resi +7 -15
- package/src/IndexerState.res +10 -0
- package/src/IndexerState.res.mjs +9 -3
- package/src/IndexerState.resi +3 -0
- package/src/Internal.res +10 -1
- package/src/Main.res.mjs +4 -4
- package/src/PgStorage.res +16 -1
- package/src/PgStorage.res.mjs +9 -1
- package/src/SimulateItems.res +61 -40
- package/src/SimulateItems.res.mjs +37 -21
- package/src/TestIndexer.res +453 -454
- package/src/TestIndexer.res.mjs +321 -344
- package/src/bindings/ClickHouse.res +68 -2
- package/src/bindings/ClickHouse.res.mjs +47 -2
- package/src/sources/EvmChain.res +1 -1
- package/src/sources/EvmChain.res.mjs +2 -2
- package/src/sources/FuelHyperSync.res +74 -0
- package/src/sources/FuelHyperSync.res.mjs +80 -0
- package/src/sources/FuelHyperSync.resi +22 -0
- package/src/sources/FuelHyperSyncClient.res +120 -0
- package/src/sources/FuelHyperSyncClient.res.mjs +71 -0
- package/src/sources/FuelHyperSyncSource.res +257 -0
- package/src/sources/FuelHyperSyncSource.res.mjs +252 -0
- package/src/sources/HyperSyncClient.res +2 -2
- package/src/sources/HyperSyncClient.res.mjs +1 -1
- package/src/sources/SvmHyperSyncClient.res +139 -102
- package/src/sources/SvmHyperSyncClient.res.mjs +45 -5
- package/src/sources/SvmHyperSyncSource.res +60 -440
- package/src/sources/SvmHyperSyncSource.res.mjs +42 -363
- package/src/TestIndexerProxyStorage.res +0 -196
- package/src/TestIndexerProxyStorage.res.mjs +0 -121
- package/src/TestIndexerWorker.res +0 -4
- package/src/TestIndexerWorker.res.mjs +0 -7
- package/src/sources/EventRouter.res +0 -165
- package/src/sources/EventRouter.res.mjs +0 -153
- package/src/sources/HyperFuel.res +0 -179
- package/src/sources/HyperFuel.res.mjs +0 -146
- package/src/sources/HyperFuel.resi +0 -36
- package/src/sources/HyperFuelClient.res +0 -127
- package/src/sources/HyperFuelClient.res.mjs +0 -20
- package/src/sources/HyperFuelSource.res +0 -502
- package/src/sources/HyperFuelSource.res.mjs +0 -481
- /package/src/sources/{HyperSyncSource.res → EvmHyperSyncSource.res} +0 -0
- /package/src/sources/{HyperSyncSource.res.mjs → EvmHyperSyncSource.res.mjs} +0 -0
package/src/TestIndexer.res
CHANGED
|
@@ -70,48 +70,31 @@ let getIndexingAddressesByChain = (state: testIndexerState): dict<
|
|
|
70
70
|
byChain
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
let handleLoad = (state: testIndexerState, ~tableName: string, ~filter: EntityFilter.t):
|
|
73
|
+
let handleLoad = (state: testIndexerState, ~tableName: string, ~filter: EntityFilter.t): array<
|
|
74
|
+
Internal.entity,
|
|
75
|
+
> => {
|
|
74
76
|
// Loads for non-entity tables (e.g. effect caches `envio_effect_<name>`) reach
|
|
75
77
|
// here too. TestIndexer never persists those, so there's nothing to return —
|
|
76
78
|
// an empty result makes the effect recompute instead of crashing on a missing
|
|
77
79
|
// entityConfig.
|
|
78
80
|
switch state.entityConfigs->Dict.get(tableName) {
|
|
79
|
-
| None => []
|
|
80
|
-
| Some(
|
|
81
|
+
| None => []
|
|
82
|
+
| Some(_) =>
|
|
81
83
|
let entityDict = state.entities->Dict.get(tableName)->Option.getOr(Dict.make())
|
|
82
|
-
let results = []
|
|
83
|
-
|
|
84
|
-
// Field values arrive as JSON from the worker boundary, so parse them
|
|
85
|
-
// with the field's schema before comparing. This properly handles
|
|
86
|
-
// bigint and BigDecimal comparisons
|
|
87
|
-
let parseLeaf = (~fieldName, ~fieldValue: unknown, ~isArray): unknown => {
|
|
88
|
-
let queryField = switch entityConfig.table->Table.queryFields->Dict.get(fieldName) {
|
|
89
|
-
| Some(queryField) => queryField
|
|
90
|
-
| None => JsError.throwWithMessage(`Field ${fieldName} not found in entity ${tableName}`)
|
|
91
|
-
}
|
|
92
|
-
fieldValue->S.convertOrThrow(isArray ? queryField.arrayFieldSchema : queryField.fieldSchema)
|
|
93
|
-
}
|
|
94
|
-
let filter = filter->EntityFilter.mapValues(~mapValue=parseLeaf)
|
|
95
|
-
|
|
96
84
|
entityDict
|
|
97
85
|
->Dict.valuesToArray
|
|
98
|
-
->Array.
|
|
99
|
-
//
|
|
86
|
+
->Array.filter(entity => {
|
|
87
|
+
// The store holds decoded entities and the filter carries decoded values,
|
|
88
|
+
// so compare directly (same approach as InMemoryTable) — no JSON round-trip.
|
|
100
89
|
let entityAsDict = entity->(Utils.magic: Internal.entity => dict<EntityFilter.FieldValue.t>)
|
|
101
|
-
|
|
102
|
-
// Serialize entity back to JSON for worker thread
|
|
103
|
-
let jsonEntity = entity->S.reverseConvertToJsonOrThrow(entityConfig.schema)
|
|
104
|
-
results->Array.push(jsonEntity)->ignore
|
|
105
|
-
}
|
|
90
|
+
filter->EntityFilter.matches(~entity=entityAsDict)
|
|
106
91
|
})
|
|
107
|
-
|
|
108
|
-
results->JSON.Encode.array
|
|
109
92
|
}
|
|
110
93
|
}
|
|
111
94
|
|
|
112
95
|
let handleWriteBatch = (
|
|
113
96
|
state: testIndexerState,
|
|
114
|
-
~updatedEntities: array<
|
|
97
|
+
~updatedEntities: array<Persistence.updatedEntity>,
|
|
115
98
|
~checkpointIds: array<bigint>,
|
|
116
99
|
~checkpointChainIds: array<int>,
|
|
117
100
|
~checkpointBlockNumbers: array<int>,
|
|
@@ -121,7 +104,8 @@ let handleWriteBatch = (
|
|
|
121
104
|
// checkpointId -> entityName -> entityChange
|
|
122
105
|
let changesByCheckpoint: dict<dict<entityChange>> = Dict.make()
|
|
123
106
|
|
|
124
|
-
updatedEntities->Array.forEach(({
|
|
107
|
+
updatedEntities->Array.forEach(({entityConfig, changes}: Persistence.updatedEntity) => {
|
|
108
|
+
let entityName = entityConfig.name
|
|
125
109
|
let entityDict = switch state.entities->Dict.get(entityName) {
|
|
126
110
|
| Some(dict) => dict
|
|
127
111
|
| None =>
|
|
@@ -129,57 +113,35 @@ let handleWriteBatch = (
|
|
|
129
113
|
state.entities->Dict.set(entityName, dict)
|
|
130
114
|
dict
|
|
131
115
|
}
|
|
132
|
-
let entityConfig = state.entityConfigs->Dict.getUnsafe(entityName)
|
|
133
116
|
|
|
134
|
-
let
|
|
117
|
+
let entityChangeFor = checkpointId => {
|
|
118
|
+
let checkpointKey = checkpointId->BigInt.toString
|
|
119
|
+
let entityChanges = switch changesByCheckpoint->Dict.get(checkpointKey) {
|
|
120
|
+
| Some(changes) => changes
|
|
121
|
+
| None =>
|
|
122
|
+
let changes = Dict.make()
|
|
123
|
+
changesByCheckpoint->Dict.set(checkpointKey, changes)
|
|
124
|
+
changes
|
|
125
|
+
}
|
|
126
|
+
switch entityChanges->Dict.get(entityName) {
|
|
127
|
+
| Some(change) => change
|
|
128
|
+
| None =>
|
|
129
|
+
let change = {sets: [], deleted: []}
|
|
130
|
+
entityChanges->Dict.set(entityName, change)
|
|
131
|
+
change
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let processChange = (change: Change.t<Internal.entity>) => {
|
|
135
136
|
switch change {
|
|
136
137
|
| Set({entityId, entity, checkpointId}) =>
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// Update entities dict with parsed entity for load operations
|
|
142
|
-
entityDict->Dict.set(entityId, parsedEntity)
|
|
143
|
-
|
|
144
|
-
// Track change by checkpoint
|
|
145
|
-
let checkpointKey = checkpointId->BigInt.toString
|
|
146
|
-
let entityChanges = switch changesByCheckpoint->Dict.get(checkpointKey) {
|
|
147
|
-
| Some(changes) => changes
|
|
148
|
-
| None =>
|
|
149
|
-
let changes = Dict.make()
|
|
150
|
-
changesByCheckpoint->Dict.set(checkpointKey, changes)
|
|
151
|
-
changes
|
|
152
|
-
}
|
|
153
|
-
let entityChange = switch entityChanges->Dict.get(entityName) {
|
|
154
|
-
| Some(change) => change
|
|
155
|
-
| None =>
|
|
156
|
-
let change = {sets: [], deleted: []}
|
|
157
|
-
entityChanges->Dict.set(entityName, change)
|
|
158
|
-
change
|
|
159
|
-
}
|
|
160
|
-
entityChange.sets->Array.push(parsedEntity->Utils.magic)->ignore
|
|
161
|
-
|
|
138
|
+
// The store keeps decoded entities so load comparisons (bigint /
|
|
139
|
+
// BigDecimal) work on real values.
|
|
140
|
+
entityDict->Dict.set(entityId, entity)
|
|
141
|
+
entityChangeFor(checkpointId).sets->Array.push(entity->Utils.magic)->ignore
|
|
162
142
|
| Delete({entityId, checkpointId}) =>
|
|
163
|
-
// Update entities dict for load operations
|
|
164
143
|
Dict.delete(entityDict->Obj.magic, entityId)
|
|
165
|
-
|
|
166
|
-
// Track change by checkpoint
|
|
167
|
-
let checkpointKey = checkpointId->BigInt.toString
|
|
168
|
-
let entityChanges = switch changesByCheckpoint->Dict.get(checkpointKey) {
|
|
169
|
-
| Some(changes) => changes
|
|
170
|
-
| None =>
|
|
171
|
-
let changes = Dict.make()
|
|
172
|
-
changesByCheckpoint->Dict.set(checkpointKey, changes)
|
|
173
|
-
changes
|
|
174
|
-
}
|
|
175
|
-
let entityChange = switch entityChanges->Dict.get(entityName) {
|
|
176
|
-
| Some(change) => change
|
|
177
|
-
| None =>
|
|
178
|
-
let change = {sets: [], deleted: []}
|
|
179
|
-
entityChanges->Dict.set(entityName, change)
|
|
180
|
-
change
|
|
181
|
-
}
|
|
182
|
-
entityChange.deleted->Array.push(entityId)->ignore
|
|
144
|
+
entityChangeFor(checkpointId).deleted->Array.push(entityId)->ignore
|
|
183
145
|
}
|
|
184
146
|
}
|
|
185
147
|
|
|
@@ -237,7 +199,11 @@ let handleWriteBatch = (
|
|
|
237
199
|
if deleted->Array.length > 0 {
|
|
238
200
|
entityObj->Dict.set("deleted", deleted->(Utils.magic: array<string> => unknown))
|
|
239
201
|
}
|
|
240
|
-
|
|
202
|
+
// Match the capitalized entity accessor the generated change types expose.
|
|
203
|
+
change->Dict.set(
|
|
204
|
+
entityName->Utils.String.capitalize,
|
|
205
|
+
entityObj->(Utils.magic: dict<unknown> => unknown),
|
|
206
|
+
)
|
|
241
207
|
}
|
|
242
208
|
})
|
|
243
209
|
| None => ()
|
|
@@ -285,8 +251,6 @@ let makeInitialState = (
|
|
|
285
251
|
chains,
|
|
286
252
|
checkpointId: InternalTable.Checkpoints.initialCheckpointId,
|
|
287
253
|
reorgCheckpoints: [],
|
|
288
|
-
// TestIndexer fakes the resume path; mirror what Main.start passes as
|
|
289
|
-
// ~envioInfo so the compat check always sees an empty diff.
|
|
290
254
|
envioInfo: Some(Config.getPublicConfigJson()->Config.stripSensitiveData),
|
|
291
255
|
}
|
|
292
256
|
}
|
|
@@ -406,6 +370,18 @@ let parseBlockRange = (
|
|
|
406
370
|
{startBlock, endBlock}
|
|
407
371
|
}
|
|
408
372
|
|
|
373
|
+
// The store owns its entities. Copy on the boundary with user code — both when
|
|
374
|
+
// handing one out (get/getAll/getOrThrow) and when taking one in (set) — so a
|
|
375
|
+
// user mutating a returned entity, or an object they passed to `set`, can't
|
|
376
|
+
// corrupt the in-memory store. The copy is shallow (matching InMemoryTable):
|
|
377
|
+
// scalar fields (string/bigint/BigDecimal) are immutable, but array-valued
|
|
378
|
+
// fields still share the backing array, so in-place mutation of those leaks.
|
|
379
|
+
let copyEntity = (entity: Internal.entity): Internal.entity =>
|
|
380
|
+
entity
|
|
381
|
+
->(Utils.magic: Internal.entity => dict<unknown>)
|
|
382
|
+
->Utils.Dict.shallowCopy
|
|
383
|
+
->(Utils.magic: dict<unknown> => Internal.entity)
|
|
384
|
+
|
|
409
385
|
// Entity operations for direct manipulation outside of handlers
|
|
410
386
|
let getEntityFromState = (
|
|
411
387
|
~state: testIndexerState,
|
|
@@ -419,7 +395,7 @@ let getEntityFromState = (
|
|
|
419
395
|
)
|
|
420
396
|
}
|
|
421
397
|
let entityDict = state.entities->Dict.get(entityConfig.name)->Option.getOr(Dict.make())
|
|
422
|
-
entityDict->Dict.get(entityId)
|
|
398
|
+
entityDict->Dict.get(entityId)->Option.map(copyEntity)
|
|
423
399
|
}
|
|
424
400
|
|
|
425
401
|
let makeEntityGet = (~state: testIndexerState, ~entityConfig: Internal.entityConfig): (
|
|
@@ -462,7 +438,7 @@ let makeEntitySet = (~state: testIndexerState, ~entityConfig: Internal.entityCon
|
|
|
462
438
|
state.entities->Dict.set(entityConfig.name, dict)
|
|
463
439
|
dict
|
|
464
440
|
}
|
|
465
|
-
entityDict->Dict.set(entity.id, entity)
|
|
441
|
+
entityDict->Dict.set(entity.id, copyEntity(entity))
|
|
466
442
|
}
|
|
467
443
|
}
|
|
468
444
|
|
|
@@ -476,7 +452,7 @@ let makeEntityGetAll = (~state: testIndexerState, ~entityConfig: Internal.entity
|
|
|
476
452
|
)
|
|
477
453
|
}
|
|
478
454
|
let entityDict = state.entities->Dict.get(entityConfig.name)->Option.getOr(Dict.make())
|
|
479
|
-
Promise.resolve(entityDict->Dict.valuesToArray)
|
|
455
|
+
Promise.resolve(entityDict->Dict.valuesToArray->Array.map(copyEntity))
|
|
480
456
|
}
|
|
481
457
|
}
|
|
482
458
|
|
|
@@ -487,411 +463,434 @@ type entityOperations = {
|
|
|
487
463
|
set: Internal.entity => unit,
|
|
488
464
|
}
|
|
489
465
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
466
|
+
// Adapt the real storage interface to the in-memory entity store. In-process
|
|
467
|
+
// there's no worker boundary, so entities are stored and loaded decoded — no
|
|
468
|
+
// JSON serialization round-trip.
|
|
469
|
+
let makeInMemoryStorage = (~state: testIndexerState): Persistence.storage => {
|
|
470
|
+
name: "test-inmemory",
|
|
471
|
+
isInitialized: async () => true,
|
|
472
|
+
// The runner injects the config-derived initial state by setting
|
|
473
|
+
// `persistence.storageStatus = Ready(...)` directly, bypassing `Persistence.init`,
|
|
474
|
+
// so neither of these is reached.
|
|
475
|
+
initialize: async (~chainConfigs as _=?, ~entities as _=?, ~enums as _=?, ~envioInfo as _) =>
|
|
476
|
+
JsError.throwWithMessage(
|
|
477
|
+
"TestIndexer: initialize should not be called; the initial state is derived from config.",
|
|
478
|
+
),
|
|
479
|
+
resumeInitialState: async () =>
|
|
480
|
+
JsError.throwWithMessage(
|
|
481
|
+
"TestIndexer: resumeInitialState should not be called; the initial state is derived from config.",
|
|
482
|
+
),
|
|
483
|
+
loadOrThrow: async (~filter, ~table: Table.table) =>
|
|
484
|
+
state
|
|
485
|
+
->handleLoad(~tableName=table.tableName, ~filter)
|
|
486
|
+
->(Utils.magic: array<Internal.entity> => array<unknown>),
|
|
487
|
+
writeBatch: async (
|
|
488
|
+
~batch,
|
|
489
|
+
~rollback as _,
|
|
490
|
+
~isInReorgThreshold as _,
|
|
491
|
+
~config as _,
|
|
492
|
+
~allEntities as _,
|
|
493
|
+
~updatedEffectsCache as _,
|
|
494
|
+
~updatedEntities,
|
|
495
|
+
~chainMetaData as _,
|
|
496
|
+
~onWrite as _,
|
|
497
|
+
) =>
|
|
498
|
+
state->handleWriteBatch(
|
|
499
|
+
~updatedEntities,
|
|
500
|
+
~checkpointIds=batch.checkpointIds,
|
|
501
|
+
~checkpointChainIds=batch.checkpointChainIds,
|
|
502
|
+
~checkpointBlockNumbers=batch.checkpointBlockNumbers,
|
|
503
|
+
~checkpointEventsProcessed=batch.checkpointEventsProcessed,
|
|
504
|
+
),
|
|
505
|
+
dumpEffectCache: async () => (),
|
|
506
|
+
reset: async () => (),
|
|
507
|
+
setChainMeta: async _ => Obj.magic(),
|
|
508
|
+
pruneStaleCheckpoints: async (~safeCheckpointId as _) => (),
|
|
509
|
+
pruneStaleEntityHistory: async (~entityName as _, ~entityIndex as _, ~safeCheckpointId as _) =>
|
|
510
|
+
(),
|
|
511
|
+
getRollbackTargetCheckpoint: async (~reorgChainId as _, ~lastKnownValidBlockNumber as _) =>
|
|
512
|
+
JsError.throwWithMessage(
|
|
513
|
+
"TestIndexer: Rollback is not supported. The runner forces rollbackOnReorg off, so this should be unreachable.",
|
|
514
|
+
),
|
|
515
|
+
getRollbackProgressDiff: async (~rollbackTargetCheckpointId as _) =>
|
|
516
|
+
JsError.throwWithMessage(
|
|
517
|
+
"TestIndexer: Rollback is not supported. The runner forces rollbackOnReorg off, so this should be unreachable.",
|
|
518
|
+
),
|
|
519
|
+
getRollbackData: async (~entityConfig as _, ~rollbackTargetCheckpointId as _) =>
|
|
520
|
+
JsError.throwWithMessage(
|
|
521
|
+
"TestIndexer: Rollback is not supported. The runner forces rollbackOnReorg off, so this should be unreachable.",
|
|
522
|
+
),
|
|
523
|
+
close: async () => (),
|
|
496
524
|
}
|
|
497
525
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
526
|
+
// Copy the per-chain registration arrays so a process() run's simulate-source
|
|
527
|
+
// additions (SimulateItems.patchConfig pushes onEventRegistrations) never
|
|
528
|
+
// mutate the shared base registration — lets independent createTestIndexer runs
|
|
529
|
+
// proceed in parallel without clobbering each other's registrations.
|
|
530
|
+
let cloneRegistrations = (
|
|
531
|
+
base: HandlerRegister.registrationsByChainId,
|
|
532
|
+
): HandlerRegister.registrationsByChainId => {
|
|
533
|
+
let clone = Dict.make()
|
|
534
|
+
base
|
|
535
|
+
->Dict.toArray
|
|
536
|
+
->Array.forEach(((chainIdStr, chainRegistrations: HandlerRegister.chainRegistrations)) =>
|
|
537
|
+
clone->Dict.set(
|
|
538
|
+
chainIdStr,
|
|
539
|
+
{
|
|
540
|
+
HandlerRegister.onEventRegistrations: chainRegistrations.onEventRegistrations->Array.copy,
|
|
541
|
+
onBlockRegistrations: chainRegistrations.onBlockRegistrations->Array.copy,
|
|
542
|
+
},
|
|
543
|
+
)
|
|
544
|
+
)
|
|
545
|
+
clone
|
|
546
|
+
}
|
|
509
547
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
registrationLogIndex: -1,
|
|
524
|
-
}
|
|
525
|
-
envioAddressesDict->Dict.set(entity.id, entity->Config.EnvioAddresses.castToInternal)
|
|
526
|
-
},
|
|
527
|
-
)
|
|
528
|
-
})
|
|
529
|
-
})
|
|
548
|
+
// User handlers register into the process-global HandlerRegister as an import
|
|
549
|
+
// side effect. Capture the resolved registrations once per process (imports are
|
|
550
|
+
// module-cached anyway) and reuse them across every createTestIndexer run, so
|
|
551
|
+
// the global registration cycle runs a single time and never races.
|
|
552
|
+
let registrationsRef: ref<option<promise<HandlerRegister.registrationsByChainId>>> = ref(None)
|
|
553
|
+
let getRegistrations = (~config) =>
|
|
554
|
+
switch registrationsRef.contents {
|
|
555
|
+
| Some(promise) => promise
|
|
556
|
+
| None =>
|
|
557
|
+
let promise = HandlerLoader.registerAllHandlers(~config)
|
|
558
|
+
registrationsRef := Some(promise)
|
|
559
|
+
promise
|
|
560
|
+
}
|
|
530
561
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
562
|
+
let createTestIndexer = (): t<'processConfig> => {
|
|
563
|
+
let config = Config.load()
|
|
564
|
+
let allEntities = config.allEntities
|
|
565
|
+
let entities = Dict.make()
|
|
566
|
+
let entityConfigs = Dict.make()
|
|
567
|
+
allEntities->Array.forEach(entityConfig => {
|
|
568
|
+
entities->Dict.set(entityConfig.name, Dict.make())
|
|
569
|
+
entityConfigs->Dict.set(entityConfig.name, entityConfig)
|
|
570
|
+
})
|
|
538
571
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
572
|
+
// Populate config addresses into the entity dict, mirroring PgStorage.initialize
|
|
573
|
+
let envioAddressesDict = entities->Dict.getUnsafe(InternalTable.EnvioAddresses.name)
|
|
574
|
+
config.chainMap
|
|
575
|
+
->ChainMap.values
|
|
576
|
+
->Array.forEach(chainConfig => {
|
|
577
|
+
chainConfig.contracts->Array.forEach(contract => {
|
|
578
|
+
contract.addresses->Array.forEach(
|
|
579
|
+
address => {
|
|
580
|
+
let entity: InternalTable.EnvioAddresses.t = {
|
|
581
|
+
id: Config.EnvioAddresses.makeId(~chainId=chainConfig.id, ~address),
|
|
582
|
+
chainId: chainConfig.id,
|
|
583
|
+
contractName: contract.name,
|
|
584
|
+
registrationBlock: -1,
|
|
585
|
+
registrationLogIndex: -1,
|
|
586
|
+
}
|
|
587
|
+
envioAddressesDict->Dict.set(entity.id, entity->Config.EnvioAddresses.castToInternal)
|
|
588
|
+
},
|
|
589
|
+
)
|
|
554
590
|
})
|
|
591
|
+
})
|
|
555
592
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
chainIds->Array.push(chainConfig.id)->ignore
|
|
593
|
+
let state = {
|
|
594
|
+
processInProgress: false,
|
|
595
|
+
progressBlockByChain: Dict.make(),
|
|
596
|
+
entities,
|
|
597
|
+
entityConfigs,
|
|
598
|
+
processChanges: [],
|
|
599
|
+
}
|
|
564
600
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
601
|
+
// Per-instance in-memory storage over `state.entities`. Separate indexers
|
|
602
|
+
// get separate storages, so independent indexers run in parallel without
|
|
603
|
+
// shared mutable state.
|
|
604
|
+
let storage = makeInMemoryStorage(~state)
|
|
605
|
+
let persistence = Persistence.make(
|
|
606
|
+
~userEntities=config.userEntities,
|
|
607
|
+
~allEnums=config.allEnums,
|
|
608
|
+
~storage,
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
// Silence logs by default in test mode unless LOG_LEVEL is explicitly set.
|
|
612
|
+
switch Env.userLogLevel {
|
|
613
|
+
| None => Logging.setLogLevel(#silent)
|
|
614
|
+
| Some(_) => ()
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Build entity operations for each user entity
|
|
618
|
+
let entityOpsDict: dict<entityOperations> = Dict.make()
|
|
619
|
+
allEntities->Array.forEach(entityConfig => {
|
|
620
|
+
// Only create ops for user entities (not internal tables like envio_addresses)
|
|
621
|
+
if entityConfig.name !== InternalTable.EnvioAddresses.name {
|
|
622
|
+
entityOpsDict->Dict.set(
|
|
623
|
+
entityConfig.name,
|
|
624
|
+
{
|
|
625
|
+
get: makeEntityGet(~state, ~entityConfig),
|
|
626
|
+
getAll: makeEntityGetAll(~state, ~entityConfig),
|
|
627
|
+
getOrThrow: makeEntityGetOrThrow(~state, ~entityConfig),
|
|
628
|
+
set: makeEntitySet(~state, ~entityConfig),
|
|
629
|
+
},
|
|
571
630
|
)
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
631
|
+
}
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
// Build chain info from config (similar to Main.getGlobalIndexer but static)
|
|
635
|
+
let chainIds = []
|
|
636
|
+
let chains = Utils.Object.createNullObject()
|
|
637
|
+
config.chainMap
|
|
638
|
+
->ChainMap.values
|
|
639
|
+
->Array.forEach(chainConfig => {
|
|
640
|
+
let chainIdStr = chainConfig.id->Int.toString
|
|
641
|
+
chainIds->Array.push(chainConfig.id)->ignore
|
|
642
|
+
|
|
643
|
+
let chainObj = Utils.Object.createNullObject()
|
|
644
|
+
chainObj
|
|
645
|
+
->Utils.Object.definePropertyWithValue("id", {enumerable: true, value: chainConfig.id})
|
|
646
|
+
->Utils.Object.definePropertyWithValue(
|
|
647
|
+
"startBlock",
|
|
648
|
+
{enumerable: true, value: chainConfig.startBlock},
|
|
649
|
+
)
|
|
650
|
+
->Utils.Object.definePropertyWithValue(
|
|
651
|
+
"endBlock",
|
|
652
|
+
{enumerable: true, value: chainConfig.endBlock},
|
|
653
|
+
)
|
|
654
|
+
->Utils.Object.definePropertyWithValue("name", {enumerable: true, value: chainConfig.name})
|
|
655
|
+
->Utils.Object.definePropertyWithValue("isRealtime", {enumerable: true, value: false})
|
|
656
|
+
->ignore
|
|
657
|
+
|
|
658
|
+
// Add contracts to chain object
|
|
659
|
+
chainConfig.contracts->Array.forEach(contract => {
|
|
660
|
+
let contractObj = Utils.Object.createNullObject()
|
|
661
|
+
contractObj
|
|
662
|
+
->Utils.Object.definePropertyWithValue("name", {enumerable: true, value: contract.name})
|
|
663
|
+
->Utils.Object.definePropertyWithValue("abi", {enumerable: true, value: contract.abi})
|
|
664
|
+
->Utils.Object.defineProperty(
|
|
665
|
+
"addresses",
|
|
666
|
+
{
|
|
667
|
+
enumerable: true,
|
|
668
|
+
get: () => {
|
|
669
|
+
if state.processInProgress {
|
|
670
|
+
JsError.throwWithMessage(
|
|
671
|
+
`Cannot access ${contract.name}.addresses while indexer.process() is running. ` ++ "Wait for process() to complete before reading contract addresses.",
|
|
672
|
+
)
|
|
673
|
+
}
|
|
674
|
+
getIndexingAddressesByChain(state)
|
|
675
|
+
->Dict.get(chainConfig.id->Int.toString)
|
|
676
|
+
->Option.getOr([])
|
|
677
|
+
->Array.filterMap(ia => ia.contractName === contract.name ? Some(ia.address) : None)
|
|
678
|
+
},
|
|
679
|
+
},
|
|
575
680
|
)
|
|
576
|
-
->Utils.Object.definePropertyWithValue("name", {enumerable: true, value: chainConfig.name})
|
|
577
|
-
->Utils.Object.definePropertyWithValue("isRealtime", {enumerable: true, value: false})
|
|
578
681
|
->ignore
|
|
579
682
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
->Utils.Object.definePropertyWithValue("name", {enumerable: true, value: contract.name})
|
|
585
|
-
->Utils.Object.definePropertyWithValue("abi", {enumerable: true, value: contract.abi})
|
|
586
|
-
->Utils.Object.defineProperty(
|
|
587
|
-
"addresses",
|
|
588
|
-
{
|
|
589
|
-
enumerable: true,
|
|
590
|
-
get: () => {
|
|
591
|
-
if state.processInProgress {
|
|
592
|
-
JsError.throwWithMessage(
|
|
593
|
-
`Cannot access ${contract.name}.addresses while indexer.process() is running. ` ++ "Wait for process() to complete before reading contract addresses.",
|
|
594
|
-
)
|
|
595
|
-
}
|
|
596
|
-
getIndexingAddressesByChain(state)
|
|
597
|
-
->Dict.get(chainConfig.id->Int.toString)
|
|
598
|
-
->Option.getOr([])
|
|
599
|
-
->Array.filterMap(ia => ia.contractName === contract.name ? Some(ia.address) : None)
|
|
600
|
-
},
|
|
601
|
-
},
|
|
602
|
-
)
|
|
603
|
-
->ignore
|
|
604
|
-
|
|
605
|
-
chainObj
|
|
606
|
-
->Utils.Object.definePropertyWithValue(
|
|
607
|
-
contract.name,
|
|
608
|
-
{enumerable: true, value: contractObj},
|
|
609
|
-
)
|
|
610
|
-
->ignore
|
|
611
|
-
})
|
|
683
|
+
chainObj
|
|
684
|
+
->Utils.Object.definePropertyWithValue(contract.name, {enumerable: true, value: contractObj})
|
|
685
|
+
->ignore
|
|
686
|
+
})
|
|
612
687
|
|
|
688
|
+
chains
|
|
689
|
+
->Utils.Object.definePropertyWithValue(chainIdStr, {enumerable: true, value: chainObj})
|
|
690
|
+
->ignore
|
|
691
|
+
|
|
692
|
+
if chainConfig.name !== chainIdStr {
|
|
613
693
|
chains
|
|
614
|
-
->Utils.Object.definePropertyWithValue(
|
|
694
|
+
->Utils.Object.definePropertyWithValue(chainConfig.name, {enumerable: false, value: chainObj})
|
|
615
695
|
->ignore
|
|
696
|
+
}
|
|
697
|
+
})
|
|
616
698
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
699
|
+
// Build the result object with process + entity operations + chain info
|
|
700
|
+
let result: dict<unknown> = Dict.make()
|
|
701
|
+
result->Dict.set("chainIds", chainIds->(Utils.magic: array<int> => unknown))
|
|
702
|
+
result->Dict.set("chains", chains->(Utils.magic: {..} => unknown))
|
|
703
|
+
entityOpsDict
|
|
704
|
+
->Dict.toArray
|
|
705
|
+
->Array.forEach(((name, ops)) => {
|
|
706
|
+
// Expose the capitalized accessor (indexer.Pool_snapshots) the generated
|
|
707
|
+
// types declare, matching the handler-context keys.
|
|
708
|
+
result->Dict.set(name->Utils.String.capitalize, ops->(Utils.magic: entityOperations => unknown))
|
|
709
|
+
})
|
|
626
710
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
711
|
+
result->Dict.set(
|
|
712
|
+
"process",
|
|
713
|
+
(
|
|
714
|
+
processConfig => {
|
|
715
|
+
// Check if already processing
|
|
716
|
+
if state.processInProgress {
|
|
717
|
+
JsError.throwWithMessage(
|
|
718
|
+
"createTestIndexer process is already running. Only one process call is allowed at a time",
|
|
719
|
+
)
|
|
720
|
+
}
|
|
636
721
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
}
|
|
722
|
+
// Parse and validate processConfig
|
|
723
|
+
let parsedConfig = try processConfig->S.parseOrThrow(processConfigSchema) catch {
|
|
724
|
+
| S.Raised(exn) =>
|
|
725
|
+
JsError.throwWithMessage(
|
|
726
|
+
`Invalid processConfig: ${exn->Utils.prettifyExn->(Utils.magic: exn => string)}`,
|
|
727
|
+
)
|
|
728
|
+
}
|
|
729
|
+
let rawChains = parsedConfig["chains"]
|
|
730
|
+
let chainKeys = rawChains->Dict.keysToArray
|
|
647
731
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
732
|
+
if chainKeys->Array.length === 0 {
|
|
733
|
+
JsError.throwWithMessage("createTestIndexer requires at least one chain to be defined")
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Sort chain keys by chain ID for deterministic ordering
|
|
737
|
+
let sortedChainKeys = chainKeys->Array.copy
|
|
738
|
+
sortedChainKeys->Array.sort((a, b) => {
|
|
739
|
+
let aId = a->Int.fromString->Option.getOr(0)
|
|
740
|
+
let bId = b->Int.fromString->Option.getOr(0)
|
|
741
|
+
Int.compare(aId, bId)
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
// Parse and validate the block ranges upfront before running any chain.
|
|
745
|
+
let chainEntries = sortedChainKeys->Array.map(chainIdStr => {
|
|
746
|
+
let rawChainConfig = rawChains->Dict.getUnsafe(chainIdStr)
|
|
747
|
+
if chainIdStr->Int.fromString->Option.isNone {
|
|
651
748
|
JsError.throwWithMessage(
|
|
652
|
-
`Invalid
|
|
749
|
+
`Invalid chain ID "${chainIdStr}": expected a numeric chain ID`,
|
|
653
750
|
)
|
|
654
751
|
}
|
|
655
|
-
let
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
752
|
+
let processChainConfig = parseBlockRange(
|
|
753
|
+
~chainIdStr,
|
|
754
|
+
~config,
|
|
755
|
+
~rawChainConfig,
|
|
756
|
+
~progressBlock=state.progressBlockByChain->Dict.get(chainIdStr),
|
|
757
|
+
)
|
|
758
|
+
(chainIdStr, rawChainConfig, processChainConfig)
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
// Reset processChanges for this run
|
|
762
|
+
state.processChanges = []
|
|
763
|
+
|
|
764
|
+
let runChainInProcess = async ((
|
|
765
|
+
chainIdStr,
|
|
766
|
+
rawChainConfig: rawChainConfig,
|
|
767
|
+
processChainConfig,
|
|
768
|
+
)) => {
|
|
769
|
+
// Build initialState from resolved block range. Rebuilt per chain so
|
|
770
|
+
// later chains in the same process() call see contracts registered
|
|
771
|
+
// by earlier ones.
|
|
772
|
+
let chains: dict<chainConfig> = Dict.make()
|
|
773
|
+
chains->Dict.set(chainIdStr, processChainConfig)
|
|
774
|
+
let indexingAddressesByChain = getIndexingAddressesByChain(state)
|
|
775
|
+
let initialState = makeInitialState(
|
|
776
|
+
~config,
|
|
777
|
+
~processConfigChains=chains,
|
|
778
|
+
~indexingAddressesByChain,
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
// No endBlock means auto-exit mode: process one block checkpoint at a
|
|
782
|
+
// time and stop after the first block with events.
|
|
783
|
+
let exitAfterFirstEventBlock = processChainConfig.endBlock->Option.isNone
|
|
784
|
+
|
|
785
|
+
// Rebuild the processConfig JSON that SimulateItems.patchConfig reads
|
|
786
|
+
// to turn `simulate` items into a SimulateSource for the chain.
|
|
787
|
+
let resolvedChainDict: dict<unknown> = Dict.make()
|
|
788
|
+
resolvedChainDict->Dict.set(
|
|
789
|
+
"startBlock",
|
|
790
|
+
processChainConfig.startBlock->(Utils.magic: int => unknown),
|
|
791
|
+
)
|
|
792
|
+
switch processChainConfig.endBlock {
|
|
793
|
+
| Some(eb) => resolvedChainDict->Dict.set("endBlock", eb->(Utils.magic: int => unknown))
|
|
794
|
+
| None => ()
|
|
660
795
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
})
|
|
669
|
-
|
|
670
|
-
// Parse and validate the block ranges upfront before starting any workers.
|
|
671
|
-
let chainEntries = sortedChainKeys->Array.map(chainIdStr => {
|
|
672
|
-
let rawChainConfig = rawChains->Dict.getUnsafe(chainIdStr)
|
|
673
|
-
let chainId = switch chainIdStr->Int.fromString {
|
|
674
|
-
| Some(id) => id
|
|
675
|
-
| None =>
|
|
676
|
-
JsError.throwWithMessage(
|
|
677
|
-
`Invalid chain ID "${chainIdStr}": expected a numeric chain ID`,
|
|
678
|
-
)
|
|
679
|
-
}
|
|
680
|
-
let processChainConfig = parseBlockRange(
|
|
681
|
-
~chainIdStr,
|
|
682
|
-
~config,
|
|
683
|
-
~rawChainConfig,
|
|
684
|
-
~progressBlock=state.progressBlockByChain->Dict.get(chainIdStr),
|
|
685
|
-
)
|
|
686
|
-
(chainIdStr, chainId, rawChainConfig, processChainConfig)
|
|
687
|
-
})
|
|
688
|
-
|
|
689
|
-
// Reset processChanges for this run
|
|
690
|
-
state.processChanges = []
|
|
691
|
-
|
|
692
|
-
let runChainWorker = ((
|
|
796
|
+
switch rawChainConfig.simulate {
|
|
797
|
+
| Some(s) =>
|
|
798
|
+
resolvedChainDict->Dict.set("simulate", s->(Utils.magic: array<JSON.t> => unknown))
|
|
799
|
+
| None => ()
|
|
800
|
+
}
|
|
801
|
+
let resolvedChainsDict: dict<unknown> = Dict.make()
|
|
802
|
+
resolvedChainsDict->Dict.set(
|
|
693
803
|
chainIdStr,
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
)
|
|
731
|
-
} catch {
|
|
732
|
-
| exn =>
|
|
733
|
-
reject(exn->Utils.magic)
|
|
734
|
-
throw(exn)
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// Handle messages from worker
|
|
738
|
-
worker->NodeJs.WorkerThreads.onMessage((
|
|
739
|
-
msg: TestIndexerProxyStorage.workerMessage,
|
|
740
|
-
) => {
|
|
741
|
-
let respond = data =>
|
|
742
|
-
worker->NodeJs.WorkerThreads.workerPostMessage(
|
|
743
|
-
{
|
|
744
|
-
TestIndexerProxyStorage.id: msg.id,
|
|
745
|
-
payload: TestIndexerProxyStorage.Response({data: data}),
|
|
746
|
-
}->Utils.magic,
|
|
747
|
-
)
|
|
748
|
-
|
|
749
|
-
switch msg.payload {
|
|
750
|
-
| Load({tableName, filter}) => state->handleLoad(~tableName, ~filter)->respond
|
|
751
|
-
|
|
752
|
-
| WriteBatch({
|
|
753
|
-
updatedEntities,
|
|
754
|
-
checkpointIds,
|
|
755
|
-
checkpointChainIds,
|
|
756
|
-
checkpointBlockNumbers,
|
|
757
|
-
checkpointBlockHashes: _,
|
|
758
|
-
checkpointEventsProcessed,
|
|
759
|
-
}) =>
|
|
760
|
-
state->handleWriteBatch(
|
|
761
|
-
~updatedEntities,
|
|
762
|
-
~checkpointIds,
|
|
763
|
-
~checkpointChainIds,
|
|
764
|
-
~checkpointBlockNumbers,
|
|
765
|
-
~checkpointEventsProcessed,
|
|
766
|
-
)
|
|
767
|
-
JSON.Encode.null->respond
|
|
768
|
-
}
|
|
769
|
-
})
|
|
770
|
-
|
|
771
|
-
worker->NodeJs.WorkerThreads.onError(err => {
|
|
772
|
-
worker->NodeJs.WorkerThreads.terminate->ignore
|
|
773
|
-
reject(err)
|
|
774
|
-
})
|
|
775
|
-
|
|
776
|
-
worker->NodeJs.WorkerThreads.onExit(code => {
|
|
777
|
-
if code !== 0 {
|
|
778
|
-
reject(Utils.Error.make(`Worker exited with code ${code->Int.toString}`))
|
|
779
|
-
} else {
|
|
780
|
-
resolve()
|
|
804
|
+
resolvedChainDict->(Utils.magic: dict<unknown> => unknown),
|
|
805
|
+
)
|
|
806
|
+
let processConfigJson =
|
|
807
|
+
{"chains": resolvedChainsDict}->(Utils.magic: {"chains": dict<unknown>} => JSON.t)
|
|
808
|
+
|
|
809
|
+
// Each run gets its own copy of the shared base registration so the
|
|
810
|
+
// simulate-source registration it appends stays isolated.
|
|
811
|
+
let registrationsByChainId = cloneRegistrations(await getRegistrations(~config))
|
|
812
|
+
let patchedConfig: Config.t = SimulateItems.patchConfig(
|
|
813
|
+
~config,
|
|
814
|
+
~processConfig=processConfigJson,
|
|
815
|
+
~registrationsByChainId,
|
|
816
|
+
)
|
|
817
|
+
let runConfig = {...patchedConfig, shouldRollbackOnReorg: false}
|
|
818
|
+
let runConfig = exitAfterFirstEventBlock ? {...runConfig, batchSize: 1} : runConfig
|
|
819
|
+
|
|
820
|
+
// Bypass Persistence.init: hand the loop the config-derived initial
|
|
821
|
+
// state directly (never a real DB) and mark the storage Ready so
|
|
822
|
+
// writes go through.
|
|
823
|
+
persistence.storageStatus = Ready(initialState)
|
|
824
|
+
|
|
825
|
+
let indexerStateRef = ref(None)
|
|
826
|
+
// Stop the loop and let any in-flight processing/write settle, so a
|
|
827
|
+
// finished run leaves nothing driving the shared state into the next.
|
|
828
|
+
let cleanup = async () => {
|
|
829
|
+
switch indexerStateRef.contents {
|
|
830
|
+
| Some(indexerState) =>
|
|
831
|
+
indexerState->IndexerState.stop
|
|
832
|
+
while (
|
|
833
|
+
indexerState->IndexerState.isProcessing ||
|
|
834
|
+
indexerState->IndexerState.writeFiber->Option.isSome
|
|
835
|
+
) {
|
|
836
|
+
switch indexerState->IndexerState.writeFiber {
|
|
837
|
+
// Await the in-flight write directly; only fall back to a tick
|
|
838
|
+
// yield while processing hasn't yet spawned a write fiber.
|
|
839
|
+
| Some(fiber) => await fiber
|
|
840
|
+
| None => await Utils.delay(0)
|
|
781
841
|
}
|
|
782
|
-
}
|
|
783
|
-
|
|
842
|
+
}
|
|
843
|
+
| None => ()
|
|
844
|
+
}
|
|
784
845
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
846
|
+
try {
|
|
847
|
+
await Promise.make((resolve, reject) => {
|
|
848
|
+
let indexerState = IndexerState.makeFromDbState(
|
|
849
|
+
~config=runConfig,
|
|
850
|
+
~persistence,
|
|
851
|
+
~initialState,
|
|
852
|
+
~registrationsByChainId,
|
|
853
|
+
~exitAfterFirstEventBlock,
|
|
854
|
+
~onError=errHandler => {
|
|
855
|
+
errHandler->ErrorHandling.log
|
|
856
|
+
reject(errHandler.exn->Utils.prettifyExn)
|
|
857
|
+
},
|
|
858
|
+
// Caught up: resolve the run instead of exiting the process.
|
|
859
|
+
~onExit=() => resolve(),
|
|
797
860
|
)
|
|
798
|
-
|
|
861
|
+
indexerStateRef := Some(indexerState)
|
|
862
|
+
indexerState->IndexerLoop.start
|
|
863
|
+
})
|
|
864
|
+
await cleanup()
|
|
865
|
+
} catch {
|
|
866
|
+
| exn =>
|
|
867
|
+
await cleanup()
|
|
868
|
+
throw(exn)
|
|
799
869
|
}
|
|
800
|
-
|
|
801
|
-
runChains(0)->Promise.catch(err => {
|
|
802
|
-
state.processInProgress = false
|
|
803
|
-
Promise.reject(err->Utils.prettifyExn)
|
|
804
|
-
})
|
|
805
870
|
}
|
|
806
|
-
)->(Utils.magic: ('a => promise<processResult>) => unknown),
|
|
807
|
-
)
|
|
808
|
-
|
|
809
|
-
result->(Utils.magic: dict<unknown> => t<'processConfig>)
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
let initTestWorker = () => {
|
|
814
|
-
if NodeJs.WorkerThreads.isMainThread {
|
|
815
|
-
JsError.throwWithMessage("initTestWorker must be called from a worker thread")
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
let parentPort = switch NodeJs.WorkerThreads.parentPort->Nullable.toOption {
|
|
819
|
-
| Some(port) => port
|
|
820
|
-
| None => JsError.throwWithMessage("initTestWorker: No parent port available")
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
let workerData: option<workerData> = NodeJs.WorkerThreads.workerData->Nullable.toOption
|
|
824
|
-
switch workerData {
|
|
825
|
-
| Some({chainId, startBlock, endBlock, simulate, initialState}) =>
|
|
826
|
-
let chainIdStr = chainId->Int.toString
|
|
827
|
-
|
|
828
|
-
// auto-exit mode: no endBlock means fetch first block with events and exit
|
|
829
|
-
let exitAfterFirstEventBlock = endBlock->Option.isNone
|
|
830
|
-
|
|
831
|
-
// Build processConfig JSON for SimulateItems.patchConfig
|
|
832
|
-
let resolvedChainDict: dict<unknown> = Dict.make()
|
|
833
|
-
resolvedChainDict->Dict.set("startBlock", startBlock->(Utils.magic: int => unknown))
|
|
834
|
-
switch endBlock {
|
|
835
|
-
| Some(eb) => resolvedChainDict->Dict.set("endBlock", eb->(Utils.magic: int => unknown))
|
|
836
|
-
| None => ()
|
|
837
|
-
}
|
|
838
|
-
switch simulate {
|
|
839
|
-
| Some(s) => resolvedChainDict->Dict.set("simulate", s->(Utils.magic: array<JSON.t> => unknown))
|
|
840
|
-
| None => ()
|
|
841
|
-
}
|
|
842
|
-
let resolvedChainsDict: dict<unknown> = Dict.make()
|
|
843
|
-
resolvedChainsDict->Dict.set(
|
|
844
|
-
chainIdStr,
|
|
845
|
-
resolvedChainDict->(Utils.magic: dict<unknown> => unknown),
|
|
846
|
-
)
|
|
847
|
-
let processConfig =
|
|
848
|
-
{"chains": resolvedChainsDict}->(Utils.magic: {"chains": dict<unknown>} => JSON.t)
|
|
849
|
-
|
|
850
|
-
// Create proxy storage that communicates with main thread
|
|
851
|
-
let proxy = TestIndexerProxyStorage.make(~parentPort, ~initialState)
|
|
852
|
-
let storage = TestIndexerProxyStorage.makeStorage(proxy)
|
|
853
|
-
let config = Config.load()
|
|
854
|
-
let persistence = Persistence.make(
|
|
855
|
-
~userEntities=config.userEntities,
|
|
856
|
-
~allEnums=config.allEnums,
|
|
857
|
-
~storage,
|
|
858
|
-
)
|
|
859
871
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
| None => Logging.setLogLevel(#silent)
|
|
863
|
-
| Some(_) => ()
|
|
864
|
-
}
|
|
872
|
+
// Set flag before starting the run
|
|
873
|
+
state.processInProgress = true
|
|
865
874
|
|
|
866
|
-
|
|
867
|
-
|
|
875
|
+
// Run chains sequentially, one at a time
|
|
876
|
+
let rec runChains = idx => {
|
|
877
|
+
if idx >= chainEntries->Array.length {
|
|
878
|
+
state.processInProgress = false
|
|
879
|
+
Promise.resolve({changes: state.processChanges})
|
|
880
|
+
} else {
|
|
881
|
+
runChainInProcess(chainEntries->Array.getUnsafe(idx))->Promise.then(_ =>
|
|
882
|
+
runChains(idx + 1)
|
|
883
|
+
)
|
|
884
|
+
}
|
|
885
|
+
}
|
|
868
886
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
config
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
Main.start(~persistence, ~isTest=true, ~patchConfig, ~exitAfterFirstEventBlock)
|
|
877
|
-
->Promise.catch(exn => {
|
|
878
|
-
// `Main.start` rejects on any fatal error: a runtime failure arrives wrapped
|
|
879
|
-
// in `Main.FatalError` (already logged), a setup throw (e.g. an invalid
|
|
880
|
-
// simulate item) arrives raw. The parent only learns of failures
|
|
881
|
-
// through the worker `error` event, which fires on an *uncaught* exception.
|
|
882
|
-
// Throwing synchronously in this catch would just reject the catch's own
|
|
883
|
-
// promise (swallowed by `ignore`); `setImmediate` re-throws outside the
|
|
884
|
-
// promise chain so it becomes uncaught and reaches the parent.
|
|
885
|
-
let toThrow = switch exn {
|
|
886
|
-
| Main.FatalError(inner) => inner
|
|
887
|
-
| _ => exn->Utils.prettifyExn
|
|
887
|
+
runChains(0)->Promise.catch(err => {
|
|
888
|
+
state.processInProgress = false
|
|
889
|
+
Promise.reject(err->Utils.prettifyExn)
|
|
890
|
+
})
|
|
888
891
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
| None =>
|
|
894
|
-
Logging.error("TestIndexerWorker: No worker data provided")
|
|
895
|
-
NodeJs.process->NodeJs.exitWithCode(Failure)
|
|
896
|
-
}
|
|
892
|
+
)->(Utils.magic: ('a => promise<processResult>) => unknown),
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
result->(Utils.magic: dict<unknown> => t<'processConfig>)
|
|
897
896
|
}
|