envio 3.12.1 → 3.13.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/package.json +6 -6
  2. package/src/BatchProcessing.res +9 -9
  3. package/src/BatchProcessing.res.mjs +5 -5
  4. package/src/Bin.res +24 -17
  5. package/src/Bin.res.mjs +5 -0
  6. package/src/ChainFetching.res +24 -10
  7. package/src/ChainFetching.res.mjs +8 -3
  8. package/src/ChainState.res +43 -0
  9. package/src/ChainState.res.mjs +43 -0
  10. package/src/ChainState.resi +2 -0
  11. package/src/Config.res +35 -0
  12. package/src/Config.res.mjs +39 -0
  13. package/src/Core.res +4 -0
  14. package/src/Core.res.mjs +4 -0
  15. package/src/CrossChainState.res +60 -3
  16. package/src/CrossChainState.res.mjs +38 -4
  17. package/src/CrossChainState.resi +10 -1
  18. package/src/Env.res +4 -0
  19. package/src/IndexerLoop.res +2 -0
  20. package/src/IndexerLoop.res.mjs +1 -0
  21. package/src/IndexerState.res +41 -1
  22. package/src/IndexerState.res.mjs +43 -4
  23. package/src/IndexerState.resi +10 -0
  24. package/src/Logging.res +38 -6
  25. package/src/Logging.res.mjs +32 -5
  26. package/src/Main.res +131 -268
  27. package/src/Main.res.mjs +28 -152
  28. package/src/Metrics.res +263 -102
  29. package/src/Metrics.res.mjs +227 -48
  30. package/src/Persistence.res +27 -2
  31. package/src/Persistence.res.mjs +9 -2
  32. package/src/PgStorage.res +17 -9
  33. package/src/PgStorage.res.mjs +11 -8
  34. package/src/Server.res +181 -0
  35. package/src/Server.res.mjs +143 -0
  36. package/src/Supervisor.res +415 -0
  37. package/src/Supervisor.res.mjs +325 -0
  38. package/src/TestIndexer.res.mjs +1 -1
  39. package/src/Worker.res +95 -0
  40. package/src/Worker.res.mjs +80 -0
  41. package/src/bindings/NodeJs.res +41 -0
  42. package/src/db/InternalTable.res +8 -1
  43. package/src/db/InternalTable.res.mjs +5 -1
  44. package/src/tui/Tui.res +24 -0
  45. package/src/tui/Tui.res.mjs +18 -0
  46. package/src/tui/components/SyncETA.res +12 -6
  47. package/src/tui/components/SyncETA.res.mjs +12 -8
