envio 3.13.0-rc.0 → 3.14.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 (77) hide show
  1. package/bin.mjs +10 -1
  2. package/index.d.ts +11 -0
  3. package/package.json +8 -20
  4. package/rescript.json +1 -4
  5. package/src/BatchProcessing.res +0 -2
  6. package/src/ChainState.res +39 -25
  7. package/src/ChainState.res.mjs +26 -23
  8. package/src/ChainState.resi +1 -2
  9. package/src/Config.res +69 -100
  10. package/src/Config.res.mjs +50 -63
  11. package/src/Core.res +46 -6
  12. package/src/Core.res.mjs +41 -4
  13. package/src/CrossChainState.res +13 -6
  14. package/src/CrossChainState.res.mjs +17 -4
  15. package/src/EventConfigBuilder.res +4 -10
  16. package/src/EventConfigBuilder.res.mjs +1 -3
  17. package/src/ExitOnCaughtUp.res +5 -1
  18. package/src/ExitOnCaughtUp.res.mjs +8 -3
  19. package/src/FinalizeBackfill.res +16 -10
  20. package/src/FinalizeBackfill.res.mjs +16 -5
  21. package/src/HandlerLoader.res +74 -32
  22. package/src/HandlerLoader.res.mjs +69 -23
  23. package/src/HandlerTypeCheck.mjs +14 -0
  24. package/src/HandlerTypeCheckWorker.mjs +68 -0
  25. package/src/Internal.res +1 -1
  26. package/src/Main.res +17 -12
  27. package/src/Main.res.mjs +9 -3
  28. package/src/NodeVersion.mjs +13 -0
  29. package/src/Persistence.res +79 -38
  30. package/src/Persistence.res.mjs +41 -10
  31. package/src/PgStorage.res +62 -34
  32. package/src/PgStorage.res.mjs +65 -31
  33. package/src/ResumePlan.res +172 -0
  34. package/src/ResumePlan.res.mjs +122 -0
  35. package/src/Supervisor.res +8 -8
  36. package/src/Supervisor.res.mjs +6 -1
  37. package/src/TestIndexer.res +19 -6
  38. package/src/TestIndexer.res.mjs +5 -4
  39. package/src/TsModuleHooks.mjs +121 -0
  40. package/src/Utils.res +0 -8
  41. package/src/Worker.res +8 -0
  42. package/src/Worker.res.mjs +11 -0
  43. package/src/bindings/NodeJs.res +8 -7
  44. package/src/bindings/NodeJs.res.mjs +0 -3
  45. package/src/bindings/Pino.res +2 -2
  46. package/src/db/InternalTable.res +52 -2
  47. package/src/db/InternalTable.res.mjs +32 -3
  48. package/src/sources/FuelHyperSync.res +2 -0
  49. package/src/sources/FuelHyperSync.res.mjs +1 -0
  50. package/src/sources/FuelHyperSync.resi +1 -0
  51. package/src/sources/FuelHyperSyncClient.res +25 -8
  52. package/src/sources/FuelHyperSyncClient.res.mjs +8 -2
  53. package/src/sources/FuelHyperSyncSource.res +24 -22
  54. package/src/sources/FuelHyperSyncSource.res.mjs +18 -13
  55. package/src/tui/InitApi.res +85 -0
  56. package/src/tui/{components/CustomHooks.res.mjs → InitApi.res.mjs} +11 -64
  57. package/src/tui/Tui.res +142 -249
  58. package/src/tui/Tui.res.mjs +123 -332
  59. package/src/Js.shim.ts +0 -11
  60. package/src/bindings/DateFns.res +0 -74
  61. package/src/bindings/DateFns.res.mjs +0 -22
  62. package/src/sources/FuelSDK.res +0 -38
  63. package/src/sources/FuelSDK.res.mjs +0 -27
  64. package/src/sources/vendored-fuel-abi-coder.js +0 -1792
  65. package/src/tui/bindings/Ink.res +0 -369
  66. package/src/tui/bindings/Ink.res.mjs +0 -66
  67. package/src/tui/bindings/Style.res +0 -123
  68. package/src/tui/bindings/Style.res.mjs +0 -2
  69. package/src/tui/components/BufferedProgressBar.res +0 -40
  70. package/src/tui/components/BufferedProgressBar.res.mjs +0 -57
  71. package/src/tui/components/CustomHooks.res +0 -122
  72. package/src/tui/components/Messages.res +0 -40
  73. package/src/tui/components/Messages.res.mjs +0 -71
  74. package/src/tui/components/SyncETA.res +0 -179
  75. package/src/tui/components/SyncETA.res.mjs +0 -249
  76. package/src/tui/components/TuiData.res +0 -98
  77. package/src/tui/components/TuiData.res.mjs +0 -73
