envio 3.14.0 → 3.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/evm.schema.json +1 -1
- package/package.json +6 -6
- package/src/HandlerLoader.res +7 -13
- package/src/HandlerLoader.res.mjs +7 -14
- package/src/HandlerTypeCheck.res +207 -0
- package/src/HandlerTypeCheck.res.mjs +262 -0
- package/src/bindings/NodeJs.res +31 -0
- package/src/bindings/NodeJs.res.mjs +3 -0
- package/src/sources/EvmRpcClient.res +2 -0
- package/src/sources/RpcSource.res +17 -4
- package/src/sources/RpcSource.res.mjs +23 -5
- package/src/sources/SourceManager.res +63 -40
- package/src/sources/SourceManager.res.mjs +32 -20
- package/src/HandlerTypeCheck.mjs +0 -14
- package/src/HandlerTypeCheckWorker.mjs +0 -68
package/evm.schema.json
CHANGED
|
@@ -543,7 +543,7 @@
|
|
|
543
543
|
"type": "object",
|
|
544
544
|
"properties": {
|
|
545
545
|
"url": {
|
|
546
|
-
"description": "The RPC endpoint URL.",
|
|
546
|
+
"description": "The RPC endpoint URL. An entry whose url is empty, for example `${RPC_URL:-}` with the variable unset, is ignored.",
|
|
547
547
|
"type": "string"
|
|
548
548
|
},
|
|
549
549
|
"for": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "envio",
|
|
3
|
-
"version": "3.14.
|
|
3
|
+
"version": "3.14.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A latency and sync speed optimized, developer friendly blockchain data indexer.",
|
|
6
6
|
"bin": "./bin.mjs",
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"postgres": "3.4.8"
|
|
55
55
|
},
|
|
56
56
|
"optionalDependencies": {
|
|
57
|
-
"envio-linux-x64": "3.14.
|
|
58
|
-
"envio-linux-x64-musl": "3.14.
|
|
59
|
-
"envio-linux-arm64": "3.14.
|
|
60
|
-
"envio-darwin-x64": "3.14.
|
|
61
|
-
"envio-darwin-arm64": "3.14.
|
|
57
|
+
"envio-linux-x64": "3.14.1",
|
|
58
|
+
"envio-linux-x64-musl": "3.14.1",
|
|
59
|
+
"envio-linux-arm64": "3.14.1",
|
|
60
|
+
"envio-darwin-x64": "3.14.1",
|
|
61
|
+
"envio-darwin-arm64": "3.14.1"
|
|
62
62
|
}
|
|
63
63
|
}
|
package/src/HandlerLoader.res
CHANGED
|
@@ -77,15 +77,6 @@ let getAutoLoadFiles = async (~config: Config.t) => {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
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
80
|
let typeCheck = async (~config: Config.t, ~autoLoadFiles) => {
|
|
90
81
|
// A contract's `handler:` may also be an auto-loaded file.
|
|
91
82
|
let files =
|
|
@@ -96,10 +87,13 @@ let typeCheck = async (~config: Config.t, ~autoLoadFiles) => {
|
|
|
96
87
|
->Utils.Set.fromArray
|
|
97
88
|
->Utils.Set.toArray
|
|
98
89
|
if files->Array.length > 0 {
|
|
99
|
-
switch await
|
|
100
|
-
|
|
|
101
|
-
|
|
102
|
-
|
|
90
|
+
switch await HandlerTypeCheck.check(~cwd=NodeJs.Process.cwd(), ~files) {
|
|
91
|
+
| TypeErrors(errors) =>
|
|
92
|
+
JsError.throwWithMessage(
|
|
93
|
+
`The handler type check found errors. Fix them to start the indexer:\n\n${errors}`,
|
|
94
|
+
)
|
|
95
|
+
| Skipped(warning) => Logging.warn(warning)
|
|
96
|
+
| Passed => ()
|
|
103
97
|
}
|
|
104
98
|
}
|
|
105
99
|
}
|
|
@@ -10,10 +10,10 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
|
10
10
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
11
11
|
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
12
12
|
import * as HandlerRegister from "./HandlerRegister.res.mjs";
|
|
13
|
+
import * as HandlerTypeCheck from "./HandlerTypeCheck.res.mjs";
|
|
13
14
|
import * as Promises from "node:fs/promises";
|
|
14
15
|
import * as TsModuleHooksMjs from "./TsModuleHooks.mjs";
|
|
15
16
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
16
|
-
import * as HandlerTypeCheckMjs from "./HandlerTypeCheck.mjs";
|
|
17
17
|
|
|
18
18
|
function registerTsHooksWith(prim) {
|
|
19
19
|
TsModuleHooksMjs.register(prim);
|
|
@@ -70,24 +70,18 @@ async function getAutoLoadFiles(config) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function checkTypesInWorker(prim0, prim1) {
|
|
74
|
-
return HandlerTypeCheckMjs.check(prim0, prim1);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
73
|
async function typeCheck(config, autoLoadFiles) {
|
|
78
74
|
let files = Array.from(new Set(autoLoadFiles.concat(Stdlib_Array.filterMap(config.contractHandlers, param => param.handler)).filter(isTypeScript).map(toAbsolutePath)));
|
|
79
75
|
if (files.length === 0) {
|
|
80
76
|
return;
|
|
81
77
|
}
|
|
82
|
-
let
|
|
83
|
-
|
|
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 {
|
|
78
|
+
let errors = await HandlerTypeCheck.check(Process.cwd(), files);
|
|
79
|
+
if (typeof errors !== "object") {
|
|
90
80
|
return;
|
|
81
|
+
} else if (errors.TAG === "TypeErrors") {
|
|
82
|
+
return Stdlib_JsError.throwWithMessage(`The handler type check found errors. Fix them to start the indexer:\n\n` + errors._0);
|
|
83
|
+
} else {
|
|
84
|
+
return Logging.warn(errors._0);
|
|
91
85
|
}
|
|
92
86
|
}
|
|
93
87
|
|
|
@@ -111,7 +105,6 @@ export {
|
|
|
111
105
|
importHandler,
|
|
112
106
|
registerContractHandlers,
|
|
113
107
|
getAutoLoadFiles,
|
|
114
|
-
checkTypesInWorker,
|
|
115
108
|
typeCheck,
|
|
116
109
|
registerAllHandlers,
|
|
117
110
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
type result =
|
|
2
|
+
| Passed
|
|
3
|
+
| TypeErrors(string)
|
|
4
|
+
| Skipped(string)
|
|
5
|
+
|
|
6
|
+
let timeoutSeconds = 30
|
|
7
|
+
|
|
8
|
+
// tsc's pretty output reads the same from TypeScript 5 to 7: a diagnostic
|
|
9
|
+
// starts unindented with its location, its code frame and related locations
|
|
10
|
+
// follow indented, and `--listFiles` prints the program's files after the
|
|
11
|
+
// last one, before the closing summary.
|
|
12
|
+
let fileDiagnostic = /^(.+):\d+:\d+ - error TS(\d+): /
|
|
13
|
+
let globalDiagnostic = /^error TS(\d+): /
|
|
14
|
+
let summary = /^Found \d+ errors?\b/
|
|
15
|
+
let thrownError = /^\w*Error: /
|
|
16
|
+
|
|
17
|
+
let lines = output => output->String.replaceAll("\r\n", "\n")->String.split("\n")
|
|
18
|
+
|
|
19
|
+
let rec findUp = (directory, name) => {
|
|
20
|
+
let candidate = NodeJs.Path.resolve([directory, name])->NodeJs.Path.toString
|
|
21
|
+
if NodeJs.Fs.existsSync(candidate) {
|
|
22
|
+
Some(candidate)
|
|
23
|
+
} else {
|
|
24
|
+
let parent = NodeJs.Path.dirname(directory)->NodeJs.Path.toString
|
|
25
|
+
parent === directory ? None : findUp(parent, name)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The `tsc` the project's own scripts run. TypeScript 7 has no JavaScript API
|
|
30
|
+
// to call instead, only this command.
|
|
31
|
+
let resolveTsc = (~cwd) =>
|
|
32
|
+
try {
|
|
33
|
+
let manifest =
|
|
34
|
+
NodeJs.Module.createRequire(
|
|
35
|
+
NodeJs.Path.resolve([cwd, "package.json"])->NodeJs.Path.toString,
|
|
36
|
+
)->NodeJs.Module.resolve("typescript/package.json")
|
|
37
|
+
let bin = switch NodeJs.Fs.readFileSync(manifest)->JSON.parseOrThrow {
|
|
38
|
+
| JSON.Object(fields) =>
|
|
39
|
+
switch fields->Dict.get("bin") {
|
|
40
|
+
| Some(JSON.String(bin)) => Some(bin)
|
|
41
|
+
| Some(JSON.Object(bins)) =>
|
|
42
|
+
switch bins->Dict.get("tsc") {
|
|
43
|
+
| Some(JSON.String(bin)) => Some(bin)
|
|
44
|
+
| _ => None
|
|
45
|
+
}
|
|
46
|
+
| _ => None
|
|
47
|
+
}
|
|
48
|
+
| _ => None
|
|
49
|
+
}
|
|
50
|
+
bin->Option.map(bin =>
|
|
51
|
+
NodeJs.Path.resolve([
|
|
52
|
+
NodeJs.Path.dirname(manifest)->NodeJs.Path.toString,
|
|
53
|
+
bin,
|
|
54
|
+
])->NodeJs.Path.toString
|
|
55
|
+
)
|
|
56
|
+
} catch {
|
|
57
|
+
| _ => None
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type run = {error: option<NodeJs.ChildProcess.execFileError>, output: string}
|
|
61
|
+
|
|
62
|
+
let runTsc = (~tsc, ~cwd, args) =>
|
|
63
|
+
Promise.make((resolve, _) =>
|
|
64
|
+
NodeJs.ChildProcess.execFile(
|
|
65
|
+
NodeJs.Process.process->NodeJs.Process.execPath,
|
|
66
|
+
[tsc]->Array.concat(args),
|
|
67
|
+
{cwd, timeout: timeoutSeconds * 1000, maxBuffer: Float.Constants.positiveInfinity},
|
|
68
|
+
(error, stdout, stderr) =>
|
|
69
|
+
resolve({
|
|
70
|
+
error: error->Null.toOption,
|
|
71
|
+
output: NodeJs.Util.stripVTControlCharacters(stdout ++ stderr),
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
type diagnostic = {file: option<string>, code: int, lines: array<string>}
|
|
77
|
+
|
|
78
|
+
// tsc type-checks nothing after a syntax error, an options error or a global
|
|
79
|
+
// error, and an options error is reported at its line in tsconfig.json.
|
|
80
|
+
let canStopTypeChecking = ({file, code}) =>
|
|
81
|
+
file->Option.isNone || code < 2000 || (code >= 5000 && code < 7000)
|
|
82
|
+
|
|
83
|
+
// Paths compare relative to the project, which on Windows also ignores the
|
|
84
|
+
// case of a drive letter tsc may print differently.
|
|
85
|
+
let parse = (output, ~cwd) => {
|
|
86
|
+
let relative = file =>
|
|
87
|
+
NodeJs.Path.relative(cwd, NodeJs.Path.resolve([cwd, file])->NodeJs.Path.toString)
|
|
88
|
+
let diagnostics = []
|
|
89
|
+
let programFiles = Utils.Set.make()
|
|
90
|
+
let current = ref(None)
|
|
91
|
+
let start = (~file, ~code, line) => {
|
|
92
|
+
let diagnostic = {
|
|
93
|
+
file,
|
|
94
|
+
code: code->Int.fromString->Option.getOr(0),
|
|
95
|
+
lines: [line],
|
|
96
|
+
}
|
|
97
|
+
diagnostics->Array.push(diagnostic)
|
|
98
|
+
current := Some(diagnostic)
|
|
99
|
+
}
|
|
100
|
+
output
|
|
101
|
+
->lines
|
|
102
|
+
->Array.forEach(line =>
|
|
103
|
+
switch (
|
|
104
|
+
fileDiagnostic->RegExp.exec(line)->Option.map(RegExp.Result.matches),
|
|
105
|
+
globalDiagnostic->RegExp.exec(line)->Option.map(RegExp.Result.matches),
|
|
106
|
+
) {
|
|
107
|
+
| (Some([Some(file), Some(code)]), _) => start(~file=Some(relative(file)), ~code, line)
|
|
108
|
+
| (_, Some([Some(code)])) => start(~file=None, ~code, line)
|
|
109
|
+
| _ if NodeJs.Path.isAbsolute(line) =>
|
|
110
|
+
programFiles->Utils.Set.add(relative(line))->ignore
|
|
111
|
+
current := None
|
|
112
|
+
| _ if summary->RegExp.test(line) => current := None
|
|
113
|
+
| _ => current.contents->Option.forEach(diagnostic => diagnostic.lines->Array.push(line))
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
(diagnostics, programFiles)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Exit codes say nothing here: TypeScript 5 and 6 exit with 2 on type errors,
|
|
120
|
+
// 7 with 1, and 1 is also how its launcher fails when the native compiler
|
|
121
|
+
// isn't installed.
|
|
122
|
+
let failure = ({error, output}) =>
|
|
123
|
+
switch error {
|
|
124
|
+
| Some({killed: true}) =>
|
|
125
|
+
`Skipped the handler type check: tsc didn't finish within ${timeoutSeconds->Int.toString} seconds.`
|
|
126
|
+
| _ =>
|
|
127
|
+
let reason =
|
|
128
|
+
output
|
|
129
|
+
->lines
|
|
130
|
+
->Array.find(line => thrownError->RegExp.test(line))
|
|
131
|
+
->Option.getOr(output->String.trim)
|
|
132
|
+
`Skipped the handler type check: tsc failed to run:\n\n${reason}`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let check = async (~cwd, ~files) => {
|
|
136
|
+
// The ReScript compiler checks a ReScript project's handlers, so it has no
|
|
137
|
+
// reason to set up TypeScript, and the warning would only be noise there.
|
|
138
|
+
// `rescript.json` at the root is what makes codegen treat it as ReScript.
|
|
139
|
+
let skip = warning =>
|
|
140
|
+
NodeJs.Fs.existsSync(NodeJs.Path.resolve([cwd, "rescript.json"])->NodeJs.Path.toString)
|
|
141
|
+
? Passed
|
|
142
|
+
: Skipped(warning)
|
|
143
|
+
|
|
144
|
+
switch (resolveTsc(~cwd), findUp(cwd, "tsconfig.json")) {
|
|
145
|
+
| (None, _) =>
|
|
146
|
+
skip(
|
|
147
|
+
"Skipped the handler type check: typescript isn't installed. Add it to devDependencies to type-check handlers on start.",
|
|
148
|
+
)
|
|
149
|
+
| (_, None) =>
|
|
150
|
+
skip(
|
|
151
|
+
"Skipped the handler type check: no tsconfig.json found. Add one, like the one envio init creates, to type-check handlers on start.",
|
|
152
|
+
)
|
|
153
|
+
| (Some(tsc), Some(tsconfig)) =>
|
|
154
|
+
// The project's own `tsc --noEmit`, minus the build info it would write.
|
|
155
|
+
// `explainFiles` would replace the listed files' absolute paths with
|
|
156
|
+
// relative ones and the reasons each is included.
|
|
157
|
+
let run = await runTsc(
|
|
158
|
+
~tsc,
|
|
159
|
+
~cwd,
|
|
160
|
+
[
|
|
161
|
+
"--project",
|
|
162
|
+
tsconfig,
|
|
163
|
+
"--noEmit",
|
|
164
|
+
"--incremental",
|
|
165
|
+
"false",
|
|
166
|
+
"--composite",
|
|
167
|
+
"false",
|
|
168
|
+
"--listFiles",
|
|
169
|
+
"--explainFiles",
|
|
170
|
+
"false",
|
|
171
|
+
"--pretty",
|
|
172
|
+
],
|
|
173
|
+
)
|
|
174
|
+
let (diagnostics, programFiles) = run.output->parse(~cwd)
|
|
175
|
+
let report = diagnostics =>
|
|
176
|
+
diagnostics
|
|
177
|
+
->Array.map(({lines}) => lines->Array.join("\n")->String.trimEnd)
|
|
178
|
+
->Array.join("\n\n")
|
|
179
|
+
let handlers = files->Array.map(file => NodeJs.Path.relative(cwd, file))
|
|
180
|
+
let isHandler = ({file}) => file->Option.mapOr(false, file => handlers->Array.includes(file))
|
|
181
|
+
let errors = diagnostics->Array.filter(isHandler)
|
|
182
|
+
let stopping =
|
|
183
|
+
diagnostics->Array.filter(diagnostic =>
|
|
184
|
+
!isHandler(diagnostic) && canStopTypeChecking(diagnostic)
|
|
185
|
+
)
|
|
186
|
+
let unchecked = handlers->Array.filter(file => !(programFiles->Utils.Set.has(file)))
|
|
187
|
+
if run.error->Option.isSome && diagnostics->Array.length === 0 {
|
|
188
|
+
Skipped(failure(run))
|
|
189
|
+
} else if errors->Array.length > 0 {
|
|
190
|
+
TypeErrors(report(errors))
|
|
191
|
+
} else if stopping->Array.length > 0 {
|
|
192
|
+
let reason = report(stopping)
|
|
193
|
+
Skipped(
|
|
194
|
+
`Skipped the handler type check: tsc can't check the handlers until these errors are fixed:\n\n${reason}`,
|
|
195
|
+
)
|
|
196
|
+
} else if unchecked->Array.length > 0 {
|
|
197
|
+
let names = unchecked->Array.join(", ")
|
|
198
|
+
let tsconfig = NodeJs.Path.relative(cwd, tsconfig)
|
|
199
|
+
let pronoun = unchecked->Array.length === 1 ? "it" : "them"
|
|
200
|
+
Skipped(
|
|
201
|
+
`Skipped the handler type check: ${tsconfig} doesn't include ${names}. Add ${pronoun} to "include" to type-check ${pronoun} on start.`,
|
|
202
|
+
)
|
|
203
|
+
} else {
|
|
204
|
+
Passed
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Fs from "fs";
|
|
4
|
+
import * as Path from "path";
|
|
5
|
+
import * as Util from "util";
|
|
6
|
+
import * as Module from "module";
|
|
7
|
+
import * as Process from "process";
|
|
8
|
+
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
9
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
10
|
+
import * as Child_process from "child_process";
|
|
11
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
12
|
+
|
|
13
|
+
let fileDiagnostic = /^(.+):\d+:\d+ - error TS(\d+): /;
|
|
14
|
+
|
|
15
|
+
let globalDiagnostic = /^error TS(\d+): /;
|
|
16
|
+
|
|
17
|
+
let summary = /^Found \d+ errors?\b/;
|
|
18
|
+
|
|
19
|
+
let thrownError = /^\w*Error: /;
|
|
20
|
+
|
|
21
|
+
function lines(output) {
|
|
22
|
+
return output.replaceAll("\r\n", "\n").split("\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function findUp(_directory, name) {
|
|
26
|
+
while (true) {
|
|
27
|
+
let directory = _directory;
|
|
28
|
+
let candidate = Path.resolve(directory, name);
|
|
29
|
+
if (Fs.existsSync(candidate)) {
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
32
|
+
let parent = Path.dirname(directory);
|
|
33
|
+
if (parent === directory) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
_directory = parent;
|
|
37
|
+
continue;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveTsc(cwd) {
|
|
42
|
+
try {
|
|
43
|
+
let manifest = Module.createRequire(Path.resolve(cwd, "package.json")).resolve("typescript/package.json");
|
|
44
|
+
let fields = JSON.parse(Fs.readFileSync(manifest, "utf8"));
|
|
45
|
+
let bin;
|
|
46
|
+
if (typeof fields === "object" && fields !== null && !Array.isArray(fields)) {
|
|
47
|
+
let match = fields["bin"];
|
|
48
|
+
if (match !== undefined && !(match === null || Array.isArray(match))) {
|
|
49
|
+
switch (typeof match) {
|
|
50
|
+
case "string" :
|
|
51
|
+
bin = match;
|
|
52
|
+
break;
|
|
53
|
+
case "object" :
|
|
54
|
+
let match$1 = match["tsc"];
|
|
55
|
+
bin = typeof match$1 === "string" ? match$1 : undefined;
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
bin = undefined;
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
bin = undefined;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
bin = undefined;
|
|
65
|
+
}
|
|
66
|
+
return Stdlib_Option.map(bin, bin => Path.resolve(Path.dirname(manifest), bin));
|
|
67
|
+
} catch (exn) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function runTsc(tsc, cwd, args) {
|
|
73
|
+
return new Promise((resolve, param) => {
|
|
74
|
+
Child_process.execFile(Process.execPath, [tsc].concat(args), {
|
|
75
|
+
cwd: cwd,
|
|
76
|
+
timeout: 30000,
|
|
77
|
+
maxBuffer: Number.POSITIVE_INFINITY
|
|
78
|
+
}, (error, stdout, stderr) => resolve({
|
|
79
|
+
error: error === null ? undefined : Primitive_option.some(error),
|
|
80
|
+
output: Util.stripVTControlCharacters(stdout + stderr)
|
|
81
|
+
}));
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function canStopTypeChecking(param) {
|
|
86
|
+
let code = param.code;
|
|
87
|
+
if (Stdlib_Option.isNone(param.file) || code < 2000) {
|
|
88
|
+
return true;
|
|
89
|
+
} else if (code >= 5000) {
|
|
90
|
+
return code < 7000;
|
|
91
|
+
} else {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parse(output, cwd) {
|
|
97
|
+
let relative = file => Path.relative(cwd, Path.resolve(cwd, file));
|
|
98
|
+
let diagnostics = [];
|
|
99
|
+
let programFiles = new Set();
|
|
100
|
+
let current = {
|
|
101
|
+
contents: undefined
|
|
102
|
+
};
|
|
103
|
+
let start = (file, code, line) => {
|
|
104
|
+
let diagnostic_code = Stdlib_Option.getOr(Stdlib_Int.fromString(code, undefined), 0);
|
|
105
|
+
let diagnostic_lines = [line];
|
|
106
|
+
let diagnostic = {
|
|
107
|
+
file: file,
|
|
108
|
+
code: diagnostic_code,
|
|
109
|
+
lines: diagnostic_lines
|
|
110
|
+
};
|
|
111
|
+
diagnostics.push(diagnostic);
|
|
112
|
+
current.contents = diagnostic;
|
|
113
|
+
};
|
|
114
|
+
lines(output).forEach(line => {
|
|
115
|
+
let match = Stdlib_Option.map(Primitive_option.fromNullable(fileDiagnostic.exec(line)), prim => prim.slice(1));
|
|
116
|
+
let match$1 = Stdlib_Option.map(Primitive_option.fromNullable(globalDiagnostic.exec(line)), prim => prim.slice(1));
|
|
117
|
+
let exit = 0;
|
|
118
|
+
if (match !== undefined && match.length === 2) {
|
|
119
|
+
let file = match[0];
|
|
120
|
+
if (file !== undefined) {
|
|
121
|
+
let code = match[1];
|
|
122
|
+
if (code !== undefined) {
|
|
123
|
+
return start(relative(file), code, line);
|
|
124
|
+
}
|
|
125
|
+
exit = 2;
|
|
126
|
+
} else {
|
|
127
|
+
exit = 2;
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
exit = 2;
|
|
131
|
+
}
|
|
132
|
+
if (exit === 2 && match$1 !== undefined && match$1.length === 1) {
|
|
133
|
+
let code$1 = match$1[0];
|
|
134
|
+
if (code$1 !== undefined) {
|
|
135
|
+
return start(undefined, code$1, line);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (Path.isAbsolute(line)) {
|
|
139
|
+
programFiles.add(relative(line));
|
|
140
|
+
current.contents = undefined;
|
|
141
|
+
return;
|
|
142
|
+
} else if (summary.test(line)) {
|
|
143
|
+
current.contents = undefined;
|
|
144
|
+
return;
|
|
145
|
+
} else {
|
|
146
|
+
return Stdlib_Option.forEach(current.contents, diagnostic => {
|
|
147
|
+
diagnostic.lines.push(line);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
return [
|
|
152
|
+
diagnostics,
|
|
153
|
+
programFiles
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function failure(param) {
|
|
158
|
+
let output = param.output;
|
|
159
|
+
let error = param.error;
|
|
160
|
+
if (error !== undefined && error.killed) {
|
|
161
|
+
return `Skipped the handler type check: tsc didn't finish within ` + (30).toString() + ` seconds.`;
|
|
162
|
+
}
|
|
163
|
+
let reason = Stdlib_Option.getOr(lines(output).find(line => thrownError.test(line)), output.trim());
|
|
164
|
+
return `Skipped the handler type check: tsc failed to run:\n\n` + reason;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function check(cwd, files) {
|
|
168
|
+
let skip = warning => {
|
|
169
|
+
if (Fs.existsSync(Path.resolve(cwd, "rescript.json"))) {
|
|
170
|
+
return "Passed";
|
|
171
|
+
} else {
|
|
172
|
+
return {
|
|
173
|
+
TAG: "Skipped",
|
|
174
|
+
_0: warning
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
let match = resolveTsc(cwd);
|
|
179
|
+
let match$1 = findUp(cwd, "tsconfig.json");
|
|
180
|
+
if (match === undefined) {
|
|
181
|
+
return skip("Skipped the handler type check: typescript isn't installed. Add it to devDependencies to type-check handlers on start.");
|
|
182
|
+
}
|
|
183
|
+
if (match$1 === undefined) {
|
|
184
|
+
return skip("Skipped the handler type check: no tsconfig.json found. Add one, like the one envio init creates, to type-check handlers on start.");
|
|
185
|
+
}
|
|
186
|
+
let run = await runTsc(match, cwd, [
|
|
187
|
+
"--project",
|
|
188
|
+
match$1,
|
|
189
|
+
"--noEmit",
|
|
190
|
+
"--incremental",
|
|
191
|
+
"false",
|
|
192
|
+
"--composite",
|
|
193
|
+
"false",
|
|
194
|
+
"--listFiles",
|
|
195
|
+
"--explainFiles",
|
|
196
|
+
"false",
|
|
197
|
+
"--pretty"
|
|
198
|
+
]);
|
|
199
|
+
let match$2 = parse(run.output, cwd);
|
|
200
|
+
let programFiles = match$2[1];
|
|
201
|
+
let diagnostics = match$2[0];
|
|
202
|
+
let report = diagnostics => diagnostics.map(param => param.lines.join("\n").trimEnd()).join("\n\n");
|
|
203
|
+
let handlers = files.map(file => Path.relative(cwd, file));
|
|
204
|
+
let isHandler = param => Stdlib_Option.mapOr(param.file, false, file => handlers.includes(file));
|
|
205
|
+
let errors = diagnostics.filter(isHandler);
|
|
206
|
+
let stopping = diagnostics.filter(diagnostic => {
|
|
207
|
+
if (isHandler(diagnostic)) {
|
|
208
|
+
return false;
|
|
209
|
+
} else {
|
|
210
|
+
return canStopTypeChecking(diagnostic);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
let unchecked = handlers.filter(file => !programFiles.has(file));
|
|
214
|
+
if (Stdlib_Option.isSome(run.error) && diagnostics.length === 0) {
|
|
215
|
+
return {
|
|
216
|
+
TAG: "Skipped",
|
|
217
|
+
_0: failure(run)
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (errors.length !== 0) {
|
|
221
|
+
return {
|
|
222
|
+
TAG: "TypeErrors",
|
|
223
|
+
_0: report(errors)
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (stopping.length !== 0) {
|
|
227
|
+
let reason = report(stopping);
|
|
228
|
+
return {
|
|
229
|
+
TAG: "Skipped",
|
|
230
|
+
_0: `Skipped the handler type check: tsc can't check the handlers until these errors are fixed:\n\n` + reason
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (unchecked.length === 0) {
|
|
234
|
+
return "Passed";
|
|
235
|
+
}
|
|
236
|
+
let names = unchecked.join(", ");
|
|
237
|
+
let tsconfig = Path.relative(cwd, match$1);
|
|
238
|
+
let pronoun = unchecked.length === 1 ? "it" : "them";
|
|
239
|
+
return {
|
|
240
|
+
TAG: "Skipped",
|
|
241
|
+
_0: `Skipped the handler type check: ` + tsconfig + ` doesn't include ` + names + `. Add ` + pronoun + ` to "include" to type-check ` + pronoun + ` on start.`
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let timeoutSeconds = 30;
|
|
246
|
+
|
|
247
|
+
export {
|
|
248
|
+
timeoutSeconds,
|
|
249
|
+
fileDiagnostic,
|
|
250
|
+
globalDiagnostic,
|
|
251
|
+
summary,
|
|
252
|
+
thrownError,
|
|
253
|
+
lines,
|
|
254
|
+
findUp,
|
|
255
|
+
resolveTsc,
|
|
256
|
+
runTsc,
|
|
257
|
+
canStopTypeChecking,
|
|
258
|
+
parse,
|
|
259
|
+
failure,
|
|
260
|
+
check,
|
|
261
|
+
}
|
|
262
|
+
/* fs Not a pure module */
|
package/src/bindings/NodeJs.res
CHANGED
|
@@ -39,6 +39,15 @@ module Util = {
|
|
|
39
39
|
@module("util") external inspect: ('a, inspectOptions) => string = "inspect"
|
|
40
40
|
|
|
41
41
|
let inspectObj = a => inspect(a, {showHidden: false, depth: Null, colors: true})
|
|
42
|
+
|
|
43
|
+
@module("util") external stripVTControlCharacters: string => string = "stripVTControlCharacters"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module Module = {
|
|
47
|
+
type require
|
|
48
|
+
|
|
49
|
+
@module("module") external createRequire: string => require = "createRequire"
|
|
50
|
+
@send external resolve: (require, string) => string = "resolve"
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
module Process = {
|
|
@@ -165,6 +174,23 @@ module ChildProcess = {
|
|
|
165
174
|
@module("child_process")
|
|
166
175
|
external execWithOptions: (string, execOptions, callback) => unit = "exec"
|
|
167
176
|
|
|
177
|
+
type execFileOptions = {
|
|
178
|
+
cwd?: string,
|
|
179
|
+
timeout?: int,
|
|
180
|
+
maxBuffer?: float,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// `killed` is set when the timeout stopped the child.
|
|
184
|
+
type execFileError = {killed: bool}
|
|
185
|
+
|
|
186
|
+
@module("child_process")
|
|
187
|
+
external execFile: (
|
|
188
|
+
string,
|
|
189
|
+
array<string>,
|
|
190
|
+
execFileOptions,
|
|
191
|
+
(Null.t<execFileError>, string, string) => unit,
|
|
192
|
+
) => unit = "execFile"
|
|
193
|
+
|
|
168
194
|
// One of a child's stdio slots, present only for a slot the parent asked to
|
|
169
195
|
// pipe rather than inherit.
|
|
170
196
|
module Stream = {
|
|
@@ -225,6 +251,8 @@ module Path = {
|
|
|
225
251
|
|
|
226
252
|
@module("path") external join: (t, string) => t = "join"
|
|
227
253
|
@module("path") external dirname: string => t = "dirname"
|
|
254
|
+
@module("path") external relative: (string, string) => string = "relative"
|
|
255
|
+
@module("path") external isAbsolute: string => bool = "isAbsolute"
|
|
228
256
|
|
|
229
257
|
external toString: t => string = "%identity"
|
|
230
258
|
|
|
@@ -275,6 +303,9 @@ module Fs = {
|
|
|
275
303
|
mode?: int,
|
|
276
304
|
}
|
|
277
305
|
|
|
306
|
+
@module("fs") external existsSync: string => bool = "existsSync"
|
|
307
|
+
@module("fs") external readFileSync: (string, @as("utf8") _) => string = "readFileSync"
|
|
308
|
+
|
|
278
309
|
module Promises = {
|
|
279
310
|
@module("fs") @scope("promises")
|
|
280
311
|
external writeFile: (
|
|
@@ -16,6 +16,8 @@ let Util$1 = {
|
|
|
16
16
|
inspectObj: inspectObj
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
let Module = {};
|
|
20
|
+
|
|
19
21
|
let Process = {};
|
|
20
22
|
|
|
21
23
|
let empty = globalThis.Buffer.alloc(0);
|
|
@@ -59,6 +61,7 @@ let Fs = {
|
|
|
59
61
|
|
|
60
62
|
export {
|
|
61
63
|
Util$1 as Util,
|
|
64
|
+
Module,
|
|
62
65
|
Process,
|
|
63
66
|
Buffer,
|
|
64
67
|
V8,
|
|
@@ -28,6 +28,7 @@ type pageOutcome =
|
|
|
28
28
|
| @as("SuggestedToBlock") SuggestedToBlock
|
|
29
29
|
| @as("Backoff") Backoff
|
|
30
30
|
| @as("FieldSelection") FieldSelection
|
|
31
|
+
| @as("BehindHead") BehindHead
|
|
31
32
|
|
|
32
33
|
type nextPageResult = {
|
|
33
34
|
kind: pageOutcome,
|
|
@@ -45,6 +46,7 @@ type nextPageResult = {
|
|
|
45
46
|
// alongside it is empty.
|
|
46
47
|
type blockHashResult = {
|
|
47
48
|
message: option<string>,
|
|
49
|
+
behindHeadBlock: option<int>,
|
|
48
50
|
requestStats: array<Source.requestStat>,
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -99,7 +99,8 @@ let make = (
|
|
|
99
99
|
|
|
100
100
|
// The cause travels as a real error so it reaches the logs the way every
|
|
101
101
|
// other source's does. Absent where the message is the whole story.
|
|
102
|
-
let exn =
|
|
102
|
+
let exn =
|
|
103
|
+
result.providerMessage->Option.map(message => JsError.make(message)->JsExn.anyToExnInternal)
|
|
103
104
|
|
|
104
105
|
let failedGettingItems = (decision): exn => Source.GetItemsError(
|
|
105
106
|
FailedGettingItems({
|
|
@@ -134,6 +135,16 @@ let make = (
|
|
|
134
135
|
}),
|
|
135
136
|
),
|
|
136
137
|
)
|
|
138
|
+
| BehindHead =>
|
|
139
|
+
throw(
|
|
140
|
+
Source.SourceBehindHead({
|
|
141
|
+
blockNumber: switch result.blockNumber {
|
|
142
|
+
| Some(blockNumber) => blockNumber
|
|
143
|
+
| None => missing("blockNumber")
|
|
144
|
+
},
|
|
145
|
+
requestStats: result.requestStats,
|
|
146
|
+
}),
|
|
147
|
+
)
|
|
137
148
|
| SuggestedToBlock =>
|
|
138
149
|
throw(
|
|
139
150
|
failedGettingItems(
|
|
@@ -185,9 +196,11 @@ let make = (
|
|
|
185
196
|
let getBlockHashes = async (~blockNumbers, ~logger as _) => {
|
|
186
197
|
let (result, pageBlockStore) = await rpcClient.getBlockHashes(blockNumbers)
|
|
187
198
|
{
|
|
188
|
-
Source.result: switch result.message {
|
|
189
|
-
| None => Ok(pageBlockStore)
|
|
190
|
-
| Some(
|
|
199
|
+
Source.result: switch (result.message, result.behindHeadBlock) {
|
|
200
|
+
| (None, _) => Ok(pageBlockStore)
|
|
201
|
+
| (Some(_), Some(blockNumber)) =>
|
|
202
|
+
Error(Source.SourceBehindHead({blockNumber, requestStats: result.requestStats}))
|
|
203
|
+
| (Some(message), None) => Error(JsError.make(message)->JsExn.anyToExnInternal)
|
|
191
204
|
},
|
|
192
205
|
requestStats: result.requestStats,
|
|
193
206
|
}
|
|
@@ -93,6 +93,14 @@ function make(param) {
|
|
|
93
93
|
},
|
|
94
94
|
Error: new Error()
|
|
95
95
|
};
|
|
96
|
+
case "BehindHead" :
|
|
97
|
+
let blockNumber$1 = result.blockNumber;
|
|
98
|
+
throw {
|
|
99
|
+
RE_EXN_ID: Source.SourceBehindHead,
|
|
100
|
+
blockNumber: blockNumber$1 !== undefined ? blockNumber$1 : missing("blockNumber"),
|
|
101
|
+
requestStats: result.requestStats,
|
|
102
|
+
Error: new Error()
|
|
103
|
+
};
|
|
96
104
|
}
|
|
97
105
|
let parsingTimeRef = Performance.now();
|
|
98
106
|
let parsedQueueItems = EvmEventItem.toInternalItems(result.items, onEventRegistrations, chainId);
|
|
@@ -114,12 +122,22 @@ function make(param) {
|
|
|
114
122
|
let getBlockHashes = async (blockNumbers, param) => {
|
|
115
123
|
let match = await rpcClient.getBlockHashes(blockNumbers);
|
|
116
124
|
let result = match[0];
|
|
117
|
-
let
|
|
125
|
+
let match$1 = result.message;
|
|
126
|
+
let match$2 = result.behindHeadBlock;
|
|
118
127
|
return {
|
|
119
|
-
result:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
128
|
+
result: match$1 !== undefined ? (
|
|
129
|
+
match$2 !== undefined ? ({
|
|
130
|
+
TAG: "Error",
|
|
131
|
+
_0: {
|
|
132
|
+
RE_EXN_ID: Source.SourceBehindHead,
|
|
133
|
+
blockNumber: match$2,
|
|
134
|
+
requestStats: result.requestStats
|
|
135
|
+
}
|
|
136
|
+
}) : ({
|
|
137
|
+
TAG: "Error",
|
|
138
|
+
_0: Primitive_exceptions.internalToException(new Error(match$1))
|
|
139
|
+
})
|
|
140
|
+
) : ({
|
|
123
141
|
TAG: "Ok",
|
|
124
142
|
_0: match[1]
|
|
125
143
|
}),
|
|
@@ -24,7 +24,7 @@ let recordStatsInto = (
|
|
|
24
24
|
aggregates: dict<requestStatAgg>,
|
|
25
25
|
requestStats: array<Source.requestStat>,
|
|
26
26
|
) => {
|
|
27
|
-
requestStats->Array.forEach(({method, seconds,
|
|
27
|
+
requestStats->Array.forEach(({method, seconds, ?responseBlocks}) => {
|
|
28
28
|
let agg = switch aggregates->Utils.Dict.dangerouslyGetNonOption(method) {
|
|
29
29
|
| Some(agg) => agg
|
|
30
30
|
| None =>
|
|
@@ -577,11 +577,14 @@ let backoffBeforeRetry = async (
|
|
|
577
577
|
~isRealtime,
|
|
578
578
|
~backoffMillis,
|
|
579
579
|
~minBackoffMillis=0,
|
|
580
|
+
~failoverAfterRetries=2,
|
|
580
581
|
~excludedSources=?,
|
|
581
582
|
) => {
|
|
582
|
-
// Give the source
|
|
583
|
+
// Give the source a few attempts before demoting it, then re-try a failover
|
|
583
584
|
// every second attempt.
|
|
584
|
-
let switchedToWorkingSource = if
|
|
585
|
+
let switchedToWorkingSource = if (
|
|
586
|
+
retry >= failoverAfterRetries && (retry - failoverAfterRetries)->mod(2) === 0
|
|
587
|
+
) {
|
|
585
588
|
let now = Date.now()
|
|
586
589
|
sourceState.lastFailedAt = Some(now)
|
|
587
590
|
switch sourceManager->getNextSource(~isRealtime, ~excludedSources?) {
|
|
@@ -608,6 +611,25 @@ let backoffBeforeRetry = async (
|
|
|
608
611
|
}
|
|
609
612
|
}
|
|
610
613
|
|
|
614
|
+
// A replica behind its siblings usually catches up within seconds, and failing
|
|
615
|
+
// over demotes an otherwise healthy source for the whole recovery timeout. So
|
|
616
|
+
// the source keeps the query for about 10s (100ms doubling, capped at 1s per
|
|
617
|
+
// attempt) before a failover is tried. Past that window the source is behind
|
|
618
|
+
// for real, and with nowhere to fail over to it is polled less and less often,
|
|
619
|
+
// doubling from 1s up to the shared cap.
|
|
620
|
+
let behindHeadFailoverRetries = 12
|
|
621
|
+
|
|
622
|
+
let behindHeadBackoffMillis = retry =>
|
|
623
|
+
if retry < behindHeadFailoverRetries {
|
|
624
|
+
Utils.expBackoff(~base=100, ~exp=retry, ~maxMillis=1_000)
|
|
625
|
+
} else {
|
|
626
|
+
Utils.expBackoff(
|
|
627
|
+
~base=1_000,
|
|
628
|
+
~exp=retry - behindHeadFailoverRetries,
|
|
629
|
+
~maxMillis=maxRetryBackoffMillis,
|
|
630
|
+
)
|
|
631
|
+
}
|
|
632
|
+
|
|
611
633
|
// The queried block hasn't reached the backend instance that served the
|
|
612
634
|
// request. Expected around the head of a load-balanced backend, so early
|
|
613
635
|
// attempts stay quiet and short; a source that stays behind fails over like any
|
|
@@ -619,18 +641,28 @@ let retryBehindHead = async (
|
|
|
619
641
|
~isRealtime,
|
|
620
642
|
~logger: Pino.t,
|
|
621
643
|
~blockNumber: int,
|
|
622
|
-
~method: string,
|
|
623
|
-
~err: exn,
|
|
624
644
|
~excludedSources=?,
|
|
625
645
|
) => {
|
|
626
|
-
let backoffMillis = retry->
|
|
627
|
-
let
|
|
646
|
+
let backoffMillis = retry->behindHeadBackoffMillis
|
|
647
|
+
let source = sourceState.source.name
|
|
648
|
+
let block = blockNumber->Int.toString
|
|
649
|
+
// The condition is routine and resolves without the user doing anything, so
|
|
650
|
+
// the wording says so; the error object would only add internals.
|
|
651
|
+
let (log, msg) = if retry < behindHeadFailoverRetries {
|
|
652
|
+
(
|
|
653
|
+
Logging.childTrace,
|
|
654
|
+
`${source} hasn't reached block ${block} on all of its servers yet. This is normal near the latest block and resolves by itself - retrying shortly.`,
|
|
655
|
+
)
|
|
656
|
+
} else {
|
|
657
|
+
(
|
|
658
|
+
Logging.childWarn,
|
|
659
|
+
`${source} is still catching up to block ${block}. This is a delay on the provider's side; indexing will continue automatically, using another data source if one is configured.`,
|
|
660
|
+
)
|
|
661
|
+
}
|
|
628
662
|
logger->log({
|
|
629
|
-
"msg":
|
|
630
|
-
"method": method,
|
|
663
|
+
"msg": msg,
|
|
631
664
|
"retry": retry,
|
|
632
665
|
"backOffMilliseconds": backoffMillis,
|
|
633
|
-
"err": err->Utils.prettifyExn,
|
|
634
666
|
})
|
|
635
667
|
await sourceManager->backoffBeforeRetry(
|
|
636
668
|
sourceState,
|
|
@@ -638,6 +670,7 @@ let retryBehindHead = async (
|
|
|
638
670
|
~isRealtime,
|
|
639
671
|
~backoffMillis,
|
|
640
672
|
~minBackoffMillis=minRecoverableBackoffMillis,
|
|
673
|
+
~failoverAfterRetries=behindHeadFailoverRetries,
|
|
641
674
|
~excludedSources?,
|
|
642
675
|
)
|
|
643
676
|
}
|
|
@@ -861,8 +894,7 @@ let waitForNewBlock = (sourceManager: t, ~knownHeight, ~isRealtime, ~reducedPoll
|
|
|
861
894
|
// Re-armed because distrusting a stream is a one-shot — the next height it
|
|
862
895
|
// delivers takes it back at its word — and the silence that earned it can
|
|
863
896
|
// come straight back.
|
|
864
|
-
let rec armPokeTimeout = () =>
|
|
865
|
-
pokeTimeoutId := Some(setTimeout(() => {
|
|
897
|
+
let rec armPokeTimeout = () => pokeTimeoutId := Some(setTimeout(() => {
|
|
866
898
|
pokeTimeoutId := None
|
|
867
899
|
if !settled.contents {
|
|
868
900
|
watched->Array.forEach(subscription => subscription.distrustStream())
|
|
@@ -879,8 +911,7 @@ let waitForNewBlock = (sourceManager: t, ~knownHeight, ~isRealtime, ~reducedPoll
|
|
|
879
911
|
// operator about when they hear about a quiet chain, and spreading that
|
|
880
912
|
// would report it early. Fires once — the warning is worth saying once per
|
|
881
913
|
// wait, and a fallback stays recruited.
|
|
882
|
-
let armStallTimeout = () =>
|
|
883
|
-
stallTimeoutId := Some(setTimeout(() => {
|
|
914
|
+
let armStallTimeout = () => stallTimeoutId := Some(setTimeout(() => {
|
|
884
915
|
stallTimeoutId := None
|
|
885
916
|
if !settled.contents {
|
|
886
917
|
stalled := true
|
|
@@ -917,9 +948,7 @@ let waitForNewBlock = (sourceManager: t, ~knownHeight, ~isRealtime, ~reducedPoll
|
|
|
917
948
|
}
|
|
918
949
|
|
|
919
950
|
// Recruited to poll, not to stream: see `watch`.
|
|
920
|
-
fallbackSources->Array.forEach(
|
|
921
|
-
sourceState => sourceState->watch(~withStream=false),
|
|
922
|
-
)
|
|
951
|
+
fallbackSources->Array.forEach(sourceState => sourceState->watch(~withStream=false))
|
|
923
952
|
|
|
924
953
|
// A fallback recruited here can still be holding a stream from an
|
|
925
954
|
// earlier wait that made it a primary, and a live stream is not
|
|
@@ -940,7 +969,6 @@ let waitForNewBlock = (sourceManager: t, ~knownHeight, ~isRealtime, ~reducedPoll
|
|
|
940
969
|
})
|
|
941
970
|
}
|
|
942
971
|
|
|
943
|
-
|
|
944
972
|
let executeQuery = async (
|
|
945
973
|
sourceManager: t,
|
|
946
974
|
~query: FetchState.query,
|
|
@@ -949,11 +977,12 @@ let executeQuery = async (
|
|
|
949
977
|
) => {
|
|
950
978
|
// Read when the manager actually runs out of sources, not on the way in: the
|
|
951
979
|
// reason is recorded by the failure that disabled the last one.
|
|
952
|
-
let noSourcesError = () =>
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
980
|
+
let noSourcesError = () =>
|
|
981
|
+
switch sourceManager.disableReason {
|
|
982
|
+
| Some(reason) =>
|
|
983
|
+
`The indexer doesn't have data-sources which can continue fetching. The last one was disabled because ${reason}`
|
|
984
|
+
| None => "The indexer doesn't have data-sources which can continue fetching. Please, check the error logs or reach out to the Envio team."
|
|
985
|
+
}
|
|
957
986
|
|
|
958
987
|
// Sources where the query is impossible - lazily allocated, excluded for the duration of this query
|
|
959
988
|
let excludedSourcesRef = ref(None)
|
|
@@ -1030,7 +1059,7 @@ let executeQuery = async (
|
|
|
1030
1059
|
await sourceManager->waitForRateLimitReset(~resetMs, ~retry, ~logger)
|
|
1031
1060
|
retryRef := retryRef.contents + 1
|
|
1032
1061
|
|
|
1033
|
-
| Source.SourceBehindHead({blockNumber, requestStats})
|
|
1062
|
+
| Source.SourceBehindHead({blockNumber, requestStats}) =>
|
|
1034
1063
|
sourceState->recordRequestStats(requestStats)
|
|
1035
1064
|
await sourceManager->retryBehindHead(
|
|
1036
1065
|
sourceState,
|
|
@@ -1038,8 +1067,6 @@ let executeQuery = async (
|
|
|
1038
1067
|
~isRealtime,
|
|
1039
1068
|
~logger,
|
|
1040
1069
|
~blockNumber,
|
|
1041
|
-
~method="getItems",
|
|
1042
|
-
~err,
|
|
1043
1070
|
~excludedSources=?excludedSourcesRef.contents,
|
|
1044
1071
|
)
|
|
1045
1072
|
retryRef := retryRef.contents + 1
|
|
@@ -1135,7 +1162,11 @@ let executeQuery = async (
|
|
|
1135
1162
|
})
|
|
1136
1163
|
retryRef := 0
|
|
1137
1164
|
|
|
1138
|
-
| FailedGettingItems({
|
|
1165
|
+
| FailedGettingItems({
|
|
1166
|
+
?exn,
|
|
1167
|
+
attemptedToBlock,
|
|
1168
|
+
retry: WithBackoff({message, backoffMillis}),
|
|
1169
|
+
}) =>
|
|
1139
1170
|
await sourceManager->retryFailedPage(
|
|
1140
1171
|
sourceState,
|
|
1141
1172
|
~retry,
|
|
@@ -1200,22 +1231,14 @@ let getBlockHashes = async (sourceManager: t, ~blockNumbers: array<int>, ~isReal
|
|
|
1200
1231
|
| Error(exn) => throw(exn)
|
|
1201
1232
|
}
|
|
1202
1233
|
} catch {
|
|
1203
|
-
|
|
1204
|
-
|
|
1234
|
+
// A failure arrives as `res.result`, whose `res.requestStats` is already
|
|
1235
|
+
// recorded above, so the stats the error carries are not recorded again.
|
|
1236
|
+
| Source.RateLimited({resetMs}) =>
|
|
1205
1237
|
await sourceManager->waitForRateLimitReset(~resetMs, ~retry, ~logger)
|
|
1206
1238
|
retryRef := retryRef.contents + 1
|
|
1207
1239
|
|
|
1208
|
-
| Source.SourceBehindHead({blockNumber
|
|
1209
|
-
|
|
1210
|
-
await sourceManager->retryBehindHead(
|
|
1211
|
-
sourceState,
|
|
1212
|
-
~retry,
|
|
1213
|
-
~isRealtime,
|
|
1214
|
-
~logger,
|
|
1215
|
-
~blockNumber,
|
|
1216
|
-
~method="getBlockHashes",
|
|
1217
|
-
~err,
|
|
1218
|
-
)
|
|
1240
|
+
| Source.SourceBehindHead({blockNumber}) =>
|
|
1241
|
+
await sourceManager->retryBehindHead(sourceState, ~retry, ~isRealtime, ~logger, ~blockNumber)
|
|
1219
1242
|
retryRef := retryRef.contents + 1
|
|
1220
1243
|
|
|
1221
1244
|
| Source.InconsistentResponse(_) as err =>
|
|
@@ -470,10 +470,11 @@ function getNextSource(sourceManager, isRealtime, excludedSources) {
|
|
|
470
470
|
}
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
-
async function backoffBeforeRetry(sourceManager, sourceState, retry, isRealtime, backoffMillis, minBackoffMillisOpt, excludedSources) {
|
|
473
|
+
async function backoffBeforeRetry(sourceManager, sourceState, retry, isRealtime, backoffMillis, minBackoffMillisOpt, failoverAfterRetriesOpt, excludedSources) {
|
|
474
474
|
let minBackoffMillis = minBackoffMillisOpt !== undefined ? minBackoffMillisOpt : 0;
|
|
475
|
+
let failoverAfterRetries = failoverAfterRetriesOpt !== undefined ? failoverAfterRetriesOpt : 2;
|
|
475
476
|
let switchedToWorkingSource;
|
|
476
|
-
if (retry >=
|
|
477
|
+
if (retry >= failoverAfterRetries && (retry - failoverAfterRetries | 0) % 2 === 0) {
|
|
477
478
|
let now = Date.now();
|
|
478
479
|
sourceState.lastFailedAt = now;
|
|
479
480
|
let next = getNextSource(sourceManager, isRealtime, excludedSources);
|
|
@@ -497,17 +498,31 @@ async function backoffBeforeRetry(sourceManager, sourceState, retry, isRealtime,
|
|
|
497
498
|
}
|
|
498
499
|
}
|
|
499
500
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
501
|
+
function behindHeadBackoffMillis(retry) {
|
|
502
|
+
if (retry < 12) {
|
|
503
|
+
return Utils.expBackoff(100, retry, 1000);
|
|
504
|
+
} else {
|
|
505
|
+
return Utils.expBackoff(1000, retry - 12 | 0, 60000);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function retryBehindHead(sourceManager, sourceState, retry, isRealtime, logger, blockNumber, excludedSources) {
|
|
510
|
+
let backoffMillis = behindHeadBackoffMillis(retry);
|
|
511
|
+
let source = sourceState.source.name;
|
|
512
|
+
let block = blockNumber.toString();
|
|
513
|
+
let match = retry < 12 ? [
|
|
514
|
+
Logging.childTrace,
|
|
515
|
+
source + ` hasn't reached block ` + block + ` on all of its servers yet. This is normal near the latest block and resolves by itself - retrying shortly.`
|
|
516
|
+
] : [
|
|
517
|
+
Logging.childWarn,
|
|
518
|
+
source + ` is still catching up to block ` + block + `. This is a delay on the provider's side; indexing will continue automatically, using another data source if one is configured.`
|
|
519
|
+
];
|
|
520
|
+
match[0](logger, {
|
|
521
|
+
msg: match[1],
|
|
506
522
|
retry: retry,
|
|
507
|
-
backOffMilliseconds: backoffMillis
|
|
508
|
-
err: Utils.prettifyExn(err)
|
|
523
|
+
backOffMilliseconds: backoffMillis
|
|
509
524
|
});
|
|
510
|
-
return await backoffBeforeRetry(sourceManager, sourceState, retry, isRealtime, backoffMillis, 50, excludedSources);
|
|
525
|
+
return await backoffBeforeRetry(sourceManager, sourceState, retry, isRealtime, backoffMillis, 50, 12, excludedSources);
|
|
511
526
|
}
|
|
512
527
|
|
|
513
528
|
function stalledLog(retry, warnAfter, msg, stalled) {
|
|
@@ -536,7 +551,7 @@ async function retryInconsistentResponse(sourceManager, sourceState, retry, isRe
|
|
|
536
551
|
err: Utils.prettifyExn(err)
|
|
537
552
|
});
|
|
538
553
|
Stdlib_Option.forEach(sourceState.source.onReorg, cb => cb());
|
|
539
|
-
return await backoffBeforeRetry(sourceManager, sourceState, retry, isRealtime, backoffMillis, 50, excludedSources);
|
|
554
|
+
return await backoffBeforeRetry(sourceManager, sourceState, retry, isRealtime, backoffMillis, 50, undefined, excludedSources);
|
|
540
555
|
}
|
|
541
556
|
|
|
542
557
|
async function retryFailedPage(sourceManager, sourceState, retry, isRealtime, logger, msg, attemptedToBlock, backoffMillis, minBackoffMillisOpt, err, excludedSources) {
|
|
@@ -549,7 +564,7 @@ async function retryFailedPage(sourceManager, sourceState, retry, isRealtime, lo
|
|
|
549
564
|
retry: retry,
|
|
550
565
|
err: Stdlib_Option.map(err, Utils.prettifyExn)
|
|
551
566
|
});
|
|
552
|
-
return await backoffBeforeRetry(sourceManager, sourceState, retry, isRealtime, backoffMillis, minBackoffMillis, excludedSources);
|
|
567
|
+
return await backoffBeforeRetry(sourceManager, sourceState, retry, isRealtime, backoffMillis, minBackoffMillis, undefined, excludedSources);
|
|
553
568
|
}
|
|
554
569
|
|
|
555
570
|
function waitForNewBlock(sourceManager, knownHeight, isRealtime, reducedPolling) {
|
|
@@ -736,7 +751,7 @@ async function executeQuery(sourceManager, query, knownHeight, isRealtime) {
|
|
|
736
751
|
retryRef = retryRef + 1 | 0;
|
|
737
752
|
} else if (err.RE_EXN_ID === Source.SourceBehindHead) {
|
|
738
753
|
recordStatsInto(sourceState.requestStats, err.requestStats);
|
|
739
|
-
await retryBehindHead(sourceManager, sourceState, retry, isRealtime, logger$2, err.blockNumber,
|
|
754
|
+
await retryBehindHead(sourceManager, sourceState, retry, isRealtime, logger$2, err.blockNumber, excludedSourcesRef);
|
|
740
755
|
retryRef = retryRef + 1 | 0;
|
|
741
756
|
} else if (err.RE_EXN_ID === Source.InconsistentResponse) {
|
|
742
757
|
await retryInconsistentResponse(sourceManager, sourceState, retry, isRealtime, logger$2, "getItems", err, excludedSourcesRef);
|
|
@@ -866,13 +881,10 @@ async function getBlockHashes(sourceManager, blockNumbers, isRealtime) {
|
|
|
866
881
|
} catch (raw_err) {
|
|
867
882
|
let err = Primitive_exceptions.internalToException(raw_err);
|
|
868
883
|
if (err.RE_EXN_ID === Source.RateLimited) {
|
|
869
|
-
|
|
870
|
-
recordStatsInto(sourceState.requestStats, match.requestStats);
|
|
871
|
-
await waitForRateLimitReset(sourceManager, match.resetMs, retry, logger$1);
|
|
884
|
+
await waitForRateLimitReset(sourceManager, err._1.resetMs, retry, logger$1);
|
|
872
885
|
retryRef = retryRef + 1 | 0;
|
|
873
886
|
} else if (err.RE_EXN_ID === Source.SourceBehindHead) {
|
|
874
|
-
|
|
875
|
-
await retryBehindHead(sourceManager, sourceState, retry, isRealtime, logger$1, err.blockNumber, "getBlockHashes", err, undefined);
|
|
887
|
+
await retryBehindHead(sourceManager, sourceState, retry, isRealtime, logger$1, err.blockNumber, undefined);
|
|
876
888
|
retryRef = retryRef + 1 | 0;
|
|
877
889
|
} else if (err.RE_EXN_ID === Source.InconsistentResponse) {
|
|
878
890
|
await retryInconsistentResponse(sourceManager, sourceState, retry, isRealtime, logger$1, "getBlockHashes", err, undefined);
|
|
@@ -886,7 +898,7 @@ async function getBlockHashes(sourceManager, blockNumbers, isRealtime) {
|
|
|
886
898
|
backOffMilliseconds: backoffMillis,
|
|
887
899
|
err: Utils.prettifyExn(err)
|
|
888
900
|
});
|
|
889
|
-
await backoffBeforeRetry(sourceManager, sourceState, retry, isRealtime, backoffMillis, 50, undefined);
|
|
901
|
+
await backoffBeforeRetry(sourceManager, sourceState, retry, isRealtime, backoffMillis, 50, undefined, undefined);
|
|
890
902
|
retryRef = retryRef + 1 | 0;
|
|
891
903
|
}
|
|
892
904
|
}
|
package/src/HandlerTypeCheck.mjs
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
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));
|