envio 3.12.0 → 3.13.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.
Files changed (84) hide show
  1. package/package.json +6 -6
  2. package/src/BatchProcessing.res +9 -9
  3. package/src/BatchProcessing.res.mjs +5 -5
  4. package/src/Bin.res +24 -17
  5. package/src/Bin.res.mjs +5 -0
  6. package/src/ChainFetching.res +31 -13
  7. package/src/ChainFetching.res.mjs +9 -4
  8. package/src/ChainState.res +55 -3
  9. package/src/ChainState.res.mjs +46 -3
  10. package/src/ChainState.resi +3 -1
  11. package/src/Config.res +35 -0
  12. package/src/Config.res.mjs +39 -0
  13. package/src/Core.res +4 -0
  14. package/src/Core.res.mjs +4 -0
  15. package/src/CrossChainState.res +61 -4
  16. package/src/CrossChainState.res.mjs +39 -5
  17. package/src/CrossChainState.resi +10 -1
  18. package/src/Env.res +4 -0
  19. package/src/FetchState.res +51 -46
  20. package/src/FetchState.res.mjs +54 -36
  21. package/src/InMemoryTable.res +155 -67
  22. package/src/InMemoryTable.res.mjs +151 -60
  23. package/src/IndexerLoop.res +2 -0
  24. package/src/IndexerLoop.res.mjs +1 -0
  25. package/src/IndexerState.res +41 -1
  26. package/src/IndexerState.res.mjs +43 -4
  27. package/src/IndexerState.resi +10 -0
  28. package/src/LoadLayer.res +57 -34
  29. package/src/LoadLayer.res.mjs +45 -62
  30. package/src/LoadLayer.resi +1 -1
  31. package/src/Logging.res +38 -6
  32. package/src/Logging.res.mjs +32 -5
  33. package/src/Main.res +131 -268
  34. package/src/Main.res.mjs +28 -152
  35. package/src/Metrics.res +263 -102
  36. package/src/Metrics.res.mjs +227 -48
  37. package/src/Persistence.res +27 -2
  38. package/src/Persistence.res.mjs +9 -2
  39. package/src/PgStorage.res +109 -71
  40. package/src/PgStorage.res.mjs +88 -58
  41. package/src/Server.res +181 -0
  42. package/src/Server.res.mjs +143 -0
  43. package/src/Supervisor.res +415 -0
  44. package/src/Supervisor.res.mjs +325 -0
  45. package/src/TestIndexer.res +9 -25
  46. package/src/TestIndexer.res.mjs +5 -4
  47. package/src/UserContext.res +13 -32
  48. package/src/UserContext.res.mjs +1 -7
  49. package/src/Utils.res +1 -4
  50. package/src/Utils.res.mjs +7 -16
  51. package/src/Worker.res +95 -0
  52. package/src/Worker.res.mjs +80 -0
  53. package/src/bindings/NodeJs.res +41 -0
  54. package/src/db/EntityFilter.res +487 -275
  55. package/src/db/EntityFilter.res.mjs +557 -309
  56. package/src/db/InternalTable.res +8 -1
  57. package/src/db/InternalTable.res.mjs +5 -1
  58. package/src/db/Table.res +21 -6
  59. package/src/db/Table.res.mjs +13 -4
  60. package/src/sources/BlockStore.res +7 -2
  61. package/src/sources/EvmHyperSyncSource.res +2 -0
  62. package/src/sources/EvmHyperSyncSource.res.mjs +2 -2
  63. package/src/sources/FuelHyperSyncSource.res +1 -0
  64. package/src/sources/FuelHyperSyncSource.res.mjs +1 -1
  65. package/src/sources/HyperSync.res +4 -0
  66. package/src/sources/HyperSync.res.mjs +4 -2
  67. package/src/sources/HyperSync.resi +1 -0
  68. package/src/sources/HyperSyncClient.res +3 -0
  69. package/src/sources/HyperSyncSSE.res +1 -1
  70. package/src/sources/HyperSyncSSE.res.mjs +4 -10
  71. package/src/sources/RpcSource.res +1 -0
  72. package/src/sources/RpcSource.res.mjs +1 -1
  73. package/src/sources/SimulateSource.res +1 -0
  74. package/src/sources/SimulateSource.res.mjs +1 -1
  75. package/src/sources/Source.res +7 -0
  76. package/src/sources/SourceManager.res +4 -3
  77. package/src/sources/SourceManager.res.mjs +2 -2
  78. package/src/sources/SvmHyperSyncClient.res +5 -0
  79. package/src/sources/SvmHyperSyncSource.res +3 -0
  80. package/src/sources/SvmHyperSyncSource.res.mjs +4 -2
  81. package/src/tui/Tui.res +24 -0
  82. package/src/tui/Tui.res.mjs +18 -0
  83. package/src/tui/components/SyncETA.res +12 -6
  84. package/src/tui/components/SyncETA.res.mjs +12 -8