@@ -0,0 +1,172 @@
1
+ // A chain as its storage holds it: what its `envio_chains` row keeps of its
2
+ // config, and the addresses the config declared for it.
3
+ type storedChain = {
4
+ id: ChainId.t,
5
+ ecosystem: string,
6
+ startBlock: int,
7
+ endBlock: option<int>,
8
+ maxReorgDepth: int,
9
+ configAddresses: array<AddressRows.row>,
10
+ }
11
+
12
+ // What a storage holds of the config it was built from. `envioInfo` is `None`
13
+ // for a storage an older envio built, which kept no such record.
14
+ type stored = {
15
+ envioInfo: option<JSON.t>,
16
+ chains: array<storedChain>,
17
+ contractMapping: ContractMapping.t,
18
+ }
19
+
20
+ type t =
21
+ | Resume
22
+ // Configured since the storage was built, and the only chain its
23
+ // `envio start --chain` process drives.
24
+ | AddChain(Config.chain)
25
+ | Incompatible(array<string>)
26
+
27
+ // The part of a chain's config its storage keeps, shaped so a stored chain and
28
+ // a configured one diff by the path config.yaml spells: `chains.137.endBlock`,
29
+ // `chains.137.contracts.Token.addresses.0x…`.
30
+ let chainEntry = (
31
+ ~startBlock: option<int>,
32
+ ~endBlock: option<int>,
33
+ ~maxReorgDepth: int,
34
+ ~addresses: array<(string, string)>,
35
+ ) => {
36
+ let contracts = Dict.make()
37
+ addresses->Array.forEach(((contractName, address)) =>
38
+ contracts->Utils.Dict.push(contractName, address)
39
+ )
40
+ let entry = dict{
41
+ "maxReorgDepth": JSON.Number(maxReorgDepth->Int.toFloat),
42
+ "contracts": JSON.Object(
43
+ contracts->Dict.mapValues(addresses => JSON.Object(
44
+ dict{
45
+ "addresses": JSON.Object(
46
+ addresses->Array.map(address => (address, JSON.Boolean(true)))->Dict.fromArray,
47
+ ),
48
+ },
49
+ )),
50
+ ),
51
+ }
52
+ startBlock->Option.forEach(startBlock =>
53
+ entry->Dict.set("startBlock", JSON.Number(startBlock->Int.toFloat))
54
+ )
55
+ endBlock->Option.forEach(endBlock =>
56
+ entry->Dict.set("endBlock", JSON.Number(endBlock->Int.toFloat))
57
+ )
58
+ JSON.Object(entry)
59
+ }
60
+
61
+ let withChains = (envioInfo: JSON.t, ~chains: array<(ChainId.t, JSON.t)>) =>
62
+ switch envioInfo {
63
+ | Object(fields) =>
64
+ let joined = fields->Dict.copy
65
+ joined->Dict.set(
66
+ "chains",
67
+ JSON.Object(
68
+ chains->Array.map(((chainId, entry)) => (chainId->ChainId.toString, entry))->Dict.fromArray,
69
+ ),
70
+ )
71
+ JSON.Object(joined)
72
+ | _ => envioInfo
73
+ }
74
+
75
+ // `envioInfo` is the running config's, `chainConfigs` the chains this process
76
+ // drives. An isolated process answers for `envioInfo` and its own chains only:
77
+ // the ones it leaves out are checked, and added, by the processes that drive
78
+ // them.
79
+ let make = (
80
+ ~stored: stored,
81
+ ~envioInfo: JSON.t,
82
+ ~chainConfigs: array<Config.chain>,
83
+ ~contractMapping: ContractMapping.t,
84
+ ~lowercaseAddresses: bool,
85
+ ~isolated: bool,
86
+ ) =>
87
+ switch stored.envioInfo {
88
+ | None => Incompatible(["storage was initialized by an older envio version"])
89
+ | Some(storedEnvioInfo) =>
90
+ let storedChainOf = chainId => stored.chains->Array.find(chain => chain.id == chainId)
91
+ let added = switch (isolated, chainConfigs) {
92
+ | (true, [chain]) if storedChainOf(chain.id)->Option.isNone => Some(chain)
93
+ | _ => None
94
+ }
95
+ let storedChains = isolated
96
+ ? stored.chains->Array.filter(storedChain =>
97
+ chainConfigs->Array.some(chain => chain.id == storedChain.id)
98
+ )
99
+ : stored.chains
100
+ let storedEntries = storedChains->Array.map(storedChain => {
101
+ let addresses =
102
+ storedChain.configAddresses
103
+ ->AddressRows.render(~ecosystem=storedChain.ecosystem, ~shouldChecksum=!lowercaseAddresses)
104
+ ->Array.mapWithIndex((address, idx) => (
105
+ stored.contractMapping->ContractMapping.nameOfOrThrow(
106
+ (storedChain.configAddresses->Array.getUnsafe(idx)).contractId,
107
+ ),
108
+ address->Address.toString,
109
+ ))
110
+ (
111
+ storedChain.id,
112
+ chainEntry(
113
+ ~startBlock=Some(storedChain.startBlock),
114
+ ~endBlock=storedChain.endBlock,
115
+ ~maxReorgDepth=storedChain.maxReorgDepth,
116
+ ~addresses,
117
+ ),
118
+ )
119
+ })
120
+ let currentEntries = chainConfigs->Array.filterMap(chain =>
121
+ switch added {
122
+ | Some(added) if added.id == chain.id => None
123
+ | _ =>
124
+ Some((
125
+ chain.id,
126
+ chainEntry(
127
+ // `latest` is whatever it resolved to when the chain was first
128
+ // stored, so it matches any stored block.
129
+ ~startBlock=switch chain.startBlock {
130
+ | Block(startBlock) => Some(startBlock)
131
+ | Latest => storedChainOf(chain.id)->Option.map(storedChain => storedChain.startBlock)
132
+ },
133
+ ~endBlock=chain.endBlock,
134
+ ~maxReorgDepth=chain.maxReorgDepth,
135
+ ~addresses=chain.contracts->Array.flatMap(contract =>
136
+ contract.addresses->Array.map(address => (contract.name, address->Address.toString))
137
+ ),
138
+ ),
139
+ ))
140
+ }
141
+ )
142
+ let changedPaths = Config.diffPaths(
143
+ ~stored=storedEnvioInfo->withChains(~chains=storedEntries),
144
+ ~current=envioInfo->withChains(~chains=currentEntries),
145
+ )
146
+ let changedPaths =
147
+ stored.contractMapping->ContractMapping.isEqual(contractMapping)
148
+ ? changedPaths
149
+ : changedPaths->Array.concat(["contracts"])
150
+ switch (changedPaths, added) {
151
+ | ([], None) => Resume
152
+ | ([], Some(chain)) => AddChain(chain)
153
+ | (changedPaths, _) => Incompatible(changedPaths)
154
+ }
155
+ }
156
+
157
+ let incompatibleMessage = (
158
+ changedPaths,
159
+ ~envioInfo: JSON.t,
160
+ ~resetCommand: string,
161
+ ~runCommand: option<string>,
162
+ ) => {
163
+ let hasClickhouse = switch envioInfo {
164
+ | Object(d) =>
165
+ switch d->Dict.get("storage") {
166
+ | Some(Object(s)) => s->Dict.get("clickhouse") == Some(Boolean(true))
167
+ | _ => false
168
+ }
169
+ | _ => false
170
+ }
171
+ Config.incompatibleMessage(changedPaths, ~resetCommand, ~runCommand, ~hasClickhouse)
172
+ }
@@ -0,0 +1,122 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Utils from "./Utils.res.mjs";
4
+ import * as Config from "./Config.res.mjs";
5
+ import * as ChainId from "./ChainId.res.mjs";
6
+ import * as AddressRows from "./AddressRows.res.mjs";
7
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
8
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
9
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
10
+ import * as ContractMapping from "./ContractMapping.res.mjs";
11
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
12
+
13
+ function chainEntry(startBlock, endBlock, maxReorgDepth, addresses) {
14
+ let contracts = {};
15
+ addresses.forEach(param => Utils.Dict.push(contracts, param[0], param[1]));
16
+ let entry = {
17
+ maxReorgDepth: maxReorgDepth,
18
+ contracts: Stdlib_Dict.mapValues(contracts, addresses => ({
19
+ addresses: Object.fromEntries(addresses.map(address => [
20
+ address,
21
+ true
22
+ ]))
23
+ }))
24
+ };
25
+ Stdlib_Option.forEach(startBlock, startBlock => {
26
+ entry["startBlock"] = startBlock;
27
+ });
28
+ Stdlib_Option.forEach(endBlock, endBlock => {
29
+ entry["endBlock"] = endBlock;
30
+ });
31
+ return entry;
32
+ }
33
+
34
+ function withChains(envioInfo, chains) {
35
+ if (typeof envioInfo !== "object" || envioInfo === null || Array.isArray(envioInfo)) {
36
+ return envioInfo;
37
+ }
38
+ let joined = Object.assign({}, envioInfo);
39
+ joined["chains"] = Object.fromEntries(chains.map(param => [
40
+ ChainId.toString(param[0]),
41
+ param[1]
42
+ ]));
43
+ return joined;
44
+ }
45
+
46
+ function make(stored, envioInfo, chainConfigs, contractMapping, lowercaseAddresses, isolated) {
47
+ let storedEnvioInfo = stored.envioInfo;
48
+ if (storedEnvioInfo === undefined) {
49
+ return {
50
+ TAG: "Incompatible",
51
+ _0: ["storage was initialized by an older envio version"]
52
+ };
53
+ }
54
+ let storedChainOf = chainId => stored.chains.find(chain => Primitive_object.equal(chain.id, chainId));
55
+ let added;
56
+ if (isolated && chainConfigs.length === 1) {
57
+ let chain = chainConfigs[0];
58
+ added = Stdlib_Option.isNone(storedChainOf(chain.id)) ? chain : undefined;
59
+ } else {
60
+ added = undefined;
61
+ }
62
+ let storedChains = isolated ? stored.chains.filter(storedChain => chainConfigs.some(chain => Primitive_object.equal(chain.id, storedChain.id))) : stored.chains;
63
+ let storedEntries = storedChains.map(storedChain => {
64
+ let addresses = AddressRows.render(storedChain.configAddresses, storedChain.ecosystem, !lowercaseAddresses).map((address, idx) => [
65
+ ContractMapping.nameOfOrThrow(stored.contractMapping, storedChain.configAddresses[idx].contractId),
66
+ address
67
+ ]);
68
+ return [
69
+ storedChain.id,
70
+ chainEntry(storedChain.startBlock, storedChain.endBlock, storedChain.maxReorgDepth, addresses)
71
+ ];
72
+ });
73
+ let currentEntries = Stdlib_Array.filterMap(chainConfigs, chain => {
74
+ if (added !== undefined && Primitive_object.equal(added.id, chain.id)) {
75
+ return;
76
+ }
77
+ let startBlock = chain.startBlock;
78
+ let tmp;
79
+ tmp = startBlock === "latest" ? Stdlib_Option.map(storedChainOf(chain.id), storedChain => storedChain.startBlock) : startBlock;
80
+ return [
81
+ chain.id,
82
+ chainEntry(tmp, chain.endBlock, chain.maxReorgDepth, chain.contracts.flatMap(contract => contract.addresses.map(address => [
83
+ contract.name,
84
+ address
85
+ ])))
86
+ ];
87
+ });
88
+ let changedPaths = Config.diffPaths(withChains(storedEnvioInfo, storedEntries), withChains(envioInfo, currentEntries));
89
+ let changedPaths$1 = ContractMapping.isEqual(stored.contractMapping, contractMapping) ? changedPaths : changedPaths.concat(["contracts"]);
90
+ if (changedPaths$1.length !== 0) {
91
+ return {
92
+ TAG: "Incompatible",
93
+ _0: changedPaths$1
94
+ };
95
+ } else if (added !== undefined) {
96
+ return {
97
+ TAG: "AddChain",
98
+ _0: added
99
+ };
100
+ } else {
101
+ return "Resume";
102
+ }
103
+ }
104
+
105
+ function incompatibleMessage(changedPaths, envioInfo, resetCommand, runCommand) {
106
+ let hasClickhouse;
107
+ if (typeof envioInfo === "object" && envioInfo !== null && !Array.isArray(envioInfo)) {
108
+ let match = envioInfo["storage"];
109
+ hasClickhouse = typeof match === "object" && match !== null && !Array.isArray(match) ? Primitive_object.equal(match["clickhouse"], true) : false;
110
+ } else {
111
+ hasClickhouse = false;
112
+ }
113
+ return Config.incompatibleMessage(changedPaths, resetCommand, runCommand, hasClickhouse);
114
+ }
115
+
116
+ export {
117
+ chainEntry,
118
+ withChains,
119
+ make,
120
+ incompatibleMessage,
121
+ }
122
+ /* Utils Not a pure module */
@@ -183,7 +183,7 @@ let fork = (
183
183
  )
