envio 3.0.0-alpha.0 → 3.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +2 -2
  2. package/evm.schema.json +53 -57
  3. package/fuel.schema.json +35 -31
  4. package/index.d.ts +446 -1
  5. package/index.js +4 -0
  6. package/package.json +23 -11
  7. package/rescript.json +4 -1
  8. package/src/Batch.res.mjs +1 -1
  9. package/src/Benchmark.res +394 -0
  10. package/src/Benchmark.res.mjs +398 -0
  11. package/src/ChainFetcher.res +526 -0
  12. package/src/ChainFetcher.res.mjs +343 -0
  13. package/src/ChainManager.res +182 -0
  14. package/src/ChainManager.res.mjs +150 -0
  15. package/src/Config.res +367 -27
  16. package/src/Config.res.mjs +338 -28
  17. package/src/{Indexer.res → Ctx.res} +1 -1
  18. package/src/Ecosystem.res +25 -0
  19. package/src/Ecosystem.res.mjs +29 -0
  20. package/src/Env.res +252 -0
  21. package/src/Env.res.mjs +271 -0
  22. package/src/Envio.gen.ts +9 -1
  23. package/src/Envio.res +12 -9
  24. package/src/EventProcessing.res +476 -0
  25. package/src/EventProcessing.res.mjs +341 -0
  26. package/src/EventRegister.res +4 -15
  27. package/src/EventRegister.res.mjs +3 -9
  28. package/src/EventRegister.resi +2 -8
  29. package/src/FetchState.res +54 -29
  30. package/src/FetchState.res.mjs +62 -35
  31. package/src/GlobalState.res +1163 -0
  32. package/src/GlobalState.res.mjs +1196 -0
  33. package/src/GlobalStateManager.res +68 -0
  34. package/src/GlobalStateManager.res.mjs +75 -0
  35. package/src/GlobalStateManager.resi +7 -0
  36. package/src/HandlerLoader.res +89 -0
  37. package/src/HandlerLoader.res.mjs +79 -0
  38. package/src/Internal.gen.ts +3 -14
  39. package/src/Internal.res +45 -13
  40. package/src/LoadLayer.res +444 -0
  41. package/src/LoadLayer.res.mjs +296 -0
  42. package/src/LoadLayer.resi +32 -0
  43. package/src/LogSelection.res +33 -27
  44. package/src/LogSelection.res.mjs +6 -0
  45. package/src/Logging.res +11 -4
  46. package/src/Logging.res.mjs +14 -6
  47. package/src/Main.res +351 -0
  48. package/src/Main.res.mjs +312 -0
  49. package/src/Persistence.res +1 -2
  50. package/src/PgStorage.gen.ts +10 -0
  51. package/src/PgStorage.res +28 -34
  52. package/src/PgStorage.res.d.mts +5 -0
  53. package/src/PgStorage.res.mjs +27 -27
  54. package/src/Prometheus.res +8 -8
  55. package/src/Prometheus.res.mjs +10 -10
  56. package/src/ReorgDetection.res +6 -10
  57. package/src/ReorgDetection.res.mjs +6 -6
  58. package/src/TestIndexer.res +536 -0
  59. package/src/TestIndexer.res.mjs +412 -0
  60. package/src/TestIndexerProxyStorage.res +210 -0
  61. package/src/TestIndexerProxyStorage.res.mjs +157 -0
  62. package/src/Types.ts +1 -1
  63. package/src/UserContext.res +355 -0
  64. package/src/UserContext.res.mjs +236 -0
  65. package/src/Utils.res +28 -0
  66. package/src/Utils.res.mjs +18 -0
  67. package/src/bindings/ClickHouse.res +31 -1
  68. package/src/bindings/ClickHouse.res.mjs +27 -1
  69. package/src/bindings/DateFns.res +71 -0
  70. package/src/bindings/DateFns.res.mjs +22 -0
  71. package/src/bindings/Ethers.res +27 -67
  72. package/src/bindings/Ethers.res.mjs +18 -70
  73. package/src/bindings/EventSource.res +13 -0
  74. package/src/bindings/EventSource.res.mjs +2 -0
  75. package/src/bindings/NodeJs.res +44 -3
  76. package/src/bindings/NodeJs.res.mjs +11 -3
  77. package/src/bindings/Pino.res +21 -7
  78. package/src/bindings/Pino.res.mjs +11 -5
  79. package/src/bindings/Postgres.gen.ts +8 -0
  80. package/src/bindings/Postgres.res +3 -0
  81. package/src/bindings/Postgres.res.d.mts +5 -0
  82. package/src/bindings/RescriptMocha.res +123 -0
  83. package/src/bindings/RescriptMocha.res.mjs +18 -0
  84. package/src/bindings/Vitest.res +134 -0
  85. package/src/bindings/Vitest.res.mjs +9 -0
  86. package/src/bindings/Yargs.res +8 -0
  87. package/src/bindings/Yargs.res.mjs +2 -0
  88. package/src/db/InternalTable.res +2 -0
  89. package/src/db/InternalTable.res.mjs +1 -1
  90. package/src/sources/Evm.res +87 -0
  91. package/src/sources/Evm.res.mjs +105 -0
  92. package/src/sources/EvmChain.res +90 -0
  93. package/src/sources/EvmChain.res.mjs +57 -0
  94. package/src/sources/Fuel.res +19 -34
  95. package/src/sources/Fuel.res.mjs +34 -16
  96. package/src/sources/FuelSDK.res +38 -0
  97. package/src/sources/FuelSDK.res.mjs +29 -0
  98. package/src/sources/HyperFuel.res +2 -2
  99. package/src/sources/HyperFuel.resi +1 -1
  100. package/src/sources/HyperFuelClient.res +2 -2
  101. package/src/sources/HyperFuelSource.res +8 -8
  102. package/src/sources/HyperFuelSource.res.mjs +5 -5
  103. package/src/sources/HyperSyncHeightStream.res +97 -0
  104. package/src/sources/HyperSyncHeightStream.res.mjs +94 -0
  105. package/src/sources/HyperSyncSource.res +64 -172
  106. package/src/sources/HyperSyncSource.res.mjs +73 -155
  107. package/src/sources/Rpc.res +43 -0
  108. package/src/sources/Rpc.res.mjs +31 -0
  109. package/src/sources/RpcSource.res +32 -130
  110. package/src/sources/RpcSource.res.mjs +47 -121
  111. package/src/sources/Source.res +3 -2
  112. package/src/sources/SourceManager.res +183 -108
  113. package/src/sources/SourceManager.res.mjs +162 -99
  114. package/src/sources/SourceManager.resi +4 -5
  115. package/src/sources/Svm.res +59 -0
  116. package/src/sources/Svm.res.mjs +79 -0
  117. package/src/tui/Tui.res +266 -0
  118. package/src/tui/Tui.res.mjs +342 -0
  119. package/src/tui/bindings/Ink.res +376 -0
  120. package/src/tui/bindings/Ink.res.mjs +75 -0
  121. package/src/tui/bindings/Style.res +123 -0
  122. package/src/tui/bindings/Style.res.mjs +2 -0
  123. package/src/tui/components/BufferedProgressBar.res +40 -0
  124. package/src/tui/components/BufferedProgressBar.res.mjs +57 -0
  125. package/src/tui/components/CustomHooks.res +122 -0
  126. package/src/tui/components/CustomHooks.res.mjs +179 -0
  127. package/src/tui/components/Messages.res +41 -0
  128. package/src/tui/components/Messages.res.mjs +75 -0
  129. package/src/tui/components/SyncETA.res +193 -0
  130. package/src/tui/components/SyncETA.res.mjs +269 -0
  131. package/src/tui/components/TuiData.res +46 -0
  132. package/src/tui/components/TuiData.res.mjs +29 -0
  133. package/src/Platform.res +0 -140
  134. package/src/Platform.res.mjs +0 -170
  135. package/src/bindings/Ethers.gen.ts +0 -14
  136. /package/src/{Indexer.res.mjs → Ctx.res.mjs} +0 -0