@@ -3,6 +3,7 @@
3
3
  import * as Pino from "./bindings/Pino.res.mjs";
4
4
  import * as Pino$1 from "pino";
5
5
  import * as Utils from "./Utils.res.mjs";
6
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
6
7
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
8
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
8
9
 
@@ -53,6 +54,24 @@ let logger = {
53
54
  contents: undefined
54
55
  };
55
56
 
57
+ let context = {
58
+ contents: {}
59
+ };
60
+
61
+ function mixin() {
62
+ return Object.assign({}, context.contents);
63
+ }
64
+
65
+ function withoutContext(params) {
66
+ let keys = Object.keys(context.contents);
67
+ if (keys.length === 0) {
68
+ return params;
69
+ }
70
+ let narrowed = Object.assign({}, params);
71
+ keys.forEach(key => Stdlib_Dict.$$delete(narrowed, key));
72
+ return narrowed;
73
+ }
74
+
56
75
  function makeLogger(logStrategy, logFilePath, defaultFileLogLevel, userLogLevel) {
57
76
  let pinoFile_target = "pino/file";
58
77
  let pinoFile_options = Primitive_option.some(Pino.Transport.makeTransportOptions({
@@ -71,17 +90,18 @@ function makeLogger(logStrategy, logFilePath, defaultFileLogLevel, userLogLevel)
71
90
  switch (logStrategy) {
72
91
  case "ecs-file" :
73
92
  let newrecord = {...Pino.ECS.make()};
74
- return Pino$1.pino((newrecord.base = base, newrecord.customLevels = logLevels, newrecord), Pino$1.transport(pinoFile));
93
+ return Pino$1.pino((newrecord.base = base, newrecord.mixin = mixin, newrecord.customLevels = logLevels, newrecord), Pino$1.transport(pinoFile));
75
94
  case "ecs-console" :
76
95
  let newrecord$1 = {...Pino.ECS.make()};
77
- return Pino$1.pino((newrecord$1.base = base, newrecord$1.customLevels = logLevels, newrecord$1.level = userLogLevel, newrecord$1));
96
+ return Pino$1.pino((newrecord$1.base = base, newrecord$1.mixin = mixin, newrecord$1.customLevels = logLevels, newrecord$1.level = userLogLevel, newrecord$1));
78
97
  case "ecs-console-multistream" :
79
98
  let newrecord$2 = {...Pino.ECS.make()};
80
- return makeMultiStreamLogger(undefined, (newrecord$2.base = base, newrecord$2));
99
+ return makeMultiStreamLogger(undefined, (newrecord$2.base = base, newrecord$2.mixin = mixin, newrecord$2));
81
100
  case "file-only" :
82
101
  return Pino$1.pino({
83
102
  level: defaultFileLogLevel,
84
103
  customLevels: logLevels,
104
+ mixin: mixin,
85
105
  base: base
86
106
  }, Pino$1.transport(pinoFile));
87
107
  case "console-raw" :
@@ -89,10 +109,12 @@ function makeLogger(logStrategy, logFilePath, defaultFileLogLevel, userLogLevel)
89
109
  break;
90
110
  case "both-prettyconsole" :
91
111
  return makeMultiStreamLogger(logFilePath, {
112
+ mixin: mixin,
92
113
  base: base
93
114
  });
94
115
  }
95
116
  return makeMultiStreamLogger(undefined, {
117
+ mixin: mixin,
96
118
  base: base
97
119
  });
98
120
  }
@@ -171,11 +193,15 @@ function childFatal(logger, params) {
171
193
  }
172
194
 
173
195
  function createChild(params) {
174
- return getLogger().child(Pino.createChildParams(params));
196
+ return getLogger().child(Pino.createChildParams(withoutContext(params)));
197
+ }
198
+
199
+ function setContext(fields) {
200
+ context.contents = fields;
175
201
  }
176
202
 
177
203
  function createChildFrom(logger, params) {
178
- return logger.child(Pino.createChildParams(params));
204
+ return logger.child(Pino.createChildParams(withoutContext(params)));
179
205
  }
180
206
 
181
207
  function logAtLevel(logger, level, message, params) {
@@ -246,6 +272,7 @@ export {
246
272
  childErrorWithExn,
247
273
  childFatal,
248
274
  createChild,
275
+ setContext,
249
276
  createChildFrom,
250
277
  logAtLevel,
251
278
  noopLogger,
package/src/Main.res CHANGED
@@ -1,80 +1,3 @@
1
- // The public console/state chain shape. Kept to exactly this field set for
2
- // backward compatibility with consumers like RACE — new metric fields stay off
3
- // the HTTP response.
4
- type chainData = {
5
- chainId: ChainId.t,
6
- poweredByHyperSync: bool,
7
- firstEventBlockNumber: option<int>,
8
- latestProcessedBlock: option<int>,
9
- timestampCaughtUpToHeadOrEndblock: option<Date.t>,
10
- numEventsProcessed: float,
11
- latestFetchedBlockNumber: int,
12
- // Need this for API backwards compatibility
13
- @as("currentBlockHeight")
14
- knownHeight: int,
15
- numBatchesFetched: int,
16
- startBlock: int,
17
- endBlock: option<int>,
18
- numAddresses: int,
19
- }
20
- @tag("status")
21
- type state =
22
- | @as("disabled") Disabled({})
23
- | @as("initializing") Initializing({})
24
- | @as("active")
25
- Active({
26
- envioVersion: string,
27
- chains: array<chainData>,
28
- indexerStartTime: Date.t,
29
- isPreRegisteringDynamicContracts: bool,
30
- rollbackOnReorg: bool,
31
- })
32
-
33
- let toChainData = (m: Metrics.chainMetrics): chainData => {
34
- chainId: m.chainId,
35
- poweredByHyperSync: m.poweredByHyperSync,
36
- firstEventBlockNumber: m.firstEventBlockNumber,
37
- latestProcessedBlock: m.latestProcessedBlock,
38
- timestampCaughtUpToHeadOrEndblock: m.timestampCaughtUpToHeadOrEndblock,
39
- numEventsProcessed: m.numEventsProcessed,
40
- latestFetchedBlockNumber: m.latestFetchedBlockNumber,
41
- knownHeight: m.knownHeight,
42
- numBatchesFetched: m.numBatchesFetched,
43
- startBlock: m.startBlock,
44
- endBlock: m.endBlock,
45
- numAddresses: m.numAddresses,
46
- }
47
-
48
- let chainDataSchema = S.schema((s): chainData => {
49
- chainId: s.matches(ChainId.schema),
50
- poweredByHyperSync: s.matches(S.bool),
51
- firstEventBlockNumber: s.matches(S.option(S.int)),
52
- latestProcessedBlock: s.matches(S.option(S.int)),
53
- timestampCaughtUpToHeadOrEndblock: s.matches(S.option(S.datetime(S.string))),
54
- numEventsProcessed: s.matches(S.float),
55
- latestFetchedBlockNumber: s.matches(S.int),
56
- knownHeight: s.matches(S.int),
57
- numBatchesFetched: s.matches(S.int),
58
- startBlock: s.matches(S.int),
59
- endBlock: s.matches(S.option(S.int)),
60
- numAddresses: s.matches(S.int),
61
- })
62
- let stateSchema = S.union([
63
- S.literal(Disabled({})),
64
- S.literal(Initializing({})),
65
- S.schema(s => Active({
66
- envioVersion: s.matches(S.string),
67
- chains: s.matches(S.array(chainDataSchema)),
68
- indexerStartTime: s.matches(S.datetime(S.string)),
69
- // Keep the field, since Dev Console expects it to be present
70
- isPreRegisteringDynamicContracts: false,
71
- rollbackOnReorg: s.matches(S.bool),
72
- })),
73
- ])
74
-
75
- // Runtime state lives in the process-wide `EnvioGlobal` record (shared
76
- // across duplicate envio module instances); the slots are opaque there, so
77
- // cast them to the real types here.
78
1
  let getIndexerState = () =>
79
2
  EnvioGlobal.value.indexerState->(Utils.magic: option<unknown> => option<IndexerState.t>)
80
3
  let setIndexerState = (state: IndexerState.t) =>
@@ -488,111 +411,8 @@ let getGlobalIndexer = (): 'indexer => {
488
411
  Utils.Proxy.make(Utils.Object.createNullObject(), traps)->(Utils.magic: {..} => 'indexer)
489
412
  }
490
413
 
491
- let startServer = (
492
- ~getMetrics: unit => option<Metrics.t>,
493
- ~envioVersion: string,
494
- ~persistence: Persistence.t,
495
- ~isDevelopmentMode: bool,
496
- ) => {
497
- open Express
498
-
499
- let app = make()
500
-
501
- let consoleCorsMiddleware = (req, res, next) => {
502
- switch req.headers->Dict.get("origin") {
503
- | Some(origin) if origin === Env.prodEnvioAppUrl || origin === Env.envioAppUrl =>
504
- res->setHeader("Access-Control-Allow-Origin", origin)
505
- | _ => ()
506
- }
507
-
508
- res->setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
509
- res->setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
510
-
511
- if req.method === Rest.Options {
512
- res->sendStatus(200)
513
- } else {
514
- next()
515
- }
516
- }
517
- app->useFor("/console", consoleCorsMiddleware)
518
- app->useFor("/metrics", consoleCorsMiddleware)
519
- app->useFor("/metrics/runtime", consoleCorsMiddleware)
520
-
521
- app->get("/healthz", (_req, res) => {
522
- // this is the machine readable port used in kubernetes to check the health of this service.
523
- // aditional health information could be added in the future (info about errors, back-offs, etc).
524
- res->sendStatus(200)
525
- })
526
-
527
- app->get("/console/state", (_req, res) => {
528
- let state = if !isDevelopmentMode {
529
- Disabled({})
530
- } else {
531
- switch getMetrics() {
532
- | None => Initializing({})
533
- | Some(metrics) =>
534
- Active({
535
- envioVersion,
536
- chains: metrics.chains->Array.map(toChainData),
537
- indexerStartTime: metrics.startTime,
538
- isPreRegisteringDynamicContracts: false,
539
- rollbackOnReorg: metrics.rollbackEnabled,
540
- })
541
- }
542
- }
543
-
544
- res->json(state->S.reverseConvertToJsonOrThrow(stateSchema))
545
- })
546
-
547
- app->post("/console/syncCache", (_req, res) => {
548
- if isDevelopmentMode {
549
- (persistence->Persistence.getInitializedStorageOrThrow).dumpEffectCache()
550
- ->Promise.thenResolve(_ => res->json(Boolean(true)))
551
- ->Promise.ignore
552
- } else {
553
- res->json(Boolean(false))
554
- }
555
- })
556
-
557
- Metrics.startRuntimeCollectors()
558
-
559
- app->get("/metrics", (_req, res) => {
560
- res->set("Content-Type", Metrics.contentType)
561
- let _ = res->endWithData(Metrics.collect(~metrics=getMetrics()))
562
- })
563
-
564
- app->get("/metrics/runtime", (_req, res) => {
565
- res->set("Content-Type", Metrics.contentType)
566
- let _ = res->endWithData(Metrics.collectRuntime())
567
- })
568
-
569
- let server = app->listen(Env.serverPort)
570
- server->Express.onError(err => {
571
- let code = (err->(Utils.magic: JsExn.t => {..}))["code"]
572
- if code === "EADDRINUSE" {
573
- Logging.error(
574
- `Port ${Env.serverPort->Int.toString} is already in use. To fix this either:` ++
575
- `\n 1. Kill the process using the port: lsof -ti :${Env.serverPort->Int.toString} | xargs kill -9` ++ `\n 2. Use a different port by setting the ENVIO_INDEXER_PORT environment variable: ENVIO_INDEXER_PORT=9899 envio start`,
576
- )
577
- } else {
578
- Logging.errorWithExn(err, "Failed to start indexer server")
579
- }
580
- NodeJs.process->NodeJs.exitWithCode(Failure)
581
- })
582
- }
583
-
584
- type args = {@as("tui-off") tuiOff?: bool}
585
-
586
- type process
587
- @val external process: process = "process"
588
- @get external argv: process => 'a = "argv"
589
-
590
- type mainArgs = Yargs.parsedArgs<args>
591
-
592
414
  // The RPC-stripped public config that the storage layer persists in
593
415
  // `envio_info` (on initialize) and validates against (on resume).
594
- let getEnvioInfo = () => Config.getPublicConfigJson()->Config.stripSensitiveData
595
-
596
416
  let migrate = async (~reset) => {
597
417
  let config = Config.load()
598
418
  let persistence = PgStorage.makePersistenceFromConfig(~config)
@@ -600,7 +420,7 @@ let migrate = async (~reset) => {
600
420
  ~reset,
601
421
  ~chainConfigs=config.chainMap->ChainMap.values,
602
422
  ~contractMapping=config.contractMapping,
603
- ~envioInfo=getEnvioInfo(),
423
+ ~envioInfo=Config.envioInfo(),
604
424
  ~resetCommand="envio local db-migrate setup",
605
425
  ~runCommand=None,
606
426
  ~lowercaseAddresses=config.lowercaseAddresses,
@@ -622,6 +442,118 @@ let dropSchema = async () => {
622
442
  // context, so callers should act on it (exit / re-throw) without logging again.
623
443
  exception FatalError(exn)
624
444
 
445
+ %%private(
446
+ let startIndexer = async (
447
+ ~config: Config.t,
448
+ ~persistence: option<Persistence.t>=?,
449
+ ~reset=false,
450
+ ~isTest=false,
451
+ ~exitAfterFirstEventBlock=false,
452
+ ~patchConfig: option<(Config.t, HandlerRegister.registrationsByChainId) => Config.t>=?,
453
+ ) => {
454
+ // A worker reports to its supervisor, which draws for the whole run.
455
+ let shouldUseTui = Tui.shouldUse(~suppressed=isTest || Worker.isEnabled)
456
+ // In per-chain mode every line this process writes belongs to the chains it
457
+ // drives, whether or not a supervisor split the run across processes.
458
+ config->Config.logContext->Option.forEach(Logging.setContext)
459
+ // isDevelopmentMode controls whether the indexer stays alive after all
460
+ // chains finish (keepProcessAlive) and whether the console API is exposed.
461
+ // Set by `envio dev` via the public config's `isDev` field; `envio start`
462
+ // leaves it false so the process exits cleanly when indexing completes.
463
+ let isDevelopmentMode = !isTest && config.isDev
464
+ // Initialized first so the exported indexer value contains state from the
465
+ // database when handler files are loaded (they may access the indexer at
466
+ // module top level).
467
+ let persistence = switch persistence {
468
+ | Some(p) => p
469
+ | None => PgStorage.makePersistenceFromConfig(~config)
470
+ }
471
+ setGlobalPersistence(persistence)
472
+ await persistence->Persistence.initForRun(
473
+ ~config,
474
+ ~reset,
475
+ ~isDevelopmentMode,
476
+ ~requireInitialized=config.isolated,
477
+ )
478
+
479
+ // Loads user handler files, which register handler/contractRegister/where
480
+ // state into the global `HandlerRegister` registry as a side effect; this
481
+ // returns that state resolved into per-chain registrations. `config` itself
482
+ // is never mutated by registration — it holds only event definitions.
483
+ let registrationsByChainId = await HandlerLoader.registerAllHandlers(~config)
484
+ let config = if isTest {
485
+ {...config, shouldRollbackOnReorg: false}
486
+ } else {
487
+ config
488
+ }
489
+
490
+ let config = switch patchConfig {
491
+ | Some(patchConfig) => patchConfig(config, registrationsByChainId)
492
+ | None => config
493
+ }
494
+ // The single fatal-error handler, invoked once via IndexerState.errorExit.
495
+ // It logs the failure once (with chain context) and rejects the run wrapped in
496
+ // `FatalError` so callers know it's already logged — `Bin.res` just exits, the
497
+ // test worker unwraps and re-throws it to the parent thread. `runUntilFatalError`
498
+ // only ever rejects: on a clean run it stays pending and the process exits via
499
+ // ExitOnCaughtUp / when the indexer loop drains.
500
+ let onErrorReject = ref(None)
501
+ let runUntilFatalError: promise<unit> = Promise.make((_resolve, reject) =>
502
+ onErrorReject := Some(reject)
503
+ )
504
+ // `onErrorReject` is filled synchronously by `Promise.make` above, before the
505
+ // indexer can run and call `onError`, so it's always present here.
506
+ let onError = (errHandler: ErrorHandling.t) => {
507
+ errHandler->ErrorHandling.log
508
+ (onErrorReject.contents->Option.getUnsafe)(FatalError(errHandler.exn->Utils.prettifyExn))
509
+ }
510
+ let envioVersion = Utils.EnvioPackage.value.version
511
+
512
+ let getMetrics = () => getIndexerState()->Option.map(IndexerState.toMetrics)
513
+ let dumpEffectCache = () =>
514
+ (persistence->Persistence.getInitializedStorageOrThrow).dumpEffectCache()
515
+
516
+ // A worker reports through its supervisor, which owns the one server and the
517
+ // one display the run has.
518
+ if !isTest && !Worker.isEnabled {
519
+ Metrics.startRuntimeCollectors()
520
+ Server.startServer(
521
+ ~onSyncCache=() => dumpEffectCache()->Promise.thenResolve(ignore),
522
+ ~collectRuntime=Metrics.collectRuntime,
523
+ ~isDevelopmentMode,
524
+ ~envioVersion,
525
+ ~getMetrics,
526
+ )
527
+ }
528
+
529
+ let state = IndexerState.makeFromDbState(
530
+ ~config,
531
+ ~persistence,
532
+ ~initialState=persistence->Persistence.getInitializedState,
533
+ ~registrationsByChainId,
534
+ ~isDevelopmentMode,
535
+ ~shouldUseTui,
536
+ ~exitAfterFirstEventBlock,
537
+ ~holdRealtime=Worker.config->Option.mapOr(false, worker => worker.holdRealtime),
538
+ ~onError,
539
+ )
540
+ if shouldUseTui {
541
+ let _rerender = Tui.start(~config, ~getMetrics=() => state->IndexerState.toMetrics)
542
+ }
543
+ Worker.bindRun(
544
+ ~getMetrics=() => state->IndexerState.toMetrics,
545
+ ~onReleaseRealtime=() => state->IndexerState.releaseRealtime,
546
+ )
547
+ setIndexerState(state)
548
+ state->IndexerLoop.start
549
+ await runUntilFatalError
550
+ }
551
+ )
552
+
553
+ // Starts this process's part of a run: the group's supervisor when the budget
554
+ // and the schema afford splitting the chains across processes, and the indexer
555
+ // itself otherwise. A worker is already one process's part, so it never splits
556
+ // again — `planForRun` refuses an isolated config.
625
557
  let start = async (
626
558
  ~persistence: option<Persistence.t>=?,
627
559
  ~reset=false,
@@ -629,93 +561,24 @@ let start = async (
629
561
  ~exitAfterFirstEventBlock=false,
630
562
  ~patchConfig: option<(Config.t, HandlerRegister.registrationsByChainId) => Config.t>=?,
631
563
  ) => {
632
- let mainArgs: mainArgs = process->argv->Yargs.hideBin->Yargs.yargs->Yargs.argv
633
- let explicitTui = switch mainArgs.tuiOff {
634
- | Some(off) => Some(!off)
635
- | None => Env.tuiEnvVar
636
- }
637
- let shouldUseTui = switch (isTest, explicitTui) {
638
- | (true, _) => false
639
- | (_, Some(tui)) => tui
640
- | (_, None) => !Envio.isNonInteractive()
641
- }
642
- // Initialize persistence first so the exported indexer value contains state from the database
643
- // when handler files are loaded (they may access the indexer at module top level).
644
- let config = Config.load()
645
- // isDevelopmentMode controls whether the indexer stays alive after all
646
- // chains finish (keepProcessAlive) and whether the console API is exposed.
647
- // Set by `envio dev` via the public config's `isDev` field; `envio start`
648
- // leaves it false so the process exits cleanly when indexing completes.
649
- let isDevelopmentMode = !isTest && config.isDev
650
- let persistence = switch persistence {
651
- | Some(p) => p
652
- | None => PgStorage.makePersistenceFromConfig(~config)
653
- }
654
- setGlobalPersistence(persistence)
655
- await persistence->Persistence.init(
656
- ~reset,
657
- ~chainConfigs=config.chainMap->ChainMap.values,
658
- ~contractMapping=config.contractMapping,
659
- ~envioInfo=getEnvioInfo(),
660
- ~resetCommand=isDevelopmentMode ? "envio dev -r" : "envio start -r",
661
- ~runCommand=Some(isDevelopmentMode ? "envio dev" : "envio start"),
662
- ~lowercaseAddresses=config.lowercaseAddresses,
663
- ~requireInitialized=config.isolated,
564
+ // A worker parses the same config its supervisor did and narrows it to the
565
+ // chains it was handed, rather than being told what to index: the storage it
566
+ // resumes refuses a config that disagrees with the one the run was created
567
+ // from, which is a stronger guarantee than a handover could give.
568
+ Worker.config->Option.forEach(({chainIds}) =>
569
+ Config.prime(Config.getPublicConfigJson()->Config.withIsolatedChains(~chainIds))
664
570
  )
665
-
666
- // Loads user handler files, which register handler/contractRegister/where
667
- // state into the global `HandlerRegister` registry as a side effect; this
668
- // returns that state resolved into per-chain registrations. `config` itself
669
- // is never mutated by registration — it holds only event definitions.
670
- let registrationsByChainId = await HandlerLoader.registerAllHandlers(~config)
671
- let config = if isTest {
672
- {...config, shouldRollbackOnReorg: false}
673
- } else {
674
- config
675
- }
676
-
677
- let config = switch patchConfig {
678
- | Some(patchConfig) => patchConfig(config, registrationsByChainId)
679
- | None => config
680
- }
681
- // The single fatal-error handler, invoked once via IndexerState.errorExit.
682
- // It logs the failure once (with chain context) and rejects the run wrapped in
683
- // `FatalError` so callers know it's already logged — `Bin.res` just exits, the
684
- // test worker unwraps and re-throws it to the parent thread. `runUntilFatalError`
685
- // only ever rejects: on a clean run it stays pending and the process exits via
686
- // ExitOnCaughtUp / when the indexer loop drains.
687
- let onErrorReject = ref(None)
688
- let runUntilFatalError: promise<unit> = Promise.make((_resolve, reject) =>
689
- onErrorReject := Some(reject)
690
- )
691
- // `onErrorReject` is filled synchronously by `Promise.make` above, before the
692
- // indexer can run and call `onError`, so it's always present here.
693
- let onError = (errHandler: ErrorHandling.t) => {
694
- errHandler->ErrorHandling.log
695
- (onErrorReject.contents->Option.getUnsafe)(FatalError(errHandler.exn->Utils.prettifyExn))
696
- }
697
- let envioVersion = Utils.EnvioPackage.value.version
698
-
699
- let getMetrics = () => getIndexerState()->Option.map(IndexerState.toMetrics)
700
-
701
- if !isTest {
702
- startServer(~persistence, ~isDevelopmentMode, ~envioVersion, ~getMetrics)
703
- }
704
-
705
- let state = IndexerState.makeFromDbState(
706
- ~config,
707
- ~persistence,
708
- ~initialState=persistence->Persistence.getInitializedState,
709
- ~registrationsByChainId,
710
- ~isDevelopmentMode,
711
- ~shouldUseTui,
712
- ~exitAfterFirstEventBlock,
713
- ~onError,
714
- )
715
- if shouldUseTui {
716
- let _rerender = Tui.start(~config, ~getMetrics=() => state->IndexerState.toMetrics)
571
+ let config = Config.load()
572
+ switch isTest ? None : Supervisor.planForRun(~config) {
573
+ | Some(workers) => await Supervisor.run(~config, ~workers, ~reset)
574
+ | None =>
575
+ await startIndexer(
576
+ ~config,
577
+ ~persistence?,
578
+ ~reset,
579
+ ~isTest,
580
+ ~exitAfterFirstEventBlock,
581
+ ~patchConfig?,
582
+ )
717
583
  }
718
- setIndexerState(state)
719
- state->IndexerLoop.start
720
- await runUntilFatalError
721
584
  }