184
184
  if pipeOutput {
185
185
  // The supervisor writes a worker's lines the way it writes its own, which is
186
- // the only way ink can keep them out of its frame. Each stream keeps the one
186
+ // the only way the display can print them above its frame. Each stream keeps the one
187
187
  // it was written to, so a worker's errors stay on stderr for whoever is
188
188
  // redirecting it.
189
189
  [
@@ -430,12 +430,12 @@ let run = async (~config: Config.t, ~workers: array<worker>, ~reset) => {
430
430
  // same initialization an unsplit run does, and the supervisor hands the
431
431
  // connections it used to its workers.
432
432
  let persistence = PgStorage.makePersistenceFromConfig(~config)
433
- await persistence->Persistence.initForRun(
434
- ~config,
435
- ~reset,
436
- ~isDevelopmentMode=config.isDev,
437
- ~requireInitialized=false,
438
- )
433
+ // Checked here once, so the workers don't each check the same handlers.
434
+ let autoLoadFiles = await HandlerLoader.getAutoLoadFiles(~config)
435
+ let _ = await Promise.all2((
436
+ persistence->Persistence.initForRun(~config, ~reset, ~isDevelopmentMode=config.isDev),
437
+ HandlerLoader.typeCheck(~config, ~autoLoadFiles),
438
+ ))
439
439
  await persistence.storage.close()
440
440
 
441
441
  let startTime = Date.make()
@@ -508,7 +508,7 @@ let run = async (~config: Config.t, ~workers: array<worker>, ~reset) => {
508
508
  )
509
509
 
510
510
  if shouldUseTui {
511
- let _rerender = Tui.start(~config, ~getMetrics=() =>
511
+ Tui.start(~config, ~getMetrics=() =>
512
512
  switch reported() {
513
513
  | [] => {...[]->merge, chains: configuredChains(config)}
514
514
  | snapshots => snapshots->merge
@@ -15,6 +15,7 @@ import * as PgStorage from "./PgStorage.res.mjs";
15
15
  import * as Performance from "./bindings/Performance.res.mjs";
16
16
  import * as Persistence from "./Persistence.res.mjs";
17
17
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
18
+ import * as HandlerLoader from "./HandlerLoader.res.mjs";
18
19
  import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
19
20
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
20
21
  import * as Child_process from "child_process";
@@ -367,7 +368,11 @@ function configuredChains(config) {
367
368
 
368
369
  async function run(config, workers, reset) {
369
370
  let persistence = PgStorage.makePersistenceFromConfig(config, undefined);
370
- await Persistence.initForRun(persistence, config, reset, config.isDev, false);
371
+ let autoLoadFiles = await HandlerLoader.getAutoLoadFiles(config);
372
+ await Promise.all([
373
+ Persistence.initForRun(persistence, config, reset, config.isDev),
374
+ HandlerLoader.typeCheck(config, autoLoadFiles)
375
+ ]);
371
376
  await persistence.storage.close();
372
377
  let startTime = new Date();
373
378
  let startTimeRef = Performance.now();
@@ -254,13 +254,16 @@ let makeInitialState = (
254
254
  addressRowsByChain
255
255
  ->Utils.Dict.dangerouslyGetNonOption(chainIdStr)
256
256
  ->Option.getOr(AddressRows.emptySeedRows())
257
+ // The pinned startBlock resumes the chain rather than moving its start, so
258
+ // contract start blocks and onBlock intervals stay what config.yaml says.
259
+ // https://github.com/enviodev/hyperindex/issues/1656
257
260
  {
258
261
  Persistence.id: chain,
259
- startBlock: processChainConfig.startBlock,
262
+ startBlock: config.chainMap->ChainMap.get(chain)->Config.startBlockOrZero,
260
263
  endBlock: processChainConfig.endBlock,
261
264
  sourceBlockNumber: processChainConfig.endBlock->Option.getOr(0),
262
265
  maxReorgDepth: 0, // No reorg support in test indexer
263
- progressBlockNumber: -1,
266
+ progressBlockNumber: processChainConfig.startBlock - 1,
264
267
  progressBlockTime: None,
265
268
  numEventsProcessed: 0.,
266
269
  firstEventBlockNumber: None,
@@ -272,7 +275,6 @@ let makeInitialState = (
272
275
  {
273
276
  cleanRun: true,
274
277
  contractMapping,
275
- envioInfo: Some(JSON.Encode.object(Dict.make())),
276
278
  cache: Dict.make(),
277
279
  chains,
278
280
  checkpointFrontier: Frontier.empty(),
@@ -564,7 +566,7 @@ let makeInMemoryStorage = (~state: testIndexerState): Persistence.storage => {
564
566
  isInitialized: async () => true,
565
567
  // The runner injects the config-derived initial state by setting
566
568
  // `persistence.storageStatus = Ready(...)` directly, bypassing `Persistence.init`,
567
- // so neither of these is reached.
569
+ // so none of these is reached.
568
570
  initialize: async (
569
571
  ~chainConfigs as _=?,
570
572
  ~entities as _=?,
@@ -575,7 +577,15 @@ let makeInMemoryStorage = (~state: testIndexerState): Persistence.storage => {
575
577
  JsError.throwWithMessage(
576
578
  "TestIndexer: initialize should not be called; the initial state is derived from config.",
577
579
  ),
578
- resumeInitialState: async (~entities as _, ~chainIds as _, ~throwIfIncompatible as _) =>
580
+ readStoredConfig: async () =>
581
+ JsError.throwWithMessage(
582
+ "TestIndexer: readStoredConfig should not be called; the initial state is derived from config.",
583
+ ),
584
+ addChain: async (~chainConfig as _, ~entities as _, ~contractMapping as _) =>
585
+ JsError.throwWithMessage(
586
+ "TestIndexer: addChain should not be called; the initial state is derived from config.",
587
+ ),
588
+ resumeInitialState: async (~entities as _, ~chainIds as _, ~contractMapping as _) =>
579
589
  JsError.throwWithMessage(
580
590
  "TestIndexer: resumeInitialState should not be called; the initial state is derived from config.",
581
591
  ),
@@ -663,7 +673,10 @@ let getRegistrations = (~config) =>
663
673
  switch registrationsRef.contents {
664
674
  | Some(promise) => promise
665
675
  | None =>
666
- let promise = HandlerLoader.registerAllHandlers(~config)
676
+ let promise =
677
+ HandlerLoader.getAutoLoadFiles(~config)->Promise.then(autoLoadFiles =>
678
+ HandlerLoader.registerAllHandlers(~config, ~autoLoadFiles)
679
+ )
667
680
  registrationsRef := Some(promise)
668
681
  promise
669
682
  }
@@ -180,10 +180,10 @@ function makeInitialState(config, processConfigChains, addressRowsByChain, contr
180
180
  let addressRows = Stdlib_Option.getOr(addressRowsByChain[chainIdStr], AddressRows.emptySeedRows());
181
181
  return {
182
182
  id: chain,
183
- startBlock: processChainConfig.startBlock,
183
+ startBlock: Config.startBlockOrZero(ChainMap.get(config.chainMap, chain)),
184
184
  endBlock: processChainConfig.endBlock,
185
185
  maxReorgDepth: 0,
186
- progressBlockNumber: -1,
186
+ progressBlockNumber: processChainConfig.startBlock - 1 | 0,
187
187
  progressBlockTime: undefined,
188
188
  numEventsProcessed: 0,
189
189
  firstEventBlockNumber: undefined,
@@ -195,7 +195,6 @@ function makeInitialState(config, processConfigChains, addressRowsByChain, contr
195
195
  return {
196
196
  cleanRun: true,
197
197
  contractMapping: contractMapping,
198
- envioInfo: {},
199
198
  cache: {},
200
199
  chains: chains,
201
200
  checkpointFrontier: Frontier.empty(),
@@ -388,6 +387,8 @@ function makeInMemoryStorage(state) {
388
387
  name: "test-inmemory",
389
388
  isInitialized: async () => true,
390
389
  initialize: async (param, param$1, param$2, param$3, param$4) => Stdlib_JsError.throwWithMessage("TestIndexer: initialize should not be called; the initial state is derived from config."),
390
+ readStoredConfig: async () => Stdlib_JsError.throwWithMessage("TestIndexer: readStoredConfig should not be called; the initial state is derived from config."),
391
+ addChain: async (param, param$1, param$2) => Stdlib_JsError.throwWithMessage("TestIndexer: addChain should not be called; the initial state is derived from config."),
391
392
  resumeInitialState: async (param, param$1, param$2) => Stdlib_JsError.throwWithMessage("TestIndexer: resumeInitialState should not be called; the initial state is derived from config."),
392
393
  loadOrThrow: async (filter, table) => handleLoad(state, table.tableName, filter),
393
394
  ensureQueryIndexes: async (param, param$1, param$2) => {},
@@ -427,7 +428,7 @@ function getRegistrations(config) {
427
428
  if (promise !== undefined) {
428
429
  return promise;
429
430
  }
430
- let promise$1 = HandlerLoader.registerAllHandlers(config);
431
+ let promise$1 = HandlerLoader.getAutoLoadFiles(config).then(autoLoadFiles => HandlerLoader.registerAllHandlers(config, autoLoadFiles));
431
432
  registrationsRef.contents = promise$1;
432
433
  return promise$1;
433
434
  }
@@ -0,0 +1,121 @@
1
+ import module from "node:module";
2
+ import { fileURLToPath } from "node:url";
3
+ import { unsupportedNodeMessage } from "./NodeVersion.mjs";
4
+
5
+ const TS_URL = /\.[cm]?tsx?$/;
6
+ const JSON_URL = /\.json($|\?)/;
7
+ const NOT_FOUND_CODES = new Set([
8
+ "ERR_MODULE_NOT_FOUND",
9
+ "ERR_PACKAGE_PATH_NOT_EXPORTED",
10
+ "ERR_UNSUPPORTED_DIR_IMPORT",
11
+ ]);
12
+
13
+ let registered = false;
14
+
15
+ // Node's own resolver still resolves every candidate the addon offers, so
16
+ // package exports and conditions behave as they do without the hooks.
17
+ export const register = (addon) => {
18
+ if (registered) return;
19
+ registered = true;
20
+
21
+ // The CLI checks this first thing; a test indexer gets here without it.
22
+ const unsupported = unsupportedNodeMessage();
23
+ if (unsupported !== undefined) throw new Error(unsupported);
24
+
25
+ // Node ignores the source maps `loadTs` inlines unless this is on.
26
+ module.setSourceMapsSupport(true);
27
+
28
+ // What legacy decorators lower to; tslib's `__decorate`, `__param` and
29
+ // `__metadata`.
30
+ const helpers = (globalThis.babelHelpers ??= {});
31
+ // A function rather than an arrow: the argument count tells a class
32
+ // decorator from a member's.
33
+ helpers.decorate ??= function (decorators, target, key, descriptor) {
34
+ const argumentCount = arguments.length;
35
+ let result =
36
+ argumentCount < 3
37
+ ? target
38
+ : descriptor === null
39
+ ? (descriptor = Object.getOwnPropertyDescriptor(target, key))
40
+ : descriptor;
41
+ for (let index = decorators.length - 1; index >= 0; index--) {
42
+ const decorator = decorators[index];
43
+ if (decorator) {
44
+ result =
45
+ (argumentCount < 3
46
+ ? decorator(result)
47
+ : argumentCount > 3
48
+ ? decorator(target, key, result)
49
+ : decorator(target, key)) || result;
50
+ }
51
+ }
52
+ if (argumentCount > 3 && result) Object.defineProperty(target, key, result);
53
+ return result;
54
+ };
55
+ helpers.decorateParam ??= (index, decorator) => (target, key) => decorator(target, key, index);
56
+ helpers.decorateMetadata ??= (key, value) =>
57
+ typeof Reflect === "object" && typeof Reflect.metadata === "function"
58
+ ? Reflect.metadata(key, value)
59
+ : undefined;
60
+
61
+ const firstResolved = (candidates, context, nextResolve) => {
62
+ for (const candidate of candidates) {
63
+ try {
64
+ return nextResolve(candidate, context);
65
+ } catch (error) {
66
+ if (!NOT_FOUND_CODES.has(error?.code)) throw error;
67
+ }
68
+ }
69
+ return undefined;
70
+ };
71
+
72
+ module.registerHooks({
73
+ resolve(specifier, context, nextResolve) {
74
+ // Handlers are ES modules, so `require()` only comes from dependencies,
75
+ // including the one Node makes to read a CommonJS module's named
76
+ // exports. It resolves as it would without the hooks.
77
+ if (specifier.startsWith("node:") || context.conditions?.includes("require")) {
78
+ return nextResolve(specifier, context);
79
+ }
80
+ const [path, query] = specifier.split("?");
81
+ let resolved = firstResolved(
82
+ addon.tsResolveCandidates(path, context.parentURL ?? null),
83
+ context,
84
+ nextResolve
85
+ );
86
+ if (resolved === undefined) {
87
+ try {
88
+ resolved = nextResolve(path, context);
89
+ } catch (error) {
90
+ resolved =
91
+ NOT_FOUND_CODES.has(error?.code) &&
92
+ firstResolved(
93
+ addon.tsNotFoundCandidates(error.code, error.url ?? null, error.message),
94
+ context,
95
+ nextResolve
96
+ );
97
+ if (!resolved) throw error;
98
+ }
99
+ }
100
+ const url = query ? `${resolved.url}?${query}` : resolved.url;
101
+ // TypeScript's `resolveJsonModule` imports JSON without the
102
+ // `type: "json"` attribute Node requires.
103
+ if (JSON_URL.test(url) && context.importAttributes?.type === undefined) {
104
+ return {
105
+ ...resolved,
106
+ url,
107
+ importAttributes: { ...context.importAttributes, type: "json" },
108
+ shortCircuit: true,
109
+ };
110
+ }
111
+ return { ...resolved, url, shortCircuit: true };
112
+ },
113
+
114
+ load(url, context, nextLoad) {
115
+ if (!url.startsWith("file:") || !TS_URL.test(new URL(url).pathname)) {
116
+ return nextLoad(url, context);
117
+ }
118
+ return { format: "module", source: addon.loadTs(fileURLToPath(url)), shortCircuit: true };
119
+ },
120
+ });
121
+ };
package/src/Utils.res CHANGED
@@ -10,14 +10,6 @@ external toMethod: 'a => 'a = "%unsafe_to_method"
10
10
 
11
11
  @val external importPath: string => promise<unknown> = "import"
12
12
 
13
- @val
14
- external importPathWithJson: (
15
- string,
16
- @as(json`{with: {type: "json"}}`) _,
17
- ) => promise<{
18
- "default": JSON.t,
19
- }> = "import"
20
-
21
13
  // Half the delay fixed, half spread across it. Every indexer pointed at one
22
14
  // provider loses its stream, or gives up on a quiet chain, in the same instant
23
15
  // that provider blinks, and putting them all back on the same schedule is how a
package/src/Worker.res CHANGED
@@ -82,6 +82,14 @@ let bindToSupervisor = () => {
82
82
 
83
83
  %%private(let send = (message: workerMessage) => NodeJs.Process.sendToParent(message)->ignore)
84
84
 
85
+ // A worker done with its chains exits only once its final snapshot is on the
86
+ // channel: the last periodic one can be up to an interval old, and the
87
+ // supervisor would go on reporting those chains as it left them.
88
+ let exitAfterReporting = (~metrics: Metrics.t) =>
89
+ NodeJs.Process.sendToParentThen(Snapshot({metrics, runtime: Metrics.sampleRuntime()}), () =>
90
+ NodeJs.process->NodeJs.exitWithCode(Success)
91
+ )->ignore
92
+
85
93
  // Reports this process's chains and its own runtime for as long as it runs, so
86
94
  // the supervisor can merge every worker's into the one snapshot the run serves,
87
95
  // and listens for the one decision the supervisor makes on the run's behalf.
@@ -51,6 +51,16 @@ function bindToSupervisor() {
51
51
  });
52
52
  }
53
53
 
54
+ function exitAfterReporting(metrics) {
55
+ process.send({
56
+ kind: "snapshot",
57
+ metrics: metrics,
58
+ runtime: Metrics.sampleRuntime()
59
+ }, () => {
60
+ Process.exit(0);
61
+ });
62
+ }
63
+
54
64
  function bindRun(getMetrics, onReleaseRealtime) {
55
65
  if (isEnabled) {
56
66
  Metrics.startRuntimeCollectors();
@@ -76,6 +86,7 @@ export {
76
86
  config,
77
87
  isEnabled,
78
88
  bindToSupervisor,
89
+ exitAfterReporting,
79
90
  bindRun,
80
91
  }
81
92
  /* configSchema Not a pure module */
@@ -69,9 +69,17 @@ module Process = {
69
69
  // `process` rather than off a namespace import, which would drop the
70
70
  // receiver Node's own implementations read.
71
71
  @val @scope("process") external sendToParent: 'msg => bool = "send"
72
+ // The callback runs once the message is handed to the channel.
73
+ @val @scope("process") external sendToParentThen: ('msg, unit => unit) => bool = "send"
72
74
  @val @scope("process")
73
75
  external onMessage: (@as("message") _, 'msg => unit) => unit = "on"
74
76
  @val @scope("process") external onSignal: (string, unit => unit) => unit = "on"
77
+ @val @scope("process") external offSignal: (string, unit => unit) => unit = "removeListener"
78
+ @val @scope("process") external listenerCount: string => int = "listenerCount"
79
+ @val @scope("process") external pid: int = "pid"
80
+ @val @scope("process") external kill: (int, string) => bool = "kill"
81
+ @val @scope("process")
82
+ external onExit: (@as("exit") _, unit => unit) => unit = "on"
75
83
  // Present only in a process forked with an IPC channel.
76
84
  @val @scope("process") external channel: Nullable.t<unknown> = "channel"
77
85
  @val @scope("process")
@@ -205,17 +213,10 @@ module Url = {
205
213
  module ImportMeta = {
206
214
  type t = {url: Url.t}
207
215
  @val external importMeta: t = "import.meta"
208
- // Get import.meta.url as string for module registration
209
- @val external url: string = "import.meta.url"
210
216
  // Resolve module specifier to file:// URL
211
217
  @val external resolve: string => string = "import.meta.resolve"
212
218
  }
213
219
 
214
- module Module = {
215
- // Register ESM loader hooks (e.g., for TypeScript support via tsx)
216
- @module("node:module") external register: (string, string) => unit = "register"
217
- }
218
-
219
220
  module Path = {
220
221
  type t
221
222