@@ -0,0 +1,325 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Env from "./Env.res.mjs";
4
+ import * as Tui from "./tui/Tui.res.mjs";
5
+ import * as Utils from "./Utils.res.mjs";
6
+ import * as Config from "./Config.res.mjs";
7
+ import * as Server from "./Server.res.mjs";
8
+ import * as Worker from "./Worker.res.mjs";
9
+ import * as ChainId from "./ChainId.res.mjs";
10
+ import * as Logging from "./Logging.res.mjs";
11
+ import * as Metrics from "./Metrics.res.mjs";
12
+ import * as Process from "process";
13
+ import * as ChainMap from "./ChainMap.res.mjs";
14
+ import * as PgStorage from "./PgStorage.res.mjs";
15
+ import * as Performance from "./bindings/Performance.res.mjs";
16
+ import * as Persistence from "./Persistence.res.mjs";
17
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
18
+ import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
19
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
20
+ import * as Child_process from "child_process";
21
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
22
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
23
+ import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
24
+
25
+ function plan(chainIds, maxConnections) {
26
+ let workerCount = Primitive_int.min(chainIds.length, maxConnections / 2 | 0);
27
+ if (workerCount < 2) {
28
+ return;
29
+ }
30
+ let evenShare = Primitive_int.div(maxConnections, workerCount);
31
+ let remainder = Primitive_int.mod_(maxConnections, workerCount);
32
+ return Stdlib_Array.fromInitializer(workerCount, workerIndex => ({
33
+ chainIds: chainIds.filter((param, dealIndex) => {
34
+ let position = Primitive_int.mod_(dealIndex, workerCount);
35
+ let isReversePass = Primitive_int.div(dealIndex, workerCount) % 2 === 1;
36
+ return (
37
+ isReversePass ? (workerCount - 1 | 0) - position | 0 : position
38
+ ) === workerIndex;
39
+ }),
40
+ maxConnections: evenShare + (
41
+ workerIndex < remainder ? 1 : 0
42
+ ) | 0
43
+ }));
44
+ }
45
+
46
+ function planForRun(config, maxConnectionsOpt) {
47
+ let maxConnections = maxConnectionsOpt !== undefined ? maxConnectionsOpt : Env.Db.maxConnections;
48
+ if (config.isolated || !Config.isPerChain(config)) {
49
+ return;
50
+ } else {
51
+ return plan(ChainMap.values(config.chainMap).map(chain => chain.id), maxConnections);
52
+ }
53
+ }
54
+
55
+ function name(worker) {
56
+ return worker.chainIds.map(ChainId.toString).join(";");
57
+ }
58
+
59
+ function label(worker) {
60
+ return `[chain ` + name(worker) + `]`;
61
+ }
62
+
63
+ function logFilePath(workerIndex, pathOpt) {
64
+ let path = pathOpt !== undefined ? pathOpt : Env.logFilePath;
65
+ let suffix = `.worker-` + workerIndex.toString();
66
+ let dot = path.lastIndexOf(".");
67
+ if (dot > path.lastIndexOf("/")) {
68
+ return path.slice(0, dot) + suffix + path.slice(dot, path.length);
69
+ } else {
70
+ return path + suffix;
71
+ }
72
+ }
73
+
74
+ function readLines(onLine) {
75
+ let pending = {
76
+ contents: ""
77
+ };
78
+ let read = chunk => {
79
+ let parts = (pending.contents + chunk).split("\n");
80
+ pending.contents = Stdlib_Option.getOr(parts.pop(), "");
81
+ parts.forEach(onLine);
82
+ };
83
+ let flush = () => {
84
+ let line = pending.contents;
85
+ if (line === "") {
86
+ return;
87
+ }
88
+ pending.contents = "";
89
+ onLine(line);
90
+ };
91
+ return [
92
+ read,
93
+ flush
94
+ ];
95
+ }
96
+
97
+ function fork(worker, workerIndex, holdRealtime, entryPathOpt, pipeOutputOpt, onOutputOpt) {
98
+ let entryPath = entryPathOpt !== undefined ? entryPathOpt : process.argv[1];
99
+ let pipeOutput = pipeOutputOpt !== undefined ? pipeOutputOpt : false;
100
+ let onOutput = onOutputOpt !== undefined ? onOutputOpt : prim => {
101
+ console.log(prim);
102
+ };
103
+ let env = Object.assign({}, Process.env);
104
+ env[Worker.envVar] = S$RescriptSchema.reverseConvertToJsonStringOrThrow({
105
+ chainIds: worker.chainIds,
106
+ holdRealtime: holdRealtime
107
+ }, Worker.configSchema, undefined);
108
+ env["ENVIO_PG_MAX_CONNECTIONS"] = worker.maxConnections.toString();
109
+ env["LOG_FILE"] = logFilePath(workerIndex, undefined);
110
+ if (pipeOutput && Stdlib_Option.getOr((process.stdout.isTTY == null) ? undefined : Primitive_option.some(process.stdout.isTTY), false)) {
111
+ env["FORCE_COLOR"] = "1";
112
+ }
113
+ let child = Child_process.fork(entryPath, [], {
114
+ env: env,
115
+ serialization: "advanced",
116
+ stdio: pipeOutput ? [
117
+ "inherit",
118
+ "pipe",
119
+ "pipe",
120
+ "ipc"
121
+ ] : [
122
+ "inherit",
123
+ "inherit",
124
+ "inherit",
125
+ "ipc"
126
+ ]
127
+ });
128
+ if (pipeOutput) {
129
+ [
130
+ child.stdout,
131
+ child.stderr
132
+ ].forEach(stream => {
133
+ if (stream === null) {
134
+ return;
135
+ }
136
+ let match = readLines(onOutput);
137
+ stream.setEncoding("utf8");
138
+ stream.on("data", match[0]);
139
+ stream.on("end", match[1]);
140
+ });
141
+ }
142
+ let running = {
143
+ worker: worker,
144
+ child: child,
145
+ snapshot: undefined,
146
+ runtime: undefined,
147
+ settled: false
148
+ };
149
+ child.on("message", message => {
150
+ running.snapshot = message.metrics;
151
+ running.runtime = message.runtime;
152
+ });
153
+ return running;
154
+ }
155
+
156
+ function stop(group) {
157
+ group.stopping = true;
158
+ group.running.forEach(r => {
159
+ r.child.kill("SIGTERM");
160
+ });
161
+ }
162
+
163
+ let inFlight = {
164
+ contents: undefined
165
+ };
166
+
167
+ function syncCache(dump) {
168
+ let dumping = inFlight.contents;
169
+ if (dumping !== undefined) {
170
+ return dumping;
171
+ }
172
+ let dumping$1 = dump().finally(() => {
173
+ inFlight.contents = undefined;
174
+ });
175
+ inFlight.contents = dumping$1;
176
+ return dumping$1;
177
+ }
178
+
179
+ function dumpCache(config) {
180
+ let storage = PgStorage.makeStorageFromEnv(config, Primitive_option.some(PgStorage.makeClient(1)), undefined, undefined);
181
+ return storage.dumpEffectCache().finally(() => {
182
+ storage.close();
183
+ });
184
+ }
185
+
186
+ async function awaitExit(group) {
187
+ let failed = {
188
+ contents: false
189
+ };
190
+ let alive = {
191
+ contents: group.running.length
192
+ };
193
+ await new Promise((resolve, param) => {
194
+ let onGone = (r, failure) => {
195
+ if (!r.settled) {
196
+ r.settled = true;
197
+ if (failure) {
198
+ failed.contents = true;
199
+ if (!group.stopping) {
200
+ stop(group);
201
+ }
202
+ }
203
+ alive.contents = alive.contents - 1 | 0;
204
+ if (alive.contents === 0) {
205
+ return resolve();
206
+ } else {
207
+ return;
208
+ }
209
+ }
210
+ };
211
+ group.running.forEach(r => {
212
+ r.child.on("exit", (code, _signal) => onGone(r, !group.stopping && (
213
+ code === null ? undefined : Primitive_option.some(code)
214
+ ) !== 0));
215
+ r.child.on("error", exn => {
216
+ Logging.errorWithExn(exn, label(r.worker) + ` failed to start`);
217
+ onGone(r, true);
218
+ });
219
+ });
220
+ });
221
+ if (failed.contents) {
222
+ Stdlib_JsError.throwWithMessage("An indexer process exited with a failure. Stopped the others.");
223
+ }
224
+ if (group.stopping) {
225
+ return "Stopped";
226
+ } else {
227
+ return "Finished";
228
+ }
229
+ }
230
+
231
+ function isRunAtHead(snapshots, workerCount) {
232
+ if (snapshots.length === workerCount) {
233
+ return snapshots.every(snapshot => snapshot.hasArrivedAtHead);
234
+ } else {
235
+ return false;
236
+ }
237
+ }
238
+
239
+ async function run(config, workers, reset) {
240
+ let persistence = PgStorage.makePersistenceFromConfig(config, undefined);
241
+ await Persistence.initForRun(persistence, config, reset, config.isDev, false);
242
+ await persistence.storage.close();
243
+ let startTime = new Date();
244
+ let startTimeRef = Performance.now();
245
+ Logging.info(`Splitting ` + ChainMap.values(config.chainMap).length.toString() + ` chains across ` + workers.length.toString() + ` processes, from a budget of ` + Env.Db.maxConnections.toString() + ` database connections.`);
246
+ let shouldUseTui = Tui.shouldUse(undefined);
247
+ let holdRealtime = Persistence.getInitializedState(persistence).chains.some(chain => Stdlib_Option.isNone(chain.timestampCaughtUpToHeadOrEndblock));
248
+ let group = {
249
+ running: workers.map((worker, workerIndex) => fork(worker, workerIndex, holdRealtime, undefined, shouldUseTui, undefined)),
250
+ stopping: false
251
+ };
252
+ let reported = () => Stdlib_Array.filterMap(group.running, r => r.snapshot);
253
+ let merge = snapshots => Metrics.merge(snapshots, startTime, new Date(), Performance.secondsSince(startTimeRef));
254
+ Server.startServer(() => {
255
+ let snapshots = reported();
256
+ if (snapshots.length !== 0) {
257
+ return merge(snapshots);
258
+ }
259
+ }, Utils.EnvioPackage.value.version, () => syncCache(() => dumpCache(config)), () => Metrics.renderRuntime(Stdlib_Array.filterMap(group.running, r => Stdlib_Option.map(r.runtime, runtime => [
260
+ `worker="` + name(r.worker) + `"`,
261
+ runtime
262
+ ]))), config.isDev);
263
+ let releaseCheck = {
264
+ contents: undefined
265
+ };
266
+ let stopReleaseCheck = () => {
267
+ Stdlib_Option.forEach(releaseCheck.contents, prim => {
268
+ clearInterval(prim);
269
+ });
270
+ releaseCheck.contents = undefined;
271
+ };
272
+ if (holdRealtime) {
273
+ releaseCheck.contents = Primitive_option.some(setInterval(() => {
274
+ if (isRunAtHead(reported(), group.running.length)) {
275
+ stopReleaseCheck();
276
+ group.running.forEach(r => {
277
+ r.child.send("release-realtime");
278
+ });
279
+ return Logging.info("Every chain has reached the head. Switching the run to realtime.");
280
+ }
281
+ }, 500));
282
+ }
283
+ if (shouldUseTui) {
284
+ Tui.start(config, () => merge(reported()));
285
+ }
286
+ process.on("SIGTERM", () => stop(group));
287
+ process.on("SIGINT", () => stop(group));
288
+ let outcome = await awaitExit(group);
289
+ stopReleaseCheck();
290
+ if (outcome === "Finished") {
291
+ if (shouldUseTui) {
292
+ process.on("SIGTERM", () => {
293
+ Process.exit(0);
294
+ });
295
+ process.on("SIGINT", () => {
296
+ Process.exit(0);
297
+ });
298
+ } else {
299
+ Logging.info("Exiting with success");
300
+ Process.exit(0);
301
+ }
302
+ return;
303
+ }
304
+ Process.exit(0);
305
+ }
306
+
307
+ let minConnectionsPerWorker = 2;
308
+
309
+ export {
310
+ minConnectionsPerWorker,
311
+ plan,
312
+ planForRun,
313
+ name,
314
+ label,
315
+ logFilePath,
316
+ readLines,
317
+ fork,
318
+ stop,
319
+ syncCache,
320
+ dumpCache,
321
+ awaitExit,
322
+ isRunAtHead,
323
+ run,
324
+ }
325
+ /* Env Not a pure module */
@@ -639,7 +639,7 @@ function createTestIndexer() {
639
639
  };