@@ -0,0 +1,536 @@
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<float>,
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->Float.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->Float.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->Float.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 =
224
+ sets->Array.map(entity => {
225
+ let dc = entity->Utils.magic->castFromDcRegistry
226
+ {"address": dc.contractAddress, "contract": dc.contractName}
227
+ })
228
+ entityObj->Js.Dict.set("sets", simplifiedSets->Utils.magic)
229
+ }
230
+ // Note: deleted is not relevant for addresses since we use address string directly
231
+ change->Js.Dict.set("addresses", entityObj->Utils.magic)
232
+ } else {
233
+ let entityObj: dict<unknown> = Js.Dict.empty()
234
+ if sets->Array.length > 0 {
235
+ entityObj->Js.Dict.set("sets", sets->Utils.magic)
236
+ }
237
+ if deleted->Array.length > 0 {
238
+ entityObj->Js.Dict.set("deleted", deleted->Utils.magic)
239
+ }
240
+ change->Js.Dict.set(entityName, entityObj->Utils.magic)
241
+ }
242
+ })
243
+ | None => ()
244
+ }
245
+
246
+ state.processChanges->Array.push(change->Utils.magic)->ignore
247
+ }
248
+ }
249
+
250
+ let makeInitialState = (
251
+ ~config: Config.t,
252
+ ~processConfigChains: Js.Dict.t<chainConfig>,
253
+ ~dynamicContractsByChain: dict<array<Internal.indexingContract>>,
254
+ ): Persistence.initialState => {
255
+ let chainKeys = processConfigChains->Js.Dict.keys
256
+ let chains = chainKeys->Array.map(chainIdStr => {
257
+ let chainId = chainIdStr->Int.fromString->Option.getWithDefault(0)
258
+ let chain = ChainMap.Chain.makeUnsafe(~chainId)
259
+
260
+ if !(config.chainMap->ChainMap.has(chain)) {
261
+ Js.Exn.raiseError(`Chain ${chainIdStr} is not configured in config.yaml`)
262
+ }
263
+
264
+ let processChainConfig = processConfigChains->Js.Dict.unsafeGet(chainIdStr)
265
+ let dynamicContracts =
266
+ dynamicContractsByChain
267
+ ->Js.Dict.get(chainIdStr)
268
+ ->Option.getWithDefault([])
269
+ {
270
+ Persistence.id: chainId,
271
+ startBlock: processChainConfig.startBlock,
272
+ endBlock: Some(processChainConfig.endBlock),
273
+ sourceBlockNumber: processChainConfig.endBlock,
274
+ maxReorgDepth: 0, // No reorg support in test indexer
275
+ progressBlockNumber: -1,
276
+ numEventsProcessed: 0,
277
+ firstEventBlockNumber: None,
278
+ timestampCaughtUpToHeadOrEndblock: None,
279
+ dynamicContracts,
280
+ }
281
+ })
282
+
283
+ {
284
+ cleanRun: true,
285
+ cache: Js.Dict.empty(),
286
+ chains,
287
+ checkpointId: InternalTable.Checkpoints.initialCheckpointId,
288
+ reorgCheckpoints: [],
289
+ }
290
+ }
291
+
292
+ let validateBlockRange = (
293
+ ~chainId: string,
294
+ ~configChain: Config.chain,
295
+ ~processChainConfig: chainConfig,
296
+ ~progressBlock: option<int>,
297
+ ) => {
298
+ // Check startBlock >= config.startBlock
299
+ if processChainConfig.startBlock < configChain.startBlock {
300
+ Js.Exn.raiseError(
301
+ `Invalid block range for chain ${chainId}: startBlock (${processChainConfig.startBlock->Int.toString}) is less than config.startBlock (${configChain.startBlock->Int.toString}). ` ++
302
+ `Either use startBlock >= ${configChain.startBlock->Int.toString} or create a new test indexer with createTestIndexer().`,
303
+ )
304
+ }
305
+
306
+ // Check endBlock <= config.endBlock (if defined)
307
+ switch configChain.endBlock {
308
+ | Some(configEndBlock) if processChainConfig.endBlock > configEndBlock =>
309
+ Js.Exn.raiseError(
310
+ `Invalid block range for chain ${chainId}: endBlock (${processChainConfig.endBlock->Int.toString}) exceeds config.endBlock (${configEndBlock->Int.toString}). ` ++
311
+ `Either use endBlock <= ${configEndBlock->Int.toString} or create a new test indexer with createTestIndexer().`,
312
+ )
313
+ | _ => ()
314
+ }
315
+
316
+ // Check startBlock > progressBlock
317
+ switch progressBlock {
318
+ | Some(prevEndBlock) if processChainConfig.startBlock <= prevEndBlock =>
319
+ Js.Exn.raiseError(
320
+ `Invalid block range for chain ${chainId}: startBlock (${processChainConfig.startBlock->Int.toString}) must be greater than previously processed endBlock (${prevEndBlock->Int.toString}). ` ++
321
+ `Either use startBlock > ${prevEndBlock->Int.toString} or create a new test indexer with createTestIndexer().`,
322
+ )
323
+ | _ => ()
324
+ }
325
+ }
326
+
327
+ let makeCreateTestIndexer = (
328
+ ~config: Config.t,
329
+ ~workerPath: string,
330
+ ~allEntities: array<Internal.entityConfig>,
331
+ ): (unit => t<'processConfig>) => {
332
+ () => {
333
+ let entities = Js.Dict.empty()
334
+ let entityConfigs = Js.Dict.empty()
335
+ allEntities->Array.forEach(entityConfig => {
336
+ entities->Js.Dict.set(entityConfig.name, Js.Dict.empty())
337
+ entityConfigs->Js.Dict.set(entityConfig.name, entityConfig)
338
+ })
339
+ let state = {
340
+ processInProgress: false,
341
+ progressBlockByChain: Js.Dict.empty(),
342
+ entities,
343
+ entityConfigs,
344
+ processChanges: [],
345
+ }
346
+ {
347
+ process: processConfig => {
348
+ // Check if already processing
349
+ if state.processInProgress {
350
+ Js.Exn.raiseError(
351
+ "createTestIndexer process is already running. Only one process call is allowed at a time",
352
+ )
353
+ }
354
+
355
+ // Validate chains
356
+ let chains: Js.Dict.t<chainConfig> = (processConfig->Utils.magic)["chains"]->Utils.magic
357
+ let chainKeys = chains->Js.Dict.keys
358
+
359
+ switch chainKeys->Array.length {
360
+ | 0 => Js.Exn.raiseError("createTestIndexer requires exactly one chain to be defined")
361
+ | 1 => ()
362
+ | n =>
363
+ Js.Exn.raiseError(
364
+ `createTestIndexer does not support processing multiple chains at once. Found ${n->Int.toString} chains defined`,
365
+ )
366
+ }
367
+
368
+ // Validate block ranges for each chain
369
+ chainKeys->Array.forEach(chainIdStr => {
370
+ let chainId = chainIdStr->Int.fromString->Option.getWithDefault(0)
371
+ let chain = ChainMap.Chain.makeUnsafe(~chainId)
372
+ let configChain = config.chainMap->ChainMap.get(chain)
373
+ let processChainConfig = chains->Js.Dict.unsafeGet(chainIdStr)
374
+ let progressBlock = state.progressBlockByChain->Js.Dict.get(chainIdStr)
375
+
376
+ validateBlockRange(~chainId=chainIdStr, ~configChain, ~processChainConfig, ~progressBlock)
377
+ })
378
+
379
+ // Reset processChanges for this run
380
+ state.processChanges = []
381
+
382
+ // Extract dynamic contracts from state.entities for each chain
383
+ let dynamicContractsByChain: dict<array<Internal.indexingContract>> = Js.Dict.empty()
384
+ switch state.entities->Js.Dict.get(InternalTable.DynamicContractRegistry.name) {
385
+ | Some(dcDict) =>
386
+ dcDict
387
+ ->Js.Dict.values
388
+ ->Array.forEach(entity => {
389
+ let dc = entity->castFromDcRegistry
390
+ let chainIdStr = dc.chainId->Int.toString
391
+ let contracts = switch dynamicContractsByChain->Js.Dict.get(chainIdStr) {
392
+ | Some(arr) => arr
393
+ | None =>
394
+ let arr = []
395
+ dynamicContractsByChain->Js.Dict.set(chainIdStr, arr)
396
+ arr
397
+ }
398
+ contracts->Array.push(dc->toIndexingContract)->ignore
399
+ })
400
+ | None => ()
401
+ }
402
+
403
+ // Create initialState from processConfig chains
404
+ let initialState = makeInitialState(
405
+ ~config,
406
+ ~processConfigChains=chains,
407
+ ~dynamicContractsByChain,
408
+ )
409
+
410
+ Promise.make((resolve, reject) => {
411
+ // Include initialState in workerData
412
+ let workerDataObj = {
413
+ "processConfig": processConfig->Utils.magic->Js.Json.serializeExn->Js.Json.parseExn,
414
+ "initialState": initialState->Utils.magic,
415
+ }
416
+ let workerData = workerDataObj->Js.Json.serializeExn->Js.Json.parseExn
417
+ let worker = try {
418
+ NodeJs.WorkerThreads.makeWorker(
419
+ workerPath,
420
+ {
421
+ workerData: workerData,
422
+ },
423
+ )
424
+ } catch {
425
+ | exn =>
426
+ reject(exn->Utils.magic)
427
+ raise(exn)
428
+ }
429
+
430
+ // Set flag only after worker is successfully created
431
+ state.processInProgress = true
432
+
433
+ // Handle messages from worker
434
+ worker->NodeJs.WorkerThreads.onMessage((msg: TestIndexerProxyStorage.workerMessage) => {
435
+ let respond = data =>
436
+ worker->NodeJs.WorkerThreads.workerPostMessage(
437
+ {
438
+ TestIndexerProxyStorage.id: msg.id,
439
+ payload: TestIndexerProxyStorage.Response({data: data}),
440
+ }->Utils.magic,
441
+ )
442
+
443
+ switch msg.payload {
444
+ | LoadByIds({tableName, ids}) => state->handleLoadByIds(~tableName, ~ids)->respond
445
+
446
+ | LoadByField({tableName, fieldName, fieldValue, operator}) =>
447
+ state->handleLoadByField(~tableName, ~fieldName, ~fieldValue, ~operator)->respond
448
+
449
+ | WriteBatch({
450
+ updatedEntities,
451
+ checkpointIds,
452
+ checkpointChainIds,
453
+ checkpointBlockNumbers,
454
+ checkpointBlockHashes,
455
+ checkpointEventsProcessed,
456
+ }) =>
457
+ state->handleWriteBatch(
458
+ ~updatedEntities,
459
+ ~checkpointIds,
460
+ ~checkpointChainIds,
461
+ ~checkpointBlockNumbers,
462
+ ~checkpointBlockHashes,
463
+ ~checkpointEventsProcessed,
464
+ )
465
+ Js.Json.null->respond
466
+ }
467
+ })
468
+
469
+ worker->NodeJs.WorkerThreads.onError(err => {
470
+ state.processInProgress = false
471
+ worker->NodeJs.WorkerThreads.terminate->ignore
472
+ reject(err)
473
+ })
474
+
475
+ worker->NodeJs.WorkerThreads.onExit(code => {
476
+ state.processInProgress = false
477
+ if code !== 0 {
478
+ reject(Utils.Error.make(`Worker exited with code ${code->Int.toString}`))
479
+ } else {
480
+ // Update progressBlockByChain with processed endBlock for each chain
481
+ chainKeys->Array.forEach(
482
+ chainIdStr => {
483
+ let processChainConfig = chains->Js.Dict.unsafeGet(chainIdStr)
484
+ state.progressBlockByChain->Js.Dict.set(chainIdStr, processChainConfig.endBlock)
485
+ },
486
+ )
487
+ // Worker exited successfully (SuccessExit was dispatched in GlobalState)
488
+ resolve({
489
+ changes: state.processChanges,
490
+ })
491
+ }
492
+ })
493
+ })
494
+ },
495
+ }
496
+ }
497
+ }
498
+
499
+ type workerData = {
500
+ processConfig: Js.Json.t,
501
+ initialState: Persistence.initialState,
502
+ }
503
+
504
+ let initTestWorker = (
505
+ ~makeGeneratedConfig,
506
+ ~makePersistence: (~storage: Persistence.storage) => Persistence.t,
507
+ ) => {
508
+ if NodeJs.WorkerThreads.isMainThread {
509
+ Js.Exn.raiseError("initTestWorker must be called from a worker thread")
510
+ }
511
+
512
+ let parentPort = switch NodeJs.WorkerThreads.parentPort->Js.Nullable.toOption {
513
+ | Some(port) => port
514
+ | None => Js.Exn.raiseError("initTestWorker: No parent port available")
515
+ }
516
+
517
+ let workerData: option<workerData> = NodeJs.WorkerThreads.workerData->Js.Nullable.toOption
518
+ switch workerData {
519
+ | Some({initialState}) =>
520
+ // Create proxy storage that communicates with main thread
521
+ let proxy = TestIndexerProxyStorage.make(~parentPort, ~initialState)
522
+ let storage = TestIndexerProxyStorage.makeStorage(proxy)
523
+ let persistence = makePersistence(~storage)
524
+
525
+ // Silence logs by default in test mode unless LOG_LEVEL is explicitly set
526
+ switch Env.userLogLevel {
527
+ | None => Logging.setLogLevel(#silent)
528
+ | Some(_) => ()
529
+ }
530
+
531
+ Main.start(~makeGeneratedConfig, ~persistence, ~isTest=true)->ignore
532
+ | None =>
533
+ Logging.error("TestIndexerWorker: No worker data provided")
534
+ NodeJs.process->NodeJs.exitWithCode(Failure)
535
+ }
536
+ }