envio 3.13.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 (69) 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/Config.res +69 -100
  6. package/src/Config.res.mjs +50 -63
  7. package/src/Core.res +46 -6
  8. package/src/Core.res.mjs +41 -4
  9. package/src/EventConfigBuilder.res +4 -10
  10. package/src/EventConfigBuilder.res.mjs +1 -3
  11. package/src/ExitOnCaughtUp.res +5 -1
  12. package/src/ExitOnCaughtUp.res.mjs +8 -3
  13. package/src/HandlerLoader.res +74 -32
  14. package/src/HandlerLoader.res.mjs +69 -23
  15. package/src/HandlerTypeCheck.mjs +14 -0
  16. package/src/HandlerTypeCheckWorker.mjs +68 -0
  17. package/src/Internal.res +1 -1
  18. package/src/Main.res +17 -12
  19. package/src/Main.res.mjs +9 -3
  20. package/src/NodeVersion.mjs +13 -0
  21. package/src/Persistence.res +79 -38
  22. package/src/Persistence.res.mjs +41 -10
  23. package/src/PgStorage.res +62 -34
  24. package/src/PgStorage.res.mjs +65 -31
  25. package/src/ResumePlan.res +172 -0
  26. package/src/ResumePlan.res.mjs +122 -0
  27. package/src/Supervisor.res +8 -8
  28. package/src/Supervisor.res.mjs +6 -1
  29. package/src/TestIndexer.res +14 -4
  30. package/src/TestIndexer.res.mjs +3 -2
  31. package/src/TsModuleHooks.mjs +121 -0
  32. package/src/Utils.res +0 -8
  33. package/src/Worker.res +8 -0
  34. package/src/Worker.res.mjs +11 -0
  35. package/src/bindings/NodeJs.res +8 -7
  36. package/src/bindings/NodeJs.res.mjs +0 -3
  37. package/src/bindings/Pino.res +2 -2
  38. package/src/db/InternalTable.res +52 -2
  39. package/src/db/InternalTable.res.mjs +32 -3
  40. package/src/sources/FuelHyperSync.res +2 -0
  41. package/src/sources/FuelHyperSync.res.mjs +1 -0
  42. package/src/sources/FuelHyperSync.resi +1 -0
  43. package/src/sources/FuelHyperSyncClient.res +25 -8
  44. package/src/sources/FuelHyperSyncClient.res.mjs +8 -2
  45. package/src/sources/FuelHyperSyncSource.res +24 -22
  46. package/src/sources/FuelHyperSyncSource.res.mjs +18 -13
  47. package/src/tui/InitApi.res +85 -0
  48. package/src/tui/{components/CustomHooks.res.mjs → InitApi.res.mjs} +11 -64
  49. package/src/tui/Tui.res +142 -249
  50. package/src/tui/Tui.res.mjs +123 -332
  51. package/src/Js.shim.ts +0 -11
  52. package/src/bindings/DateFns.res +0 -74
  53. package/src/bindings/DateFns.res.mjs +0 -22
  54. package/src/sources/FuelSDK.res +0 -38
  55. package/src/sources/FuelSDK.res.mjs +0 -27
  56. package/src/sources/vendored-fuel-abi-coder.js +0 -1792
  57. package/src/tui/bindings/Ink.res +0 -369
  58. package/src/tui/bindings/Ink.res.mjs +0 -66
  59. package/src/tui/bindings/Style.res +0 -123
  60. package/src/tui/bindings/Style.res.mjs +0 -2
  61. package/src/tui/components/BufferedProgressBar.res +0 -40
  62. package/src/tui/components/BufferedProgressBar.res.mjs +0 -57
  63. package/src/tui/components/CustomHooks.res +0 -122
  64. package/src/tui/components/Messages.res +0 -40
  65. package/src/tui/components/Messages.res.mjs +0 -71
  66. package/src/tui/components/SyncETA.res +0 -179
  67. package/src/tui/components/SyncETA.res.mjs +0 -249
  68. package/src/tui/components/TuiData.res +0 -98
  69. package/src/tui/components/TuiData.res.mjs +0 -73
package/src/Core.res.mjs CHANGED
@@ -6,6 +6,7 @@ import * as Nodepath from "node:path";
6
6
  import * as Stdlib_Null from "@rescript/runtime/lib/es6/Stdlib_Null.js";
7
7
  import * as Nodemodule from "node:module";
8
8
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
9
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
9
10
  import * as Nodechild_process from "node:child_process";
10
11
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
11
12
 