640
640
  try {
641
641
  await new Promise((resolve, reject) => {
642
- let indexerState = IndexerState.makeFromDbState(runConfig, persistence, initialState, registrationsByChainId, undefined, undefined, exitAfterFirstEventBlock, undefined, undefined, errHandler => {
642
+ let indexerState = IndexerState.makeFromDbState(runConfig, persistence, initialState, registrationsByChainId, undefined, undefined, exitAfterFirstEventBlock, undefined, undefined, undefined, errHandler => {
643
643
  ErrorHandling.log(errHandler);
644
644
  reject(Utils.prettifyExn(errHandler.exn));
645
645
  }, () => resolve());
package/src/Worker.res ADDED
@@ -0,0 +1,95 @@
1
+ // The worker side of a supervised run: a process the supervisor forked to drive
2
+ // a subset of the chains. It has no server and no TUI of its own — it reports
3
+ // through the IPC channel, and the supervisor is the one operational surface.
4
+
5
+ // Set by a supervisor in the environment of the workers it forks. Internal:
6
+ // it counts only together with the fork's own channel, so a copy left in a
7
+ // shell starts nothing, and an indexer a user starts themselves takes every
8
+ // path it takes today.
9
+ let envVar = "ENVIO_INTERNAL_WORKER"
10
+
11
+ // What the supervisor decided about this worker, handed over in the spawn
12
+ // environment rather than over the channel: it is settled before the process
13
+ // starts, and the worker needs it before it can load its own config.
14
+ type config = {
15
+ chainIds: array<ChainId.t>,
16
+ // The chains this worker drives may reach the head while chains in another
17
+ // process are still backfilling, and an indexer goes realtime as a whole or
18
+ // not at all. Cleared by the supervisor's `ReleaseRealtime`.
19
+ holdRealtime: bool,
20
+ }
21
+
22
+ let configSchema = S.object((s): config => {
23
+ chainIds: s.field("chainIds", S.array(ChainId.schema)),
24
+ holdRealtime: s.fieldOr("holdRealtime", S.bool, false),
25
+ })
26
+
27
+ // Read as this module loads, which is before anything that could catch a bare
28
+ // schema error and say where it came from.
29
+ let detect = (~env: dict<string>, ~hasChannel) =>
30
+ switch (hasChannel, env->Dict.get(envVar)) {
31
+ | (true, Some(json)) =>
32
+ switch json->S.parseJsonStringOrThrow(configSchema) {
33
+ | config => Some(config)
34
+ | exception S.Raised(error) =>
35
+ JsError.throwWithMessage(
36
+ `Invalid ${envVar}: ${error->S.Error.message}. It is set by an indexer supervisor for the processes it forks, and isn't meant to be set by hand.`,
37
+ )
38
+ }
39
+ | _ => None
40
+ }
41
+
42
+ let config = detect(
43
+ ~env=NodeJs.Process.process.env,
44
+ ~hasChannel=NodeJs.Process.channel->Nullable.toOption->Option.isSome,
45
+ )
46
+
47
+ let isEnabled = config->Option.isSome
48
+
49
+ @tag("kind")
50
+ type parentMessage =
51
+ // Every chain in the run has reached the head, so this worker may enter the
52
+ // reorg threshold and switch to realtime with the rest of them.
53
+ | @as("release-realtime") ReleaseRealtime
54
+
55
+ @tag("kind")
56
+ type workerMessage =
57
+ | @as("snapshot") Snapshot({metrics: Metrics.t, runtime: Metrics.runtimeSample})
58
+
59
+ // How often a worker reports. Matches the TUI's own refresh, so the supervised
60
+ // display moves at the same rate an unsplit run's does.
61
+ %%private(let snapshotIntervalMillis = 500)
62
+
63
+ // The supervisor is the one that stops a worker, and the one whose absence
64
+ // ends it. A terminal's interrupt reaches the whole group at once, so the
65
+ // worker leaves it to the supervisor, which stops every worker in turn; without
66
+ // that, a worker gone on its own would read as a failure to the supervisor
67
+ // still deciding what the interrupt meant. A supervisor that dies can't tear
68
+ // the group down, so losing the channel is what ends the worker then.
69
+ let bindToSupervisor = () => {
70
+ NodeJs.Process.onSignal("SIGINT", () => ())
71
+ NodeJs.Process.onDisconnect(() => {
72
+ Logging.error("The indexer supervisor is gone. Stopping this chain's process.")
73
+ NodeJs.process->NodeJs.exitWithCode(Failure)
74
+ })
75
+ }
76
+
77
+ %%private(let send = (message: workerMessage) => NodeJs.Process.sendToParent(message)->ignore)
78
+
79
+ // Reports this process's chains and its own runtime for as long as it runs, so
80
+ // the supervisor can merge every worker's into the one snapshot the run serves,
81
+ // and listens for the one decision the supervisor makes on the run's behalf.
82
+ // Does nothing in a process nobody forked.
83
+ let bindRun = (~getMetrics: unit => Metrics.t, ~onReleaseRealtime: unit => unit) =>
84
+ if isEnabled {
85
+ Metrics.startRuntimeCollectors()
86
+ let _intervalId = setInterval(
87
+ () => send(Snapshot({metrics: getMetrics(), runtime: Metrics.sampleRuntime()})),
88
+ snapshotIntervalMillis,
89
+ )
90
+ NodeJs.Process.onMessage((message: parentMessage) =>
91
+ switch message {
92
+ | ReleaseRealtime => onReleaseRealtime()
93
+ }
94
+ )
95
+ }
@@ -0,0 +1,80 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as ChainId from "./ChainId.res.mjs";
4
+ import * as Logging from "./Logging.res.mjs";
5
+ import * as Metrics from "./Metrics.res.mjs";
6
+ import * as Process from "process";
7
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
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";
10
+ import * as S$RescriptSchema from "rescript-schema/src/S.res.mjs";
11
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
12
+
13
+ let envVar = "ENVIO_INTERNAL_WORKER";
14
+
15
+ let configSchema = S$RescriptSchema.object(s => ({
16
+ chainIds: s.f("chainIds", S$RescriptSchema.array(ChainId.schema)),
17
+ holdRealtime: s.fieldOr("holdRealtime", S$RescriptSchema.bool, false)
18
+ }));
19
+
20
+ function detect(env, hasChannel) {
21
+ let match = env[envVar];
22
+ if (!hasChannel) {
23
+ return;
24
+ }
25
+ if (match === undefined) {
26
+ return;
27
+ }
28
+ let config;
29
+ try {
30
+ config = S$RescriptSchema.parseJsonStringOrThrow(match, configSchema);
31
+ } catch (raw_error) {
32
+ let error = Primitive_exceptions.internalToException(raw_error);
33
+ if (error.RE_EXN_ID === S$RescriptSchema.Raised) {
34
+ return Stdlib_JsError.throwWithMessage(`Invalid ` + envVar + `: ` + S$RescriptSchema.$$Error.message(error._1) + `. It is set by an indexer supervisor for the processes it forks, and isn't meant to be set by hand.`);
35
+ }
36
+ throw error;
37
+ }
38
+ return config;
39
+ }
40
+
41
+ let config = detect(Process.env, Stdlib_Option.isSome(Primitive_option.fromNullable(process.channel)));
42
+
43
+ let isEnabled = Stdlib_Option.isSome(config);
44
+
45
+ function bindToSupervisor() {
46
+ process.on("SIGINT", () => {});
47
+ process.on("disconnect", () => {
48
+ Logging.error("The indexer supervisor is gone. Stopping this chain's process.");
49
+ Process.exit(1);
50
+ });
51
+ }
52
+
53
+ function bindRun(getMetrics, onReleaseRealtime) {
54
+ if (isEnabled) {
55
+ Metrics.startRuntimeCollectors();
56
+ setInterval(() => {
57
+ let message_0 = getMetrics();
58
+ let message_1 = Metrics.sampleRuntime();
59
+ let message = {
60
+ kind: "snapshot",
61
+ metrics: message_0,
62
+ runtime: message_1
63
+ };
64
+ process.send(message);
65
+ }, 500);
66
+ process.on("message", message => onReleaseRealtime());
67
+ return;
68
+ }
69
+ }
70
+
71
+ export {
72
+ envVar,
73
+ configSchema,
74
+ detect,
75
+ config,
76
+ isEnabled,
77
+ bindToSupervisor,
78
+ bindRun,
79
+ }
80
+ /* configSchema Not a pure module */
@@ -64,6 +64,21 @@ module Process = {
64
64
  @module("process") external version: string = "version"
65
65
  @module("process")
66
66
  external getActiveResourcesInfo: unit => array<string> = "getActiveResourcesInfo"
67
+
68
+ // Only a process forked with an IPC channel has these. Called through
69
+ // `process` rather than off a namespace import, which would drop the
70
+ // receiver Node's own implementations read.
71
+ @val @scope("process") external sendToParent: 'msg => bool = "send"
72
+ @val @scope("process")
73
+ external onMessage: (@as("message") _, 'msg => unit) => unit = "on"
74
+ @val @scope("process") external onSignal: (string, unit => unit) => unit = "on"
75
+ // Present only in a process forked with an IPC channel.
76
+ @val @scope("process") external channel: Nullable.t<unknown> = "channel"
77
+ @val @scope("process")
78
+ external onDisconnect: (@as("disconnect") _, unit => unit) => unit = "on"
79
+ @val @scope("process") external argv: array<string> = "argv"
80
+ @val @scope("process")
81
+ external emitMessage: (@as("message") _, 'msg) => bool = "emit"
67
82
  }
68
83
 
69
84
  module Buffer = {
@@ -141,6 +156,32 @@ module ChildProcess = {
141
156
 
142
157
  @module("child_process")
143
158
  external execWithOptions: (string, execOptions, callback) => unit = "exec"
159
+
160
+ type child
161
+ type forkOptions = {
162
+ cwd?: string,
163
+ env?: dict<string>,
164
+ // "advanced" uses the structured clone algorithm, so a message keeps the
165
+ // Date values a metrics snapshot carries instead of stringifying them.
166
+ serialization?: string,
167
+ stdio?: array<string>,
168
+ }
169
+ @module("child_process")
170
+ external fork: (string, array<string>, forkOptions) => child = "fork"
171
+ @send external send: (child, 'msg) => bool = "send"
172
+ @send external onMessage: (child, @as("message") _, 'msg => unit) => unit = "on"
173
+ @send
174
+ external onExit: (child, @as("exit") _, (Null.t<int>, Null.t<string>) => unit) => unit = "on"
175
+ @send external onChildError: (child, @as("error") _, exn => unit) => unit = "on"
176
+ @send external kill: (child, string) => bool = "kill"
177
+
178
+ // Present only for a stdio slot the parent asked to pipe.
179
+ type stdioStream
180
+ @get external stdout: child => Null.t<stdioStream> = "stdout"
181
+ @get external stderr: child => Null.t<stdioStream> = "stderr"
182
+ @send external setEncoding: (stdioStream, string) => unit = "setEncoding"
183
+ @send external onData: (stdioStream, @as("data") _, string => unit) => unit = "on"
184
+ @send external onEnd: (stdioStream, @as("end") _, unit => unit) => unit = "on"
144
185
  }
145
186
 
146
187
  module Url = {
@@ -344,7 +344,14 @@ VALUES ${valuesRows->Array.joinUnsafe(",\n ")};`,
344
344
  let setClauses = Array.mapWithIndex(metaFields, (field, index) => {
345
345
  let fieldName = (field :> string)
346
346
  let paramIndex = index + 2 // +2 because $1 is for id in WHERE clause
347
- `"${fieldName}" = $${Int.toString(paramIndex)}`
347
+ switch field {
348
+ // A chain that caught up never un-catches up, so a metadata write staged
349
+ // before `markReady` and flushed after the stamp must not clear it. The
350
+ // writes race: metadata is written on a throttle of its own, outside the
351
+ // batch the finalization flushes.
352
+ | #ready_at => `"${fieldName}" = COALESCE($${Int.toString(paramIndex)}, "${fieldName}")`
353
+ | _ => `"${fieldName}" = $${Int.toString(paramIndex)}`
354
+ }
348
355
  })
349
356
 
350
357
  `UPDATE "${pgSchema}"."${table.tableName}"
@@ -246,7 +246,11 @@ let metaFields = [
246
246
  function makeMetaFieldsUpdateQuery(pgSchema) {
247
247
  let setClauses = metaFields.map((field, index) => {
248
248
  let paramIndex = index + 2 | 0;
249
- return `"` + field + `" = $` + paramIndex.toString();
249
+ if (field === "ready_at") {
250
+ return `"` + field + `" = COALESCE($` + paramIndex.toString() + `, "` + field + `")`;
251
+ } else {
252
+ return `"` + field + `" = $` + paramIndex.toString();
253
+ }
250
254
  });
251
255
  return `UPDATE "` + pgSchema + `"."` + table$2.tableName + `"
252
256
  SET ` + setClauses.join(",\n ") + `
package/src/tui/Tui.res CHANGED
@@ -248,6 +248,30 @@ module App = {
248
248
  }
249
249
  }
250
250
 
251
+ type args = {@as("tui-off") tuiOff?: bool}
252
+
253
+ type process
254
+ @val external process: process = "process"
255
+ @get external argv: process => 'a = "argv"
256
+
257
+ type mainArgs = Yargs.parsedArgs<args>
258
+
259
+ // Whether this process draws the progress display: `--tui-off` first, then
260
+ // `ENVIO_TUI`, then whether anything is watching. A supervisor asks the same
261
+ // question its workers would have, since it is the one drawing for the run.
262
+ let shouldUse = (~suppressed=false) => {
263
+ let mainArgs: mainArgs = process->argv->Yargs.hideBin->Yargs.yargs->Yargs.argv
264
+ let explicitTui = switch mainArgs.tuiOff {
265
+ | Some(off) => Some(!off)
266
+ | None => Env.tuiEnvVar
267
+ }
268
+ switch (suppressed, explicitTui) {
269
+ | (true, _) => false
270
+ | (_, Some(tui)) => tui
271
+ | (_, None) => !Envio.isNonInteractive()
272
+ }
273
+ }
274
+
251
275
  let start = (~config, ~getMetrics) => {
252
276
  let {rerender} = render(<App config getMetrics />)
253
277
  () => {
@@ -3,14 +3,17 @@
3
3
  import * as Env from "../Env.res.mjs";
4
4
  import * as Ink from "./bindings/Ink.res.mjs";
5
5
  import * as Ink$1 from "ink";
6
+ import * as Envio from "../Envio.res.mjs";
6
7
  import * as React from "react";
7
8
  import * as SyncETA from "./components/SyncETA.res.mjs";
8
9
  import * as TuiData from "./components/TuiData.res.mjs";
9
10
  import * as Messages from "./components/Messages.res.mjs";
11
+ import Yargs from "yargs/yargs";
10
12
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
11
13
  import InkBigText from "ink-big-text";
12
14
  import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
13
15
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
16
+ import * as Helpers from "yargs/helpers";
14
17
  import * as Primitive_float from "@rescript/runtime/lib/es6/Primitive_float.js";
15
18
  import * as JsxRuntime from "react/jsx-runtime";
16
19
  import * as BufferedProgressBar from "./components/BufferedProgressBar.res.mjs";
@@ -322,6 +325,20 @@ let App = {
322
325
  make: Tui$App
323
326
  };
324
327
 
328
+ function shouldUse(suppressedOpt) {
329
+ let suppressed = suppressedOpt !== undefined ? suppressedOpt : false;
330
+ let mainArgs = Yargs(Helpers.hideBin(process.argv)).argv;
331
+ let off = mainArgs["tui-off"];
332
+ let explicitTui = off !== undefined ? !off : Env.tuiEnvVar;
333
+ if (suppressed) {
334
+ return false;
335
+ } else if (explicitTui !== undefined) {
336
+ return explicitTui;
337
+ } else {
338
+ return !Envio.isNonInteractive();
339
+ }
340
+ }
341
+
325
342
  function start(config, getMetrics) {
326
343
  let match = Ink.render(undefined, JsxRuntime.jsx(Tui$App, {
327
344
  config: config,
@@ -339,6 +356,7 @@ export {
339
356
  EventsPerSecond,
340
357
  TotalEventsProcessed,
341
358
  App,
359
+ shouldUse,
342
360
  start,
343
361
  }
344
362
  /* Env Not a pure module */