envio 3.0.0-alpha.2 → 3.0.0-alpha.20
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/README.md +164 -30
- package/bin.mjs +49 -0
- package/evm.schema.json +79 -169
- package/fuel.schema.json +50 -21
- package/index.d.ts +497 -1
- package/index.js +4 -0
- package/package.json +42 -31
- package/rescript.json +4 -1
- package/src/Batch.res +11 -8
- package/src/Batch.res.mjs +11 -9
- package/src/ChainFetcher.res +531 -0
- package/src/ChainFetcher.res.mjs +339 -0
- package/src/ChainManager.res +190 -0
- package/src/ChainManager.res.mjs +166 -0
- package/src/Change.res +3 -3
- package/src/Config.gen.ts +19 -0
- package/src/Config.res +737 -22
- package/src/Config.res.mjs +703 -26
- package/src/{Indexer.res → Ctx.res} +1 -1
- package/src/Ecosystem.res +9 -124
- package/src/Ecosystem.res.mjs +19 -160
- package/src/Env.res +30 -74
- package/src/Env.res.mjs +25 -87
- package/src/Envio.gen.ts +3 -1
- package/src/Envio.res +20 -9
- package/src/EventProcessing.res +469 -0
- package/src/EventProcessing.res.mjs +337 -0
- package/src/EvmTypes.gen.ts +6 -0
- package/src/EvmTypes.res +1 -0
- package/src/FetchState.res +1256 -639
- package/src/FetchState.res.mjs +1135 -612
- package/src/GlobalState.res +1190 -0
- package/src/GlobalState.res.mjs +1183 -0
- package/src/GlobalStateManager.res +68 -0
- package/src/GlobalStateManager.res.mjs +75 -0
- package/src/GlobalStateManager.resi +7 -0
- package/src/HandlerLoader.res +89 -0
- package/src/HandlerLoader.res.mjs +79 -0
- package/src/HandlerRegister.res +357 -0
- package/src/HandlerRegister.res.mjs +299 -0
- package/src/{EventRegister.resi → HandlerRegister.resi} +13 -13
- package/src/Hasura.res +111 -175
- package/src/Hasura.res.mjs +88 -150
- package/src/InMemoryStore.res +1 -1
- package/src/InMemoryStore.res.mjs +3 -3
- package/src/InMemoryTable.res +1 -1
- package/src/InMemoryTable.res.mjs +1 -1
- package/src/Internal.gen.ts +4 -0
- package/src/Internal.res +230 -12
- package/src/Internal.res.mjs +115 -1
- package/src/LoadLayer.res +444 -0
- package/src/LoadLayer.res.mjs +296 -0
- package/src/LoadLayer.resi +32 -0
- package/src/LogSelection.res +33 -27
- package/src/LogSelection.res.mjs +6 -0
- package/src/Logging.res +21 -7
- package/src/Logging.res.mjs +16 -8
- package/src/Main.res +377 -0
- package/src/Main.res.mjs +339 -0
- package/src/Persistence.res +7 -21
- package/src/Persistence.res.mjs +3 -3
- package/src/PgStorage.gen.ts +10 -0
- package/src/PgStorage.res +116 -69
- package/src/PgStorage.res.d.mts +5 -0
- package/src/PgStorage.res.mjs +93 -50
- package/src/Prometheus.res +294 -224
- package/src/Prometheus.res.mjs +353 -340
- package/src/ReorgDetection.res +6 -10
- package/src/ReorgDetection.res.mjs +6 -6
- package/src/SafeCheckpointTracking.res +4 -4
- package/src/SafeCheckpointTracking.res.mjs +2 -2
- package/src/Sink.res +4 -2
- package/src/Sink.res.mjs +2 -1
- package/src/TableIndices.res +0 -1
- package/src/TestIndexer.res +692 -0
- package/src/TestIndexer.res.mjs +527 -0
- package/src/TestIndexerProxyStorage.res +205 -0
- package/src/TestIndexerProxyStorage.res.mjs +151 -0
- package/src/TopicFilter.res +1 -1
- package/src/Types.ts +1 -1
- package/src/UserContext.res +424 -0
- package/src/UserContext.res.mjs +279 -0
- package/src/Utils.res +97 -26
- package/src/Utils.res.mjs +91 -44
- package/src/bindings/BigInt.res +10 -0
- package/src/bindings/BigInt.res.mjs +15 -0
- package/src/bindings/ClickHouse.res +120 -23
- package/src/bindings/ClickHouse.res.mjs +118 -28
- package/src/bindings/DateFns.res +74 -0
- package/src/bindings/DateFns.res.mjs +22 -0
- package/src/bindings/EventSource.res +8 -1
- package/src/bindings/EventSource.res.mjs +8 -1
- package/src/bindings/Express.res +1 -0
- package/src/bindings/Hrtime.res +14 -1
- package/src/bindings/Hrtime.res.mjs +22 -2
- package/src/bindings/Hrtime.resi +4 -0
- package/src/bindings/Lodash.res +0 -1
- package/src/bindings/NodeJs.res +49 -3
- package/src/bindings/NodeJs.res.mjs +11 -3
- package/src/bindings/Pino.res +24 -10
- package/src/bindings/Pino.res.mjs +14 -8
- package/src/bindings/Postgres.gen.ts +8 -0
- package/src/bindings/Postgres.res +5 -1
- package/src/bindings/Postgres.res.d.mts +5 -0
- package/src/bindings/PromClient.res +0 -10
- package/src/bindings/PromClient.res.mjs +0 -3
- package/src/bindings/Vitest.res +142 -0
- package/src/bindings/Vitest.res.mjs +9 -0
- package/src/bindings/WebSocket.res +27 -0
- package/src/bindings/WebSocket.res.mjs +2 -0
- package/src/bindings/Yargs.res +8 -0
- package/src/bindings/Yargs.res.mjs +2 -0
- package/src/db/EntityHistory.res +7 -7
- package/src/db/EntityHistory.res.mjs +9 -9
- package/src/db/InternalTable.res +59 -111
- package/src/db/InternalTable.res.mjs +73 -104
- package/src/db/Table.res +27 -8
- package/src/db/Table.res.mjs +25 -14
- package/src/sources/Evm.res +84 -0
- package/src/sources/Evm.res.mjs +105 -0
- package/src/sources/EvmChain.res +94 -0
- package/src/sources/EvmChain.res.mjs +60 -0
- package/src/sources/Fuel.res +19 -34
- package/src/sources/Fuel.res.mjs +34 -16
- package/src/sources/FuelSDK.res +38 -0
- package/src/sources/FuelSDK.res.mjs +29 -0
- package/src/sources/HyperFuel.res +2 -2
- package/src/sources/HyperFuel.resi +1 -1
- package/src/sources/HyperFuelClient.res +2 -2
- package/src/sources/HyperFuelSource.res +33 -13
- package/src/sources/HyperFuelSource.res.mjs +24 -16
- package/src/sources/HyperSync.res +36 -6
- package/src/sources/HyperSync.res.mjs +9 -7
- package/src/sources/HyperSync.resi +4 -0
- package/src/sources/HyperSyncClient.res +1 -1
- package/src/sources/HyperSyncHeightStream.res +47 -116
- package/src/sources/HyperSyncHeightStream.res.mjs +46 -73
- package/src/sources/HyperSyncSource.res +118 -139
- package/src/sources/HyperSyncSource.res.mjs +104 -121
- package/src/sources/Rpc.res +86 -14
- package/src/sources/Rpc.res.mjs +101 -9
- package/src/sources/RpcSource.res +621 -364
- package/src/sources/RpcSource.res.mjs +843 -410
- package/src/sources/RpcWebSocketHeightStream.res +181 -0
- package/src/sources/RpcWebSocketHeightStream.res.mjs +196 -0
- package/src/sources/Source.res +7 -5
- package/src/sources/SourceManager.res +325 -225
- package/src/sources/SourceManager.res.mjs +314 -171
- package/src/sources/SourceManager.resi +17 -6
- package/src/sources/Svm.res +81 -0
- package/src/sources/Svm.res.mjs +90 -0
- package/src/tui/Tui.res +247 -0
- package/src/tui/Tui.res.mjs +337 -0
- package/src/tui/bindings/Ink.res +371 -0
- package/src/tui/bindings/Ink.res.mjs +72 -0
- package/src/tui/bindings/Style.res +123 -0
- package/src/tui/bindings/Style.res.mjs +2 -0
- package/src/tui/components/BufferedProgressBar.res +40 -0
- package/src/tui/components/BufferedProgressBar.res.mjs +57 -0
- package/src/tui/components/CustomHooks.res +122 -0
- package/src/tui/components/CustomHooks.res.mjs +179 -0
- package/src/tui/components/Messages.res +41 -0
- package/src/tui/components/Messages.res.mjs +75 -0
- package/src/tui/components/SyncETA.res +174 -0
- package/src/tui/components/SyncETA.res.mjs +263 -0
- package/src/tui/components/TuiData.res +47 -0
- package/src/tui/components/TuiData.res.mjs +34 -0
- package/svm.schema.json +112 -0
- package/bin.js +0 -48
- package/src/EventRegister.res +0 -241
- package/src/EventRegister.res.mjs +0 -240
- package/src/bindings/Ethers.gen.ts +0 -14
- package/src/bindings/Ethers.res +0 -204
- package/src/bindings/Ethers.res.mjs +0 -130
- /package/src/{Indexer.res.mjs → Ctx.res.mjs} +0 -0
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
open Belt
|
|
2
|
+
|
|
3
|
+
type chainConfig = {
|
|
4
|
+
startBlock: int,
|
|
5
|
+
endBlock: int,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type processResult = {changes: array<unknown>}
|
|
9
|
+
|
|
10
|
+
type t<'processConfig> = {process: 'processConfig => promise<processResult>}
|
|
11
|
+
|
|
12
|
+
type entityChange = {
|
|
13
|
+
sets: array<unknown>,
|
|
14
|
+
deleted: array<string>,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type testIndexerState = {
|
|
18
|
+
mutable processInProgress: bool,
|
|
19
|
+
progressBlockByChain: dict<int>,
|
|
20
|
+
// Store decoded entities (not JSON) for proper comparison operations
|
|
21
|
+
entities: dict<dict<Internal.entity>>,
|
|
22
|
+
entityConfigs: dict<Internal.entityConfig>,
|
|
23
|
+
mutable processChanges: array<unknown>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Cast Internal.entity back to DynamicContractRegistry.t
|
|
27
|
+
external castFromDcRegistry: Internal.entity => InternalTable.DynamicContractRegistry.t =
|
|
28
|
+
"%identity"
|
|
29
|
+
|
|
30
|
+
// Convert DynamicContractRegistry.t to Internal.indexingContract
|
|
31
|
+
let toIndexingContract = (
|
|
32
|
+
dc: InternalTable.DynamicContractRegistry.t,
|
|
33
|
+
): Internal.indexingContract => {
|
|
34
|
+
address: dc.contractAddress,
|
|
35
|
+
contractName: dc.contractName,
|
|
36
|
+
startBlock: dc.registeringEventBlockNumber,
|
|
37
|
+
registrationBlock: Some(dc.registeringEventBlockNumber),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let handleLoadByIds = (
|
|
41
|
+
state: testIndexerState,
|
|
42
|
+
~tableName: string,
|
|
43
|
+
~ids: array<string>,
|
|
44
|
+
): Js.Json.t => {
|
|
45
|
+
let entityDict = state.entities->Js.Dict.get(tableName)->Option.getWithDefault(Js.Dict.empty())
|
|
46
|
+
let entityConfig = state.entityConfigs->Js.Dict.unsafeGet(tableName)
|
|
47
|
+
let results = []
|
|
48
|
+
ids->Array.forEach(id => {
|
|
49
|
+
switch entityDict->Js.Dict.get(id) {
|
|
50
|
+
| Some(entity) =>
|
|
51
|
+
// Serialize entity back to JSON for worker thread
|
|
52
|
+
let jsonEntity = entity->S.reverseConvertToJsonOrThrow(entityConfig.schema)
|
|
53
|
+
results->Array.push(jsonEntity)->ignore
|
|
54
|
+
| None => ()
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
results->Js.Json.array
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let handleLoadByField = (
|
|
61
|
+
state: testIndexerState,
|
|
62
|
+
~tableName: string,
|
|
63
|
+
~fieldName: string,
|
|
64
|
+
~fieldValue: Js.Json.t,
|
|
65
|
+
~operator: Persistence.operator,
|
|
66
|
+
): Js.Json.t => {
|
|
67
|
+
let entityDict = state.entities->Js.Dict.get(tableName)->Option.getWithDefault(Js.Dict.empty())
|
|
68
|
+
let entityConfig = state.entityConfigs->Js.Dict.unsafeGet(tableName)
|
|
69
|
+
let results = []
|
|
70
|
+
|
|
71
|
+
// Get the field schema from the entity's table to properly parse the JSON field value
|
|
72
|
+
let fieldSchema = switch entityConfig.table->Table.getFieldByName(fieldName) {
|
|
73
|
+
| Some(Table.Field({fieldSchema})) => fieldSchema
|
|
74
|
+
| _ => Js.Exn.raiseError(`Field ${fieldName} not found in entity ${tableName}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Parse JSON field value to typed value using the field's schema
|
|
78
|
+
let parsedFieldValue = fieldValue->S.convertOrThrow(fieldSchema)->TableIndices.FieldValue.castFrom
|
|
79
|
+
|
|
80
|
+
// Compare using TableIndices.FieldValue logic (same approach as InMemoryTable)
|
|
81
|
+
// This properly handles bigint and BigDecimal comparisons
|
|
82
|
+
entityDict
|
|
83
|
+
->Js.Dict.values
|
|
84
|
+
->Array.forEach(entity => {
|
|
85
|
+
// Cast entity to dict of field values (same approach as InMemoryTable)
|
|
86
|
+
let entityAsDict = entity->(Utils.magic: Internal.entity => dict<TableIndices.FieldValue.t>)
|
|
87
|
+
switch entityAsDict->Js.Dict.get(fieldName) {
|
|
88
|
+
| Some(entityFieldValue) => {
|
|
89
|
+
let matches = switch operator {
|
|
90
|
+
| #"=" => entityFieldValue->TableIndices.FieldValue.eq(parsedFieldValue)
|
|
91
|
+
| #">" => entityFieldValue->TableIndices.FieldValue.gt(parsedFieldValue)
|
|
92
|
+
| #"<" => entityFieldValue->TableIndices.FieldValue.lt(parsedFieldValue)
|
|
93
|
+
}
|
|
94
|
+
if matches {
|
|
95
|
+
// Serialize entity back to JSON for worker thread
|
|
96
|
+
let jsonEntity = entity->S.reverseConvertToJsonOrThrow(entityConfig.schema)
|
|
97
|
+
results->Array.push(jsonEntity)->ignore
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
| None => ()
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
results->Js.Json.array
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let handleWriteBatch = (
|
|
108
|
+
state: testIndexerState,
|
|
109
|
+
~updatedEntities: array<TestIndexerProxyStorage.serializableUpdatedEntity>,
|
|
110
|
+
~checkpointIds: array<bigint>,
|
|
111
|
+
~checkpointChainIds: array<int>,
|
|
112
|
+
~checkpointBlockNumbers: array<int>,
|
|
113
|
+
~checkpointBlockHashes: array<Js.Null.t<string>>,
|
|
114
|
+
~checkpointEventsProcessed: array<int>,
|
|
115
|
+
): unit => {
|
|
116
|
+
// Group entity changes by checkpointId
|
|
117
|
+
// checkpointId -> entityName -> entityChange
|
|
118
|
+
let changesByCheckpoint: dict<dict<entityChange>> = Js.Dict.empty()
|
|
119
|
+
|
|
120
|
+
updatedEntities->Array.forEach(({entityName, updates}) => {
|
|
121
|
+
let entityDict = switch state.entities->Js.Dict.get(entityName) {
|
|
122
|
+
| Some(dict) => dict
|
|
123
|
+
| None =>
|
|
124
|
+
let dict = Js.Dict.empty()
|
|
125
|
+
state.entities->Js.Dict.set(entityName, dict)
|
|
126
|
+
dict
|
|
127
|
+
}
|
|
128
|
+
let entityConfig = state.entityConfigs->Js.Dict.unsafeGet(entityName)
|
|
129
|
+
|
|
130
|
+
updates->Array.forEach(update => {
|
|
131
|
+
// Helper to process a single change (Set or Delete)
|
|
132
|
+
let processChange = (change: TestIndexerProxyStorage.serializableChange) => {
|
|
133
|
+
switch change {
|
|
134
|
+
| Set({entityId, entity, checkpointId}) =>
|
|
135
|
+
// Parse entity immediately to store decoded values for proper comparisons
|
|
136
|
+
// (bigint/BigDecimal need actual values, not JSON strings)
|
|
137
|
+
let parsedEntity = entity->S.parseOrThrow(entityConfig.schema)
|
|
138
|
+
|
|
139
|
+
// Update entities dict with parsed entity for load operations
|
|
140
|
+
entityDict->Js.Dict.set(entityId, parsedEntity)
|
|
141
|
+
|
|
142
|
+
// Track change by checkpoint
|
|
143
|
+
let checkpointKey = checkpointId->BigInt.toString
|
|
144
|
+
let entityChanges = switch changesByCheckpoint->Js.Dict.get(checkpointKey) {
|
|
145
|
+
| Some(changes) => changes
|
|
146
|
+
| None =>
|
|
147
|
+
let changes = Js.Dict.empty()
|
|
148
|
+
changesByCheckpoint->Js.Dict.set(checkpointKey, changes)
|
|
149
|
+
changes
|
|
150
|
+
}
|
|
151
|
+
let entityChange = switch entityChanges->Js.Dict.get(entityName) {
|
|
152
|
+
| Some(change) => change
|
|
153
|
+
| None =>
|
|
154
|
+
let change = {sets: [], deleted: []}
|
|
155
|
+
entityChanges->Js.Dict.set(entityName, change)
|
|
156
|
+
change
|
|
157
|
+
}
|
|
158
|
+
entityChange.sets->Array.push(parsedEntity->Utils.magic)->ignore
|
|
159
|
+
|
|
160
|
+
| Delete({entityId, checkpointId}) =>
|
|
161
|
+
// Update entities dict for load operations
|
|
162
|
+
Js.Dict.unsafeDeleteKey(entityDict->Obj.magic, entityId)
|
|
163
|
+
|
|
164
|
+
// Track change by checkpoint
|
|
165
|
+
let checkpointKey = checkpointId->BigInt.toString
|
|
166
|
+
let entityChanges = switch changesByCheckpoint->Js.Dict.get(checkpointKey) {
|
|
167
|
+
| Some(changes) => changes
|
|
168
|
+
| None =>
|
|
169
|
+
let changes = Js.Dict.empty()
|
|
170
|
+
changesByCheckpoint->Js.Dict.set(checkpointKey, changes)
|
|
171
|
+
changes
|
|
172
|
+
}
|
|
173
|
+
let entityChange = switch entityChanges->Js.Dict.get(entityName) {
|
|
174
|
+
| Some(change) => change
|
|
175
|
+
| None =>
|
|
176
|
+
let change = {sets: [], deleted: []}
|
|
177
|
+
entityChanges->Js.Dict.set(entityName, change)
|
|
178
|
+
change
|
|
179
|
+
}
|
|
180
|
+
entityChange.deleted->Array.push(entityId)->ignore
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Iterate over all history entries (mirroring PgStorage.res behavior)
|
|
185
|
+
update.history->Array.forEach(processChange)
|
|
186
|
+
|
|
187
|
+
// Also include latestChange if history is empty (fallback for backwards compatibility)
|
|
188
|
+
if update.history->Array.length === 0 {
|
|
189
|
+
processChange(update.latestChange)
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// Build combined checkpoint + entity changes objects
|
|
195
|
+
for i in 0 to checkpointIds->Array.length - 1 {
|
|
196
|
+
let checkpointId = checkpointIds->Array.getUnsafe(i)
|
|
197
|
+
let change: dict<unknown> = Js.Dict.empty()
|
|
198
|
+
|
|
199
|
+
// Add checkpoint metadata
|
|
200
|
+
change->Js.Dict.set("block", checkpointBlockNumbers->Array.getUnsafe(i)->Utils.magic)
|
|
201
|
+
switch checkpointBlockHashes->Array.getUnsafe(i)->Js.Null.toOption {
|
|
202
|
+
| Some(hash) => change->Js.Dict.set("blockHash", hash->Utils.magic)
|
|
203
|
+
| None => () // Skip blockHash when null
|
|
204
|
+
}
|
|
205
|
+
change->Js.Dict.set("chainId", checkpointChainIds->Array.getUnsafe(i)->Utils.magic)
|
|
206
|
+
change->Js.Dict.set(
|
|
207
|
+
"eventsProcessed",
|
|
208
|
+
checkpointEventsProcessed->Array.getUnsafe(i)->Utils.magic,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
// Add entity changes for this checkpoint
|
|
212
|
+
let checkpointKey = checkpointId->BigInt.toString
|
|
213
|
+
switch changesByCheckpoint->Js.Dict.get(checkpointKey) {
|
|
214
|
+
| Some(entityChanges) =>
|
|
215
|
+
entityChanges
|
|
216
|
+
->Js.Dict.entries
|
|
217
|
+
->Array.forEach(((entityName, {sets, deleted})) => {
|
|
218
|
+
// Transform dynamic_contract_registry to addresses with simplified structure
|
|
219
|
+
if entityName === InternalTable.DynamicContractRegistry.name {
|
|
220
|
+
let entityObj: dict<unknown> = Js.Dict.empty()
|
|
221
|
+
if sets->Array.length > 0 {
|
|
222
|
+
// Transform sets to simplified {address, contract} objects
|
|
223
|
+
let simplifiedSets = sets->Array.map(entity => {
|
|
224
|
+
let dc = entity->Utils.magic->castFromDcRegistry
|
|
225
|
+
{"address": dc.contractAddress, "contract": dc.contractName}
|
|
226
|
+
})
|
|
227
|
+
entityObj->Js.Dict.set("sets", simplifiedSets->Utils.magic)
|
|
228
|
+
}
|
|
229
|
+
// Note: deleted is not relevant for addresses since we use address string directly
|
|
230
|
+
change->Js.Dict.set("addresses", entityObj->Utils.magic)
|
|
231
|
+
} else {
|
|
232
|
+
let entityObj: dict<unknown> = Js.Dict.empty()
|
|
233
|
+
if sets->Array.length > 0 {
|
|
234
|
+
entityObj->Js.Dict.set("sets", sets->Utils.magic)
|
|
235
|
+
}
|
|
236
|
+
if deleted->Array.length > 0 {
|
|
237
|
+
entityObj->Js.Dict.set("deleted", deleted->Utils.magic)
|
|
238
|
+
}
|
|
239
|
+
change->Js.Dict.set(entityName, entityObj->Utils.magic)
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
| None => ()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
state.processChanges->Array.push(change->Utils.magic)->ignore
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let makeInitialState = (
|
|
250
|
+
~config: Config.t,
|
|
251
|
+
~processConfigChains: Js.Dict.t<chainConfig>,
|
|
252
|
+
~dynamicContractsByChain: dict<array<Internal.indexingContract>>,
|
|
253
|
+
): Persistence.initialState => {
|
|
254
|
+
let chainKeys = processConfigChains->Js.Dict.keys
|
|
255
|
+
let chains = chainKeys->Array.map(chainIdStr => {
|
|
256
|
+
let chainId = chainIdStr->Int.fromString->Option.getWithDefault(0)
|
|
257
|
+
let chain = ChainMap.Chain.makeUnsafe(~chainId)
|
|
258
|
+
|
|
259
|
+
if !(config.chainMap->ChainMap.has(chain)) {
|
|
260
|
+
Js.Exn.raiseError(`Chain ${chainIdStr} is not configured in config.yaml`)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let processChainConfig = processConfigChains->Js.Dict.unsafeGet(chainIdStr)
|
|
264
|
+
let dynamicContracts =
|
|
265
|
+
dynamicContractsByChain
|
|
266
|
+
->Js.Dict.get(chainIdStr)
|
|
267
|
+
->Option.getWithDefault([])
|
|
268
|
+
{
|
|
269
|
+
Persistence.id: chainId,
|
|
270
|
+
startBlock: processChainConfig.startBlock,
|
|
271
|
+
endBlock: Some(processChainConfig.endBlock),
|
|
272
|
+
sourceBlockNumber: processChainConfig.endBlock,
|
|
273
|
+
maxReorgDepth: 0, // No reorg support in test indexer
|
|
274
|
+
progressBlockNumber: -1,
|
|
275
|
+
numEventsProcessed: 0.,
|
|
276
|
+
firstEventBlockNumber: None,
|
|
277
|
+
timestampCaughtUpToHeadOrEndblock: None,
|
|
278
|
+
dynamicContracts,
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
{
|
|
283
|
+
cleanRun: true,
|
|
284
|
+
cache: Js.Dict.empty(),
|
|
285
|
+
chains,
|
|
286
|
+
checkpointId: InternalTable.Checkpoints.initialCheckpointId,
|
|
287
|
+
reorgCheckpoints: [],
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let validateBlockRange = (
|
|
292
|
+
~chainId: string,
|
|
293
|
+
~configChain: Config.chain,
|
|
294
|
+
~processChainConfig: chainConfig,
|
|
295
|
+
~progressBlock: option<int>,
|
|
296
|
+
) => {
|
|
297
|
+
// Check startBlock >= config.startBlock
|
|
298
|
+
if processChainConfig.startBlock < configChain.startBlock {
|
|
299
|
+
Js.Exn.raiseError(
|
|
300
|
+
`Invalid block range for chain ${chainId}: startBlock (${processChainConfig.startBlock->Int.toString}) is less than config.startBlock (${configChain.startBlock->Int.toString}). ` ++
|
|
301
|
+
`Either use startBlock >= ${configChain.startBlock->Int.toString} or create a new test indexer with createTestIndexer().`,
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Check endBlock <= config.endBlock (if defined)
|
|
306
|
+
switch configChain.endBlock {
|
|
307
|
+
| Some(configEndBlock) if processChainConfig.endBlock > configEndBlock =>
|
|
308
|
+
Js.Exn.raiseError(
|
|
309
|
+
`Invalid block range for chain ${chainId}: endBlock (${processChainConfig.endBlock->Int.toString}) exceeds config.endBlock (${configEndBlock->Int.toString}). ` ++
|
|
310
|
+
`Either use endBlock <= ${configEndBlock->Int.toString} or create a new test indexer with createTestIndexer().`,
|
|
311
|
+
)
|
|
312
|
+
| _ => ()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check startBlock > progressBlock
|
|
316
|
+
switch progressBlock {
|
|
317
|
+
| Some(prevEndBlock) if processChainConfig.startBlock <= prevEndBlock =>
|
|
318
|
+
Js.Exn.raiseError(
|
|
319
|
+
`Invalid block range for chain ${chainId}: startBlock (${processChainConfig.startBlock->Int.toString}) must be greater than previously processed endBlock (${prevEndBlock->Int.toString}). ` ++
|
|
320
|
+
`Either use startBlock > ${prevEndBlock->Int.toString} or create a new test indexer with createTestIndexer().`,
|
|
321
|
+
)
|
|
322
|
+
| _ => ()
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Entity operations for direct manipulation outside of handlers
|
|
327
|
+
let makeEntityGet = (~state: testIndexerState, ~entityConfig: Internal.entityConfig): (
|
|
328
|
+
string => promise<option<Internal.entity>>
|
|
329
|
+
) => {
|
|
330
|
+
entityId => {
|
|
331
|
+
if state.processInProgress {
|
|
332
|
+
Js.Exn.raiseError(
|
|
333
|
+
`Cannot call ${entityConfig.name}.get() while indexer.process() is running. ` ++ "Wait for process() to complete before accessing entities directly.",
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
let entityDict =
|
|
337
|
+
state.entities->Js.Dict.get(entityConfig.name)->Option.getWithDefault(Js.Dict.empty())
|
|
338
|
+
Promise.resolve(entityDict->Js.Dict.get(entityId))
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let makeEntitySet = (~state: testIndexerState, ~entityConfig: Internal.entityConfig): (
|
|
343
|
+
Internal.entity => unit
|
|
344
|
+
) => {
|
|
345
|
+
entity => {
|
|
346
|
+
if state.processInProgress {
|
|
347
|
+
Js.Exn.raiseError(
|
|
348
|
+
`Cannot call ${entityConfig.name}.set() while indexer.process() is running. ` ++ "Wait for process() to complete before modifying entities directly.",
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
let entityDict = switch state.entities->Js.Dict.get(entityConfig.name) {
|
|
352
|
+
| Some(dict) => dict
|
|
353
|
+
| None =>
|
|
354
|
+
let dict = Js.Dict.empty()
|
|
355
|
+
state.entities->Js.Dict.set(entityConfig.name, dict)
|
|
356
|
+
dict
|
|
357
|
+
}
|
|
358
|
+
entityDict->Js.Dict.set(entity.id, entity)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
type entityOps = {
|
|
363
|
+
get: string => promise<option<Internal.entity>>,
|
|
364
|
+
set: Internal.entity => unit,
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let makeCreateTestIndexer = (
|
|
368
|
+
~config: Config.t,
|
|
369
|
+
~workerPath: string,
|
|
370
|
+
~allEntities: array<Internal.entityConfig>,
|
|
371
|
+
): (unit => t<'processConfig>) => {
|
|
372
|
+
() => {
|
|
373
|
+
let entities = Js.Dict.empty()
|
|
374
|
+
let entityConfigs = Js.Dict.empty()
|
|
375
|
+
allEntities->Array.forEach(entityConfig => {
|
|
376
|
+
entities->Js.Dict.set(entityConfig.name, Js.Dict.empty())
|
|
377
|
+
entityConfigs->Js.Dict.set(entityConfig.name, entityConfig)
|
|
378
|
+
})
|
|
379
|
+
let state = {
|
|
380
|
+
processInProgress: false,
|
|
381
|
+
progressBlockByChain: Js.Dict.empty(),
|
|
382
|
+
entities,
|
|
383
|
+
entityConfigs,
|
|
384
|
+
processChanges: [],
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Build entity operations for each user entity
|
|
388
|
+
let entityOpsDict: Js.Dict.t<entityOps> = Js.Dict.empty()
|
|
389
|
+
allEntities->Array.forEach(entityConfig => {
|
|
390
|
+
// Only create ops for user entities (not internal tables like dynamic_contract_registry)
|
|
391
|
+
if entityConfig.name !== InternalTable.DynamicContractRegistry.name {
|
|
392
|
+
entityOpsDict->Js.Dict.set(
|
|
393
|
+
entityConfig.name,
|
|
394
|
+
{
|
|
395
|
+
get: makeEntityGet(~state, ~entityConfig),
|
|
396
|
+
set: makeEntitySet(~state, ~entityConfig),
|
|
397
|
+
},
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
// Build chain info from config (similar to Main.getGlobalIndexer but static)
|
|
403
|
+
let chainIds = []
|
|
404
|
+
let chains = Utils.Object.createNullObject()
|
|
405
|
+
config.chainMap
|
|
406
|
+
->ChainMap.values
|
|
407
|
+
->Array.forEach(chainConfig => {
|
|
408
|
+
let chainIdStr = chainConfig.id->Int.toString
|
|
409
|
+
chainIds->Js.Array2.push(chainConfig.id)->ignore
|
|
410
|
+
|
|
411
|
+
let chainObj = Utils.Object.createNullObject()
|
|
412
|
+
chainObj
|
|
413
|
+
->Utils.Object.definePropertyWithValue("id", {enumerable: true, value: chainConfig.id})
|
|
414
|
+
->Utils.Object.definePropertyWithValue(
|
|
415
|
+
"startBlock",
|
|
416
|
+
{enumerable: true, value: chainConfig.startBlock},
|
|
417
|
+
)
|
|
418
|
+
->Utils.Object.definePropertyWithValue(
|
|
419
|
+
"endBlock",
|
|
420
|
+
{enumerable: true, value: chainConfig.endBlock},
|
|
421
|
+
)
|
|
422
|
+
->Utils.Object.definePropertyWithValue("name", {enumerable: true, value: chainConfig.name})
|
|
423
|
+
->Utils.Object.definePropertyWithValue("isLive", {enumerable: true, value: false})
|
|
424
|
+
->ignore
|
|
425
|
+
|
|
426
|
+
// Add contracts to chain object
|
|
427
|
+
chainConfig.contracts->Array.forEach(contract => {
|
|
428
|
+
let contractObj = Utils.Object.createNullObject()
|
|
429
|
+
contractObj
|
|
430
|
+
->Utils.Object.definePropertyWithValue("name", {enumerable: true, value: contract.name})
|
|
431
|
+
->Utils.Object.definePropertyWithValue("abi", {enumerable: true, value: contract.abi})
|
|
432
|
+
->Utils.Object.defineProperty(
|
|
433
|
+
"addresses",
|
|
434
|
+
{
|
|
435
|
+
enumerable: true,
|
|
436
|
+
get: () => {
|
|
437
|
+
if state.processInProgress {
|
|
438
|
+
Js.Exn.raiseError(
|
|
439
|
+
`Cannot access ${contract.name}.addresses while indexer.process() is running. ` ++ "Wait for process() to complete before reading contract addresses.",
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
// Start with static config addresses
|
|
443
|
+
let addresses = contract.addresses->Array.copy
|
|
444
|
+
// Add accumulated dynamic contract addresses
|
|
445
|
+
switch state.entities->Js.Dict.get(InternalTable.DynamicContractRegistry.name) {
|
|
446
|
+
| Some(dcDict) =>
|
|
447
|
+
dcDict
|
|
448
|
+
->Js.Dict.values
|
|
449
|
+
->Array.forEach(
|
|
450
|
+
entity => {
|
|
451
|
+
let dc = entity->castFromDcRegistry
|
|
452
|
+
if dc.contractName === contract.name && dc.chainId === chainConfig.id {
|
|
453
|
+
addresses->Array.push(dc.contractAddress)->ignore
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
)
|
|
457
|
+
| None => ()
|
|
458
|
+
}
|
|
459
|
+
addresses
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
)
|
|
463
|
+
->ignore
|
|
464
|
+
|
|
465
|
+
chainObj
|
|
466
|
+
->Utils.Object.definePropertyWithValue(
|
|
467
|
+
contract.name,
|
|
468
|
+
{enumerable: true, value: contractObj},
|
|
469
|
+
)
|
|
470
|
+
->ignore
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
chains
|
|
474
|
+
->Utils.Object.definePropertyWithValue(chainIdStr, {enumerable: true, value: chainObj})
|
|
475
|
+
->ignore
|
|
476
|
+
|
|
477
|
+
if chainConfig.name !== chainIdStr {
|
|
478
|
+
chains
|
|
479
|
+
->Utils.Object.definePropertyWithValue(
|
|
480
|
+
chainConfig.name,
|
|
481
|
+
{enumerable: false, value: chainObj},
|
|
482
|
+
)
|
|
483
|
+
->ignore
|
|
484
|
+
}
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
// Build the result object with process + entity operations + chain info
|
|
488
|
+
let result: Js.Dict.t<unknown> = Js.Dict.empty()
|
|
489
|
+
result->Js.Dict.set("chainIds", chainIds->(Utils.magic: array<int> => unknown))
|
|
490
|
+
result->Js.Dict.set("chains", chains->(Utils.magic: {..} => unknown))
|
|
491
|
+
entityOpsDict
|
|
492
|
+
->Js.Dict.entries
|
|
493
|
+
->Array.forEach(((name, ops)) => {
|
|
494
|
+
result->Js.Dict.set(name, ops->(Utils.magic: entityOps => unknown))
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
result->Js.Dict.set(
|
|
498
|
+
"process",
|
|
499
|
+
(processConfig => {
|
|
500
|
+
// Check if already processing
|
|
501
|
+
if state.processInProgress {
|
|
502
|
+
Js.Exn.raiseError(
|
|
503
|
+
"createTestIndexer process is already running. Only one process call is allowed at a time",
|
|
504
|
+
)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Validate chains
|
|
508
|
+
let chains: Js.Dict.t<chainConfig> = (processConfig->Utils.magic)["chains"]->Utils.magic
|
|
509
|
+
let chainKeys = chains->Js.Dict.keys
|
|
510
|
+
|
|
511
|
+
switch chainKeys->Array.length {
|
|
512
|
+
| 0 => Js.Exn.raiseError("createTestIndexer requires exactly one chain to be defined")
|
|
513
|
+
| 1 => ()
|
|
514
|
+
| n =>
|
|
515
|
+
Js.Exn.raiseError(
|
|
516
|
+
`createTestIndexer does not support processing multiple chains at once. Found ${n->Int.toString} chains defined`,
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Validate block ranges for each chain
|
|
521
|
+
chainKeys->Array.forEach(chainIdStr => {
|
|
522
|
+
let chainId = chainIdStr->Int.fromString->Option.getWithDefault(0)
|
|
523
|
+
let chain = ChainMap.Chain.makeUnsafe(~chainId)
|
|
524
|
+
let configChain = config.chainMap->ChainMap.get(chain)
|
|
525
|
+
let processChainConfig = chains->Js.Dict.unsafeGet(chainIdStr)
|
|
526
|
+
let progressBlock = state.progressBlockByChain->Js.Dict.get(chainIdStr)
|
|
527
|
+
|
|
528
|
+
validateBlockRange(~chainId=chainIdStr, ~configChain, ~processChainConfig, ~progressBlock)
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
// Reset processChanges for this run
|
|
532
|
+
state.processChanges = []
|
|
533
|
+
|
|
534
|
+
// Extract dynamic contracts from state.entities for each chain
|
|
535
|
+
let dynamicContractsByChain: dict<array<Internal.indexingContract>> = Js.Dict.empty()
|
|
536
|
+
switch state.entities->Js.Dict.get(InternalTable.DynamicContractRegistry.name) {
|
|
537
|
+
| Some(dcDict) =>
|
|
538
|
+
dcDict
|
|
539
|
+
->Js.Dict.values
|
|
540
|
+
->Array.forEach(entity => {
|
|
541
|
+
let dc = entity->castFromDcRegistry
|
|
542
|
+
let chainIdStr = dc.chainId->Int.toString
|
|
543
|
+
let contracts = switch dynamicContractsByChain->Js.Dict.get(chainIdStr) {
|
|
544
|
+
| Some(arr) => arr
|
|
545
|
+
| None =>
|
|
546
|
+
let arr = []
|
|
547
|
+
dynamicContractsByChain->Js.Dict.set(chainIdStr, arr)
|
|
548
|
+
arr
|
|
549
|
+
}
|
|
550
|
+
contracts->Array.push(dc->toIndexingContract)->ignore
|
|
551
|
+
})
|
|
552
|
+
| None => ()
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Create initialState from processConfig chains
|
|
556
|
+
let initialState = makeInitialState(
|
|
557
|
+
~config,
|
|
558
|
+
~processConfigChains=chains,
|
|
559
|
+
~dynamicContractsByChain,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
Promise.make((resolve, reject) => {
|
|
563
|
+
// Include initialState in workerData
|
|
564
|
+
let workerDataObj = {
|
|
565
|
+
"processConfig": processConfig->(Utils.magic: 'a => Js.Json.t),
|
|
566
|
+
"initialState": initialState->(Utils.magic: Persistence.initialState => Js.Json.t),
|
|
567
|
+
}
|
|
568
|
+
let workerData = workerDataObj->(Utils.magic: {"processConfig": Js.Json.t, "initialState": Js.Json.t} => Js.Json.t)
|
|
569
|
+
let worker = try {
|
|
570
|
+
NodeJs.WorkerThreads.makeWorker(
|
|
571
|
+
workerPath,
|
|
572
|
+
{
|
|
573
|
+
workerData: workerData,
|
|
574
|
+
},
|
|
575
|
+
)
|
|
576
|
+
} catch {
|
|
577
|
+
| exn =>
|
|
578
|
+
reject(exn->Utils.magic)
|
|
579
|
+
raise(exn)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Set flag only after worker is successfully created
|
|
583
|
+
state.processInProgress = true
|
|
584
|
+
|
|
585
|
+
// Handle messages from worker
|
|
586
|
+
worker->NodeJs.WorkerThreads.onMessage((msg: TestIndexerProxyStorage.workerMessage) => {
|
|
587
|
+
let respond = data =>
|
|
588
|
+
worker->NodeJs.WorkerThreads.workerPostMessage(
|
|
589
|
+
{
|
|
590
|
+
TestIndexerProxyStorage.id: msg.id,
|
|
591
|
+
payload: TestIndexerProxyStorage.Response({data: data}),
|
|
592
|
+
}->Utils.magic,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
switch msg.payload {
|
|
596
|
+
| LoadByIds({tableName, ids}) => state->handleLoadByIds(~tableName, ~ids)->respond
|
|
597
|
+
|
|
598
|
+
| LoadByField({tableName, fieldName, fieldValue, operator}) =>
|
|
599
|
+
state->handleLoadByField(~tableName, ~fieldName, ~fieldValue, ~operator)->respond
|
|
600
|
+
|
|
601
|
+
| WriteBatch({
|
|
602
|
+
updatedEntities,
|
|
603
|
+
checkpointIds,
|
|
604
|
+
checkpointChainIds,
|
|
605
|
+
checkpointBlockNumbers,
|
|
606
|
+
checkpointBlockHashes,
|
|
607
|
+
checkpointEventsProcessed,
|
|
608
|
+
}) =>
|
|
609
|
+
state->handleWriteBatch(
|
|
610
|
+
~updatedEntities,
|
|
611
|
+
~checkpointIds,
|
|
612
|
+
~checkpointChainIds,
|
|
613
|
+
~checkpointBlockNumbers,
|
|
614
|
+
~checkpointBlockHashes,
|
|
615
|
+
~checkpointEventsProcessed,
|
|
616
|
+
)
|
|
617
|
+
Js.Json.null->respond
|
|
618
|
+
}
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
worker->NodeJs.WorkerThreads.onError(err => {
|
|
622
|
+
state.processInProgress = false
|
|
623
|
+
worker->NodeJs.WorkerThreads.terminate->ignore
|
|
624
|
+
reject(err)
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
worker->NodeJs.WorkerThreads.onExit(code => {
|
|
628
|
+
state.processInProgress = false
|
|
629
|
+
if code !== 0 {
|
|
630
|
+
reject(Utils.Error.make(`Worker exited with code ${code->Int.toString}`))
|
|
631
|
+
} else {
|
|
632
|
+
// Update progressBlockByChain with processed endBlock for each chain
|
|
633
|
+
chainKeys->Array.forEach(
|
|
634
|
+
chainIdStr => {
|
|
635
|
+
let processChainConfig = chains->Js.Dict.unsafeGet(chainIdStr)
|
|
636
|
+
state.progressBlockByChain->Js.Dict.set(chainIdStr, processChainConfig.endBlock)
|
|
637
|
+
},
|
|
638
|
+
)
|
|
639
|
+
// Worker exited successfully (SuccessExit was dispatched in GlobalState)
|
|
640
|
+
resolve({
|
|
641
|
+
changes: state.processChanges,
|
|
642
|
+
})
|
|
643
|
+
}
|
|
644
|
+
})
|
|
645
|
+
})
|
|
646
|
+
})->(Utils.magic: ('a => promise<processResult>) => unknown),
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
result->(Utils.magic: Js.Dict.t<unknown> => t<'processConfig>)
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
type workerData = {
|
|
654
|
+
processConfig: Js.Json.t,
|
|
655
|
+
initialState: Persistence.initialState,
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
let initTestWorker = (~makeGeneratedConfig: unit => Config.t) => {
|
|
659
|
+
if NodeJs.WorkerThreads.isMainThread {
|
|
660
|
+
Js.Exn.raiseError("initTestWorker must be called from a worker thread")
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
let parentPort = switch NodeJs.WorkerThreads.parentPort->Js.Nullable.toOption {
|
|
664
|
+
| Some(port) => port
|
|
665
|
+
| None => Js.Exn.raiseError("initTestWorker: No parent port available")
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
let workerData: option<workerData> = NodeJs.WorkerThreads.workerData->Js.Nullable.toOption
|
|
669
|
+
switch workerData {
|
|
670
|
+
| Some({initialState}) =>
|
|
671
|
+
// Create proxy storage that communicates with main thread
|
|
672
|
+
let proxy = TestIndexerProxyStorage.make(~parentPort, ~initialState)
|
|
673
|
+
let storage = TestIndexerProxyStorage.makeStorage(proxy)
|
|
674
|
+
let config = makeGeneratedConfig()
|
|
675
|
+
let persistence = Persistence.make(
|
|
676
|
+
~userEntities=config.userEntities,
|
|
677
|
+
~allEnums=config.allEnums,
|
|
678
|
+
~storage,
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
// Silence logs by default in test mode unless LOG_LEVEL is explicitly set
|
|
682
|
+
switch Env.userLogLevel {
|
|
683
|
+
| None => Logging.setLogLevel(#silent)
|
|
684
|
+
| Some(_) => ()
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
Main.start(~makeGeneratedConfig, ~persistence, ~isTest=true)->ignore
|
|
688
|
+
| None =>
|
|
689
|
+
Logging.error("TestIndexerWorker: No worker data provided")
|
|
690
|
+
NodeJs.process->NodeJs.exitWithCode(Failure)
|
|
691
|
+
}
|
|
692
|
+
}
|