@@ -97,6 +98,29 @@ let loadDevAddon = (function(req, envioDir) {
97
98
 
98
99
  let rethrow = (function(e) { throw e });
99
100
 
101
+ let requirePackageJson = ((req, p) => req(p));
102
+
103
+ let devVersion = "0.0.1-dev";
104
+
105
+ let isGlibc = (() => Boolean(process.report?.getReport().header.glibcVersionRuntime));
106
+
107
+ function addCommand() {
108
+ let ua = process.env.npm_config_user_agent;
109
+ if (ua !== undefined) {
110
+ if (ua.startsWith("pnpm/")) {
111
+ return "pnpm add";
112
+ } else if (ua.startsWith("yarn/")) {
113
+ return "yarn add";
114
+ } else if (ua.startsWith("bun/")) {
115
+ return "bun add";
116
+ } else {
117
+ return "npm install";
118
+ }
119
+ } else {
120
+ return "npm install";
121
+ }
122
+ }
123
+
100
124
  function loadAddon() {
101
125
  let req = Nodemodule.createRequire(import.meta.url);
102
126
  let match = process.platform;
@@ -161,12 +185,21 @@ function loadAddon() {
161
185
  if (addon !== undefined) {
162
186
  return addon;
163
187
  }
164
- let addon$1 = loadDevAddon(req, envioPackageDir);
165
- if (addon$1 !== undefined) {
166
- return addon$1;
188
+ let version = requirePackageJson(req, "../package.json").version;
189
+ let devAddon = version === devVersion ? Primitive_option.fromNull(loadDevAddon(req, envioPackageDir)) : undefined;
190
+ if (devAddon !== undefined) {
191
+ return devAddon;
167
192
  }
168
193
  let host = process.platform + `-` + process.arch;
169
- return Stdlib_JsError.throwWithMessage(candidates.length === 0 ? `envio doesn't support ` + host + `. Supported: linux-x64 (glibc/musl), linux-arm64, darwin-x64, darwin-arm64.` : `Couldn't load the envio native addon for ` + host + `. Reinstall envio (ensure optional dependencies aren't skipped).`);
194
+ let tmp;
195
+ if (candidates.length !== 0) {
196
+ let pkg = process.platform === "linux" && process.arch === "x64" && !isGlibc() ? `envio-linux-x64-musl` : candidates[0];
197
+ tmp = `envio's native binary for ` + host + ` isn't installed (package "` + pkg + `"). This happens when optional dependencies are skipped (--omit=optional, --no-optional) or the lockfile was generated on another platform.
198
+ Reinstall dependencies, or add the package explicitly: ` + addCommand() + ` ` + pkg + `@` + version;
199
+ } else {
200
+ tmp = process.platform === "win32" ? `envio doesn't run natively on Windows. Use WSL 2 instead: https://learn.microsoft.com/windows/wsl/install` : `envio doesn't support ` + host + `. Supported: linux-x64 (glibc/musl), linux-arm64, darwin-x64, darwin-arm64.`;
201
+ }
202
+ return Stdlib_JsError.throwWithMessage(tmp);
170
203
  }
171
204
 
172
205
  let addonRef = {
@@ -211,6 +244,10 @@ export {
211
244
  envioPackageDir,
212
245
  loadDevAddon,
213
246
  rethrow,
247
+ requirePackageJson,
248
+ devVersion,
249
+ isGlibc,
250
+ addCommand,
214
251
  loadAddon,
215
252
  addonRef,
216
253
  getAddon,
@@ -343,7 +343,7 @@ let validateFieldsShapeOrThrow = (
343
343
  }
344
344
 
345
345
  let selectionList = (fields: unknown, key: string): option<array<string>> =>
346
- switch (fields->(Utils.magic: unknown => dict<unknown>))->Dict.get(key) {
346
+ switch fields->(Utils.magic: unknown => dict<unknown>)->Dict.get(key) {
347
347
  | None => None
348
348
  | Some(value) => Some(value->(Utils.magic: unknown => array<string>))
349
349
  }
@@ -371,8 +371,8 @@ let parseFieldsOrThrow = (
371
371
  if !(valid->Utils.Set.has(name)) {
372
372
  JsError.throwWithMessage(
373
373
  `Invalid "${name}" field in the fields.${kind} option of ${registration}. Valid ${kind} fields: ${Utils.Array.quotedJoin(
374
- valid->Utils.Set.toArray,
375
- )}.`,
374
+ valid->Utils.Set.toArray,
375
+ )}.`,
376
376
  )
377
377
  }
378
378
  if seen->Utils.Set.has(name) {
@@ -858,13 +858,7 @@ let buildFuelEventConfig = (
858
858
  ~rawAbi: JSON.t,
859
859
  ): Internal.fuelEventConfig => {
860
860
  let fuelKind = switch kind {
861
- | "logData" =>
862
- // Transpile raw Fuel ABI to the format expected by the vendored ABI coder
863
- let abi = FuelSDK.transpileAbi(rawAbi)
864
- Internal.LogData({
865
- logId: sighash,
866
- decode: FuelSDK.Receipt.getLogDataDecoder(~abi, ~logId=sighash),
867
- })
861
+ | "logData" => Internal.LogData({logId: sighash, abi: rawAbi})
868
862
  | "mint" => Mint
869
863
  | "burn" => Burn
870
864
  | "transfer" => Transfer
@@ -6,7 +6,6 @@ import * as Core from "./Core.res.mjs";
6
6
  import * as Fuel from "./sources/Fuel.res.mjs";
7
7
  import * as Utils from "./Utils.res.mjs";
8
8
  import * as Address from "./Address.res.mjs";
9
- import * as FuelSDK from "./sources/FuelSDK.res.mjs";
10
9
  import * as Internal from "./Internal.res.mjs";
11
10
  import * as LogSelection from "./LogSelection.res.mjs";
12
11
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
@@ -590,11 +589,10 @@ function buildFuelEventConfig(contractName, eventName, kind, sighash, rawAbi) {
590
589
  fuelKind = "Call";
591
590
  break;
592
591
  case "logData" :
593
- let abi = FuelSDK.transpileAbi(rawAbi);
594
592
  fuelKind = {
595
593
  TAG: "LogData",
596
594
  logId: sighash,
597
- decode: FuelSDK.Receipt.getLogDataDecoder(abi, sighash)
595
+ abi: rawAbi
598
596
  };
599
597
  break;
600
598
  case "mint" :
@@ -14,7 +14,11 @@ let run = async (state: IndexerState.t) => {
14
14
  | Some(onExit) => onExit()
15
15
  | None =>
16
16
  Logging.info("Exiting with success")
17
- NodeJs.process->NodeJs.exitWithCode(Success)
17
+ if Worker.isEnabled {
18
+ Worker.exitAfterReporting(~metrics=state->IndexerState.toMetrics)
19
+ } else {
20
+ NodeJs.process->NodeJs.exitWithCode(Success)
21
+ }
18
22
  }
19
23
  | Some(message) => state->IndexerState.errorExit(ErrorHandling.make(Utils.Error.make(message)))
20
24
  }
@@ -1,5 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Worker from "./Worker.res.mjs";
3
4
  import * as Logging from "./Logging.res.mjs";
4
5
  import * as Writing from "./Writing.res.mjs";
5
6
  import * as Process from "process";
@@ -24,12 +25,16 @@ async function run(state) {
24
25
  return onExit();
25
26
  } else {
26
27
  Logging.info("Exiting with success");
27
- Process.exit(0);
28
- return;
28
+ if (Worker.isEnabled) {
29
+ return Worker.exitAfterReporting(IndexerState.toMetrics(state));
30
+ } else {
31
+ Process.exit(0);
32
+ return;
33
+ }
29
34
  }
30
35
  }
31
36
 
32
37
  export {
33
38
  run,
34
39
  }
35
- /* Logging Not a pure module */
40
+ /* Worker Not a pure module */
@@ -1,20 +1,37 @@
1
1
  @module("node:fs/promises")
2
2
  external globIterator: string => Utils.asyncIterator<string> = "glob"
3
3
 
4
- // Register tsx for TypeScript handler support
5
- // Wrapped in try-catch because if tsx is already loaded via --import (e.g., in tests),
6
- // calling module.register again will throw an error
7
- try {
8
- NodeJs.Module.register("tsx/esm", NodeJs.ImportMeta.url)
9
- } catch {
10
- | _ => () // tsx already loaded, ignore
4
+ type tsHooksAddon = {
5
+ loadTs: string => string,
6
+ tsResolveCandidates: (string, Null.t<string>) => array<string>,
7
+ tsNotFoundCandidates: (string, Null.t<string>, string) => array<string>,
11
8
  }
12
9
 
13
- // Convert a relative path to a file:// URL for dynamic import
14
- // Paths are resolved relative to process.cwd() (project root)
15
- let toImportUrl = (relativePath: string) => {
16
- let absolutePath = NodeJs.Path.resolve([NodeJs.Process.cwd(), relativePath])->NodeJs.Path.toString
17
- NodeJs.Url.pathToFileURL(absolutePath)->NodeJs.Url.toString
10
+ @module("./TsModuleHooks.mjs")
11
+ external registerTsHooksWith: tsHooksAddon => unit = "register"
12
+
13
+ // The resolve hook calls into the addon, and loading the addon goes through
14
+ // the resolve hook, so the addon has to be loaded before the hooks exist.
15
+ let registerTsHooks = () => {
16
+ let addon = Core.getAddon()
17
+ registerTsHooksWith({
18
+ loadTs: addon.loadTs,
19
+ tsResolveCandidates: addon.tsResolveCandidates,
20
+ tsNotFoundCandidates: addon.tsNotFoundCandidates,
21
+ })
22
+ }
23
+
24
+ let isTypeScript = file => /\.m?tsx?$/->RegExp.test(file)
25
+
26
+ // Paths are relative to the project root, which is the working directory.
27
+ let toAbsolutePath = (file: string) =>
28
+ NodeJs.Path.resolve([NodeJs.Process.cwd(), file])->NodeJs.Path.toString
29
+
30
+ let importHandler = async file => {
31
+ if isTypeScript(file) {
32
+ Core.getAddon().tsCheckHandlerFormat(toAbsolutePath(file))
33
+ }
34
+ await Utils.importPath(toAbsolutePath(file)->NodeJs.Url.pathToFileURL->NodeJs.Url.toString)
18
35
  }
19
36
 
20
37
  let registerContractHandlers = async (~contractName, ~handler: option<string>) => {
@@ -22,7 +39,7 @@ let registerContractHandlers = async (~contractName, ~handler: option<string>) =
22
39
  | None => ()
23
40
  | Some(handlerPath) =>
24
41
  try {
25
- let _ = await Utils.importPath(toImportUrl(handlerPath))
42
+ let _ = await importHandler(handlerPath)
26
43
  } catch {
27
44
  | exn =>
28
45
  let cause = exn->Utils.prettifyExn->Obj.magic
@@ -37,10 +54,9 @@ let registerContractHandlers = async (~contractName, ~handler: option<string>) =
37
54
  }
38
55
  }
39
56
 
40
- let autoLoadFromSrcHandlers = async (~handlers: string) => {
41
- // Relative to cwd (project root)
42
- let srcPattern = `./${handlers}/**/*.{js,mjs,ts}`
43
- let handlerFiles = try {
57
+ let getAutoLoadFiles = async (~config: Config.t) => {
58
+ let srcPattern = `./${config.handlers}/**/*.{js,mjs,ts}`
59
+ try {
44
60
  let iterator = globIterator(srcPattern)
45
61
  let files = await iterator->Utils.Array.fromAsyncIterator
46
62
  // Filter out test and spec files
@@ -54,22 +70,38 @@ let autoLoadFromSrcHandlers = async (~handlers: string) => {
54
70
  } catch {
55
71
  | exn =>
56
72
  JsError.throwWithMessage(
57
- `Failed to glob src/handlers directory for auto-loading handlers. Pattern: ${srcPattern}. Before continuing, check that you're using Node.js >=22 version. Error: ${exn
73
+ `Failed to glob src/handlers directory for auto-loading handlers. Pattern: ${srcPattern}. Error: ${exn
58
74
  ->Utils.prettifyExn
59
75
  ->Obj.magic}`,
60
76
  )
61
77
  }
78
+ }
62
79
 
63
- // Import handler files using absolute file:// URLs resolved from cwd
64
- let _ = await handlerFiles
65
- ->Array.map(file => {
66
- Utils.importPath(toImportUrl(file))->Promise.catch(exn => {
67
- let cause = exn->Utils.prettifyExn->Obj.magic
68
- Logging.errorWithExn(exn, `Failed to auto-load handler file: ${file}`)
69
- JsError.throwWithMessage(`Failed to auto-load handler file: ${file}. Cause: ${cause}`)
70
- })
71
- })
72
- ->Promise.all
80
+ type typeCheckResult = {
81
+ skipped?: string,
82
+ errors?: string,
83
+ }
84
+
85
+ @module("./HandlerTypeCheck.mjs")
86
+ external checkTypesInWorker: (~cwd: string, ~files: array<string>) => promise<typeCheckResult> =
87
+ "check"
88
+
89
+ let typeCheck = async (~config: Config.t, ~autoLoadFiles) => {
90
+ // A contract's `handler:` may also be an auto-loaded file.
91
+ let files =
92
+ autoLoadFiles
93
+ ->Array.concat(config.contractHandlers->Array.filterMap(({handler}) => handler))
94
+ ->Array.filter(isTypeScript)
95
+ ->Array.map(toAbsolutePath)
96
+ ->Utils.Set.fromArray
97
+ ->Utils.Set.toArray
98
+ if files->Array.length > 0 {
99
+ switch await checkTypesInWorker(~cwd=NodeJs.Process.cwd(), ~files) {
100
+ | {errors} => JsError.throwWithMessage(`Handler files have type errors:\n\n${errors}`)
101
+ | {skipped} => Logging.warn(skipped)
102
+ | _ => ()
103
+ }
104
+ }
73
105
  }
74
106
 
75
107
  // `Config` holds only event definitions; handler + per-chain `where`
@@ -77,13 +109,23 @@ let autoLoadFromSrcHandlers = async (~handlers: string) => {
77
109
  // `HandlerRegister.finishRegistration`. This loads the user handler files
78
110
  // (populating the global `HandlerRegister` registry as a side effect) and
79
111
  // returns the resulting per-chain registrations.
80
- let registerAllHandlers = async (~config: Config.t): HandlerRegister.registrationsByChainId => {
112
+ let registerAllHandlers = async (
113
+ ~config: Config.t,
114
+ ~autoLoadFiles,
115
+ ): HandlerRegister.registrationsByChainId => {
81
116
  HandlerRegister.startRegistration(~config)
117
+ registerTsHooks()
82
118
 
83
- // Auto-load all .js files from src/handlers directory
84
- await autoLoadFromSrcHandlers(~handlers=config.handlers)
119
+ let _ = await autoLoadFiles
120
+ ->Array.map(file => {
121
+ importHandler(file)->Promise.catch(exn => {
122
+ let cause = exn->Utils.prettifyExn->Obj.magic
123
+ Logging.errorWithExn(exn, `Failed to auto-load handler file: ${file}`)
124
+ JsError.throwWithMessage(`Failed to auto-load handler file: ${file}. Cause: ${cause}`)
125
+ })
126
+ })
127
+ ->Promise.all
85
128
 
86
- // Load contract-specific handlers
87
129
  let _ = await config.contractHandlers
88
130
  ->Array.map(({name, handler}) => {
89
131
  registerContractHandlers(~contractName=name, ~handler)
@@ -1,26 +1,46 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Url from "url";
4
+ import * as Core from "./Core.res.mjs";
4
5
  import * as Path from "path";
5
6
  import * as Utils from "./Utils.res.mjs";
6
7
  import * as Logging from "./Logging.res.mjs";
7
8
  import * as Process from "process";
8
- import * as Nodemodule from "node:module";
9
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
9
10
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
10
11
  import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
11
12
  import * as HandlerRegister from "./HandlerRegister.res.mjs";
12
13
  import * as Promises from "node:fs/promises";
14
+ import * as TsModuleHooksMjs from "./TsModuleHooks.mjs";
13
15
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
16
+ import * as HandlerTypeCheckMjs from "./HandlerTypeCheck.mjs";
14
17
 
15
- try {
16
- Nodemodule.register("tsx/esm", import.meta.url);
17
- } catch (exn) {
18
-
18
+ function registerTsHooksWith(prim) {
19
+ TsModuleHooksMjs.register(prim);
19
20
  }
20
21
 
21
- function toImportUrl(relativePath) {
22
- let absolutePath = Path.resolve(Process.cwd(), relativePath);
23
- return Url.pathToFileURL(absolutePath).toString();
22
+ function registerTsHooks() {
23
+ let addon = Core.getAddon();
24
+ TsModuleHooksMjs.register({
25
+ loadTs: addon.loadTs,
26
+ tsResolveCandidates: addon.tsResolveCandidates,
27
+ tsNotFoundCandidates: addon.tsNotFoundCandidates
28
+ });
29
+ }
30
+
31
+ function isTypeScript(file) {
32
+ return /\.m?tsx?$/.test(file);
33
+ }
34
+
35
+ function toAbsolutePath(file) {
36
+ return Path.resolve(Process.cwd(), file);
37
+ }
38
+
39
+ async function importHandler(file) {
40
+ if (isTypeScript(file)) {
41
+ Core.getAddon().tsCheckHandlerFormat(toAbsolutePath(file));
42
+ }
43
+ return await import(Url.pathToFileURL(toAbsolutePath(file)).toString());
24
44
  }
25
45
 
26
46
  async function registerContractHandlers(contractName, handler) {
@@ -28,7 +48,7 @@ async function registerContractHandlers(contractName, handler) {
28
48
  return;
29
49
  }
30
50
  try {
31
- await import(toImportUrl(handler));
51
+ await importHandler(handler);
32
52
  return;
33
53
  } catch (raw_exn) {
34
54
  let exn = Primitive_exceptions.internalToException(raw_exn);
@@ -38,35 +58,61 @@ async function registerContractHandlers(contractName, handler) {
38
58
  }
39
59
  }
40
60
 
41
- async function autoLoadFromSrcHandlers(handlers) {
42
- let srcPattern = `./` + handlers + `/**/*.{js,mjs,ts}`;
43
- let handlerFiles;
61
+ async function getAutoLoadFiles(config) {
62
+ let srcPattern = `./` + config.handlers + `/**/*.{js,mjs,ts}`;
44
63
  try {
45
64
  let iterator = Promises.glob(srcPattern);
46
65
  let files = await Utils.$$Array.fromAsyncIterator(iterator);
47
- handlerFiles = files.filter(file => !(file.includes(".test.") || file.includes(".spec.") || file.includes("_test.")));
66
+ return files.filter(file => !(file.includes(".test.") || file.includes(".spec.") || file.includes("_test.")));
48
67
  } catch (raw_exn) {
49
68
  let exn = Primitive_exceptions.internalToException(raw_exn);
50
- handlerFiles = Stdlib_JsError.throwWithMessage(`Failed to glob src/handlers directory for auto-loading handlers. Pattern: ` + srcPattern + `. Before continuing, check that you're using Node.js >=22 version. Error: ` + Utils.prettifyExn(exn));
69
+ return Stdlib_JsError.throwWithMessage(`Failed to glob src/handlers directory for auto-loading handlers. Pattern: ` + srcPattern + `. Error: ` + Utils.prettifyExn(exn));
51
70
  }
52
- await Promise.all(handlerFiles.map(file => Stdlib_Promise.$$catch(import(toImportUrl(file)), exn => {
71
+ }
72
+
73
+ function checkTypesInWorker(prim0, prim1) {
74
+ return HandlerTypeCheckMjs.check(prim0, prim1);
75
+ }
76
+
77
+ async function typeCheck(config, autoLoadFiles) {
78
+ let files = Array.from(new Set(autoLoadFiles.concat(Stdlib_Array.filterMap(config.contractHandlers, param => param.handler)).filter(isTypeScript).map(toAbsolutePath)));
79
+ if (files.length === 0) {
80
+ return;
81
+ }
82
+ let match = await HandlerTypeCheckMjs.check(Process.cwd(), files);
83
+ let skipped = match.skipped;
84
+ let errors = match.errors;
85
+ if (errors !== undefined) {
86
+ return Stdlib_JsError.throwWithMessage(`Handler files have type errors:\n\n` + errors);
87
+ } else if (skipped !== undefined) {
88
+ return Logging.warn(skipped);
89
+ } else {
90
+ return;
91
+ }
92
+ }
93
+
94
+ async function registerAllHandlers(config, autoLoadFiles) {
95
+ HandlerRegister.startRegistration(config);
96
+ registerTsHooks();
97
+ await Promise.all(autoLoadFiles.map(file => Stdlib_Promise.$$catch(importHandler(file), exn => {
53
98
  let cause = Utils.prettifyExn(exn);
54
99
  Logging.errorWithExn(exn, `Failed to auto-load handler file: ` + file);
55
100
  return Stdlib_JsError.throwWithMessage(`Failed to auto-load handler file: ` + file + `. Cause: ` + cause);
56
101
  })));
57
- }
58
-
59
- async function registerAllHandlers(config) {
60
- HandlerRegister.startRegistration(config);
61
- await autoLoadFromSrcHandlers(config.handlers);
62
102
  await Promise.all(config.contractHandlers.map(param => registerContractHandlers(param.name, param.handler)));
63
103
  return HandlerRegister.finishRegistration(config);
64
104
  }
65
105
 
66
106
  export {
67
- toImportUrl,
107
+ registerTsHooksWith,
108
+ registerTsHooks,
109
+ isTypeScript,
110
+ toAbsolutePath,
111
+ importHandler,
68
112
  registerContractHandlers,
69
- autoLoadFromSrcHandlers,
113
+ getAutoLoadFiles,
114
+ checkTypesInWorker,
115
+ typeCheck,
70
116
  registerAllHandlers,
71
117
  }
72
- /* Not a pure module */
118
+ /* url Not a pure module */
@@ -0,0 +1,14 @@
1
+ import { Worker } from "node:worker_threads";
2
+
3
+ // The compiler blocks the thread it runs on, so it gets its own and the
4
+ // database setup carries on meanwhile.
5
+ export const check = (cwd, files) =>
6
+ new Promise((resolve, reject) => {
7
+ const worker = new Worker(new URL("./HandlerTypeCheckWorker.mjs", import.meta.url), {
8
+ workerData: { cwd, files },
9
+ });
10
+ worker.once("message", resolve);
11
+ worker.once("error", reject);
12
+ // Settles nothing once the result has arrived.
13
+ worker.once("exit", () => reject(new Error("The handler type check exited without a result.")));
14
+ });
@@ -0,0 +1,68 @@
1
+ import { existsSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import { stripVTControlCharacters } from "node:util";
5
+ import { parentPort, workerData } from "node:worker_threads";
6
+
7
+ const check = ({ cwd, files }) => {
8
+ // The ReScript compiler checks a ReScript project's handlers, so it has no
9
+ // reason to set up TypeScript, and the warning would only be noise there.
10
+ // `rescript.json` at the root is what makes codegen treat it as ReScript.
11
+ const skip = (warning) => (existsSync(path.join(cwd, "rescript.json")) ? {} : { skipped: warning });
12
+
13
+ let ts;
14
+ try {
15
+ // The project's own compiler, so handlers are checked exactly as the
16
+ // user's editor and `tsc` check them.
17
+ ts = createRequire(path.join(cwd, "package.json"))("typescript");
18
+ } catch {
19
+ return skip("Skipped the handler type check: the project doesn't depend on typescript.");
20
+ }
21
+
22
+ const configPath = ts.findConfigFile(cwd, ts.sys.fileExists);
23
+ if (configPath === undefined) {
24
+ return skip(
25
+ "Skipped the handler type check: no tsconfig.json found. Add one to type-check handlers on start, like the one envio init creates."
26
+ );
27
+ }
28
+
29
+ const host = {
30
+ getCanonicalFileName: (file) => file,
31
+ getCurrentDirectory: () => cwd,
32
+ getNewLine: () => "\n",
33
+ };
34
+ const format = (diagnostics) =>
35
+ stripVTControlCharacters(ts.formatDiagnosticsWithColorAndContext(diagnostics, host)).trimEnd();
36
+
37
+ const parsed = ts.getParsedCommandLineOfConfigFile(
38
+ configPath,
39
+ { noEmit: true },
40
+ {
41
+ ...ts.sys,
42
+ onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
43
+ throw new Error(format([diagnostic]));
44
+ },
45
+ }
46
+ );
47
+
48
+ // The tsconfig's files are roots, as tsc makes them, since any of them can
49
+ // declare globals a handler relies on. Only the handlers' errors count.
50
+ const handlerFiles = files.map((file) => path.resolve(cwd, file));
51
+ const program = ts.createProgram({
52
+ rootNames: [...parsed.fileNames, ...handlerFiles],
53
+ options: parsed.options,
54
+ projectReferences: parsed.projectReferences,
55
+ });
56
+
57
+ // Every file is a root, so each has a source file. Without one,
58
+ // `getSemanticDiagnostics` would check the whole program.
59
+ const diagnostics = handlerFiles
60
+ .map((file) => program.getSourceFile(file))
61
+ .flatMap((sourceFile) => [
62
+ ...program.getSyntacticDiagnostics(sourceFile),
63
+ ...program.getSemanticDiagnostics(sourceFile),
64
+ ]);
65
+ return diagnostics.length === 0 ? {} : { errors: format(diagnostics) };
66
+ };
67
+
68
+ parentPort.postMessage(check(workerData));
package/src/Internal.res CHANGED
@@ -491,7 +491,7 @@ type eventConfig = private {
491
491
  }
492
492
 
493
493
  type fuelEventKind =
494
- | LogData({logId: string, decode: string => eventParams})
494
+ | LogData({logId: string, abi: JSON.t})
495
495
  | Mint
496
496
  | Burn
497
497
  | Transfer
package/src/Main.res CHANGED
@@ -411,8 +411,6 @@ let getGlobalIndexer = (): 'indexer => {
411
411
  Utils.Proxy.make(Utils.Object.createNullObject(), traps)->(Utils.magic: {..} => 'indexer)
412
412
  }
413
413
 
414
- // The RPC-stripped public config that the storage layer persists in
415
- // `envio_info` (on initialize) and validates against (on resume).
416
414
  let migrate = async (~reset) => {
417
415
  let config = Config.load()
418
416
  let persistence = PgStorage.makePersistenceFromConfig(~config)
@@ -420,8 +418,8 @@ let migrate = async (~reset) => {
420
418
  ~reset,
421
419
  ~chainConfigs=config.chainMap->ChainMap.values,
422
420
  ~contractMapping=config.contractMapping,
423
- ~envioInfo=Config.envioInfo(),
424
- ~resetCommand="envio local db-migrate setup",
421
+ ~envioInfo=config.envioInfo,
422
+ ~resetCommand="envio local db-migrate setup"->Config.withProjectFlags(~config),
425
423
  ~runCommand=None,
426
424
  ~lowercaseAddresses=config.lowercaseAddresses,
427
425
  // A migration command runs once and exits, with nobody watching it recover:
@@ -466,18 +464,23 @@ exception FatalError(exn)
466
464
  | None => PgStorage.makePersistenceFromConfig(~config)
467
465
  }
468
466
  setGlobalPersistence(persistence)
469
- await persistence->Persistence.initForRun(
470
- ~config,
471
- ~reset,
472
- ~isDevelopmentMode,
473
- ~requireInitialized=config.isolated,
474
- )
467
+ let autoLoadFiles = await HandlerLoader.getAutoLoadFiles(~config)
468
+ // A worker's supervisor has checked the handlers already, and the test
469
+ // indexer leaves type checking to the user's test setup.
470
+ let typeCheck =
471
+ isTest || Worker.isEnabled
472
+ ? Promise.resolve()
473
+ : HandlerLoader.typeCheck(~config, ~autoLoadFiles)
474
+ let _ = await Promise.all2((
475
+ persistence->Persistence.initForRun(~config, ~reset, ~isDevelopmentMode),
476
+ typeCheck,
477
+ ))
475
478
 
476
479
  // Loads user handler files, which register handler/contractRegister/where
477
480
  // state into the global `HandlerRegister` registry as a side effect; this
478
481
  // returns that state resolved into per-chain registrations. `config` itself
479
482
  // is never mutated by registration — it holds only event definitions.
480
- let registrationsByChainId = await HandlerLoader.registerAllHandlers(~config)
483
+ let registrationsByChainId = await HandlerLoader.registerAllHandlers(~config, ~autoLoadFiles)
481
484
  let config = if isTest {
482
485
  {...config, shouldRollbackOnReorg: false}
483
486
  } else {
@@ -535,7 +538,7 @@ exception FatalError(exn)
535
538
  ~onError,
536
539
  )
537
540
  if shouldUseTui {
538
- let _rerender = Tui.start(~config, ~getMetrics=() => state->IndexerState.toMetrics)
541
+ Tui.start(~config, ~getMetrics=() => state->IndexerState.toMetrics)
539
542
  }
540
543
  Worker.bindRun(
541
544
  ~getMetrics=() => state->IndexerState.toMetrics,
@@ -565,6 +568,8 @@ let start = async (
565
568
  Worker.config->Option.forEach(({chainIds, isDev}) =>
566
569
  Config.prime(Config.getPublicConfigJson()->Config.withCommandFields(~chainIds, ~isDev))
567
570
  )
571
+ // An unsupported Node fails here, before the database is touched.
572
+ HandlerLoader.registerTsHooks()
568
573
  let config = Config.load()
569
574
  switch isTest ? None : Supervisor.planForRun(~config) {
570
575
  | Some(workers) => await Supervisor.run(~config, ~workers, ~reset)
package/src/Main.res.mjs CHANGED
@@ -351,7 +351,7 @@ function getGlobalIndexer() {
351
351
  async function migrate(reset) {
352
352
  let config = Config.load();
353
353
  let persistence = PgStorage.makePersistenceFromConfig(config, undefined);
354
- await Persistence.init(persistence, ChainMap.values(config.chainMap), config.contractMapping, Config.envioInfo(), "envio local db-migrate setup", undefined, reset, config.lowercaseAddresses, undefined, undefined, "Once");
354
+ await Persistence.init(persistence, ChainMap.values(config.chainMap), config.contractMapping, config.envioInfo, Config.withProjectFlags("envio local db-migrate setup", config), undefined, reset, config.lowercaseAddresses, undefined, undefined, "Once");
355
355
  return await persistence.storage.close();
356
356
  }
357
357
 
@@ -372,8 +372,13 @@ async function startIndexer(config, persistence, resetOpt, isTestOpt, exitAfterF
372
372
  let isDevelopmentMode = !isTest && config.isDev;
373
373
  let persistence$1 = persistence !== undefined ? persistence : PgStorage.makePersistenceFromConfig(config, undefined);
374
374
  EnvioGlobal.value.persistence = Primitive_option.some(persistence$1);
375
- await Persistence.initForRun(persistence$1, config, reset, isDevelopmentMode, config.isolated);
376
- let registrationsByChainId = await HandlerLoader.registerAllHandlers(config);
375
+ let autoLoadFiles = await HandlerLoader.getAutoLoadFiles(config);
376
+ let typeCheck = isTest || Worker.isEnabled ? Promise.resolve() : HandlerLoader.typeCheck(config, autoLoadFiles);
377
+ await Promise.all([
378
+ Persistence.initForRun(persistence$1, config, reset, isDevelopmentMode),
379
+ typeCheck
380
+ ]);
381
+ let registrationsByChainId = await HandlerLoader.registerAllHandlers(config, autoLoadFiles);
377
382
  let config$1;
378
383
  if (isTest) {
379
384
  let newrecord = {...config};
@@ -417,6 +422,7 @@ async function start(persistence, resetOpt, isTestOpt, exitAfterFirstEventBlockO
417
422
  let isTest = isTestOpt !== undefined ? isTestOpt : false;
418
423
  let exitAfterFirstEventBlock = exitAfterFirstEventBlockOpt !== undefined ? exitAfterFirstEventBlockOpt : false;
419
424
  Stdlib_Option.forEach(Worker.config, param => Config.prime(Config.withCommandFields(Config.getPublicConfigJson(), param.chainIds, param.isDev)));
425
+ HandlerLoader.registerTsHooks();
420
426
  let config = Config.load();
421
427
  let workers = isTest ? undefined : Supervisor.planForRun(config, undefined);
422
428
  if (workers !== undefined) {