ttsc 0.32.0 → 0.32.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/lib/TtscCompiler.js +1 -1
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/compiler/internal/ResidentCheckProcess.d.ts +74 -96
- package/lib/compiler/internal/ResidentCheckProcess.js +150 -335
- package/lib/compiler/internal/ResidentCheckProcess.js.map +1 -1
- package/lib/compiler/internal/ResidentCheckProcessHost.d.ts +127 -0
- package/lib/compiler/internal/ResidentCheckProcessHost.js +447 -0
- package/lib/compiler/internal/ResidentCheckProcessHost.js.map +1 -0
- package/lib/compiler/internal/ResidentCheckProcessProtocol.d.ts +64 -0
- package/lib/compiler/internal/ResidentCheckProcessProtocol.js +3 -0
- package/lib/compiler/internal/ResidentCheckProcessProtocol.js.map +1 -0
- package/lib/compiler/internal/build/BuildExecution.d.ts +103 -1
- package/lib/compiler/internal/build/BuildExecution.js +91 -11
- package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
- package/lib/compiler/internal/build/ResidentCheckWatchSession.d.ts +16 -11
- package/lib/compiler/internal/build/ResidentCheckWatchSession.js +37 -21
- package/lib/compiler/internal/build/ResidentCheckWatchSession.js.map +1 -1
- package/lib/compiler/internal/build/RunBuildOptions.d.ts +6 -5
- package/lib/compiler/internal/build/WatchInputPublication.d.ts +59 -0
- package/lib/compiler/internal/build/WatchInputPublication.js +93 -0
- package/lib/compiler/internal/build/WatchInputPublication.js.map +1 -0
- package/lib/compiler/internal/build/runBuild.d.ts +29 -2
- package/lib/compiler/internal/build/runBuild.js +45 -19
- package/lib/compiler/internal/build/runBuild.js.map +1 -1
- package/lib/compiler/internal/buildNativeCompiler.d.ts +2 -0
- package/lib/compiler/internal/buildNativeCompiler.js +1 -0
- package/lib/compiler/internal/buildNativeCompiler.js.map +1 -1
- package/lib/compiler/internal/compileProjectInMemory.js +4 -2
- package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/project/resolveOwningProjectConfig.d.ts +7 -2
- package/lib/compiler/internal/project/resolveOwningProjectConfig.js +6 -3
- package/lib/compiler/internal/project/resolveOwningProjectConfig.js.map +1 -1
- package/lib/compiler/internal/readEffectiveCompilerOptions.js +1 -1
- package/lib/compiler/internal/readEffectiveCompilerOptions.js.map +1 -1
- package/lib/compiler/internal/resolveBinary.d.ts +1 -0
- package/lib/compiler/internal/resolveBinary.js.map +1 -1
- package/lib/compiler/internal/resolveTsgo.d.ts +6 -0
- package/lib/compiler/internal/resolveTsgo.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.d.ts +28 -1
- package/lib/compiler/internal/runSingleFileEmit.js +120 -86
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.js +8 -2
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +11 -3
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/compiler/internal/transformProjectInWorker.d.ts +1 -1
- package/lib/compiler/internal/transformProjectInWorker.js +3 -2
- package/lib/compiler/internal/transformProjectInWorker.js.map +1 -1
- package/lib/internal/workerModuleUrl.d.ts +28 -0
- package/lib/internal/workerModuleUrl.js +40 -0
- package/lib/internal/workerModuleUrl.js.map +1 -0
- package/lib/launcher/internal/buildSingleRootProject.js +2 -1
- package/lib/launcher/internal/buildSingleRootProject.js.map +1 -1
- package/lib/launcher/internal/getCompilerVersionText.js +3 -2
- package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
- package/lib/launcher/internal/prepareExecution.js +2 -1
- package/lib/launcher/internal/prepareExecution.js.map +1 -1
- package/lib/launcher/internal/runTtsc.js +91 -41
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runtime/CommonJsRuntimeSource.d.ts +10 -6
- package/lib/launcher/internal/runtime/CommonJsRuntimeSource.js +11 -9
- package/lib/launcher/internal/runtime/CommonJsRuntimeSource.js.map +1 -1
- package/lib/launcher/internal/runtime/advertiseTypeScriptExtensions.d.ts +20 -0
- package/lib/launcher/internal/runtime/advertiseTypeScriptExtensions.js +27 -0
- package/lib/launcher/internal/runtime/advertiseTypeScriptExtensions.js.map +1 -0
- package/lib/launcher/internal/runtime/installRuntimeHooks.d.ts +5 -3
- package/lib/launcher/internal/runtime/installRuntimeHooks.js +7 -3
- package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
- package/lib/launcher/internal/watch/AsyncWatchTopology.d.ts +113 -0
- package/lib/launcher/internal/watch/AsyncWatchTopology.js +232 -0
- package/lib/launcher/internal/watch/AsyncWatchTopology.js.map +1 -0
- package/lib/launcher/internal/watch/WatchSettlement.d.ts +49 -0
- package/lib/launcher/internal/watch/WatchSettlement.js +70 -0
- package/lib/launcher/internal/watch/WatchSettlement.js.map +1 -0
- package/lib/launcher/internal/watch/WatchTopology.d.ts +29 -1
- package/lib/launcher/internal/watch/WatchTopology.js +86 -10
- package/lib/launcher/internal/watch/WatchTopology.js.map +1 -1
- package/lib/launcher/internal/watch/WatchTopologyWorkerProtocol.d.ts +121 -0
- package/lib/launcher/internal/watch/WatchTopologyWorkerProtocol.js +3 -0
- package/lib/launcher/internal/watch/WatchTopologyWorkerProtocol.js.map +1 -0
- package/lib/launcher/internal/watch/serveWatchTopologyWorker.d.ts +19 -0
- package/lib/launcher/internal/watch/serveWatchTopologyWorker.js +62 -0
- package/lib/launcher/internal/watch/serveWatchTopologyWorker.js.map +1 -0
- package/lib/launcher/internal/watch/watchTopologyWorker.d.ts +1 -0
- package/lib/launcher/internal/watch/watchTopologyWorker.js +22 -0
- package/lib/launcher/internal/watch/watchTopologyWorker.js.map +1 -0
- package/lib/plugin/CapabilityPluginResolver.d.ts +1 -1
- package/lib/plugin/CapabilityPluginResolver.js +3 -2
- package/lib/plugin/CapabilityPluginResolver.js.map +1 -1
- package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.d.ts +15 -12
- package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js +13 -11
- package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js.map +1 -1
- package/lib/plugin/internal/load/PluginPackageResolution.d.ts +2 -2
- package/lib/plugin/internal/load/PluginPackageResolution.js +13 -7
- package/lib/plugin/internal/load/PluginPackageResolution.js.map +1 -1
- package/lib/plugin/internal/load/ProjectPluginEntries.d.ts +23 -7
- package/lib/plugin/internal/load/ProjectPluginEntries.js +36 -14
- package/lib/plugin/internal/load/ProjectPluginEntries.js.map +1 -1
- package/lib/plugin/internal/load/collectProjectHostInputs.d.ts +3 -1
- package/lib/plugin/internal/load/collectProjectHostInputs.js +5 -2
- package/lib/plugin/internal/load/collectProjectHostInputs.js.map +1 -1
- package/lib/plugin/internal/load/hasProjectPluginEntries.d.ts +3 -1
- package/lib/plugin/internal/load/hasProjectPluginEntries.js +4 -2
- package/lib/plugin/internal/load/hasProjectPluginEntries.js.map +1 -1
- package/lib/plugin/internal/load/loadProjectPlugins.d.ts +36 -5
- package/lib/plugin/internal/load/loadProjectPlugins.js +466 -416
- package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
- package/lib/plugin/internal/load/pluginResolutionFailure.d.ts +18 -0
- package/lib/plugin/internal/load/pluginResolutionFailure.js +32 -0
- package/lib/plugin/internal/load/pluginResolutionFailure.js.map +1 -0
- package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.d.ts +1 -1
- package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js +3 -2
- package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js.map +1 -1
- package/lib/structures/ITtscCompilerContext.d.ts +12 -12
- package/lib/structures/ITtscPlugin.d.ts +7 -5
- package/lib/structures/ITtscPluginFactoryContext.d.ts +9 -8
- package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +12 -6
- package/package.json +9 -9
- package/src/TtscCompiler.ts +1 -1
- package/src/compiler/internal/ResidentCheckProcess.ts +159 -374
- package/src/compiler/internal/ResidentCheckProcessHost.ts +502 -0
- package/src/compiler/internal/ResidentCheckProcessProtocol.ts +53 -0
- package/src/compiler/internal/build/BuildExecution.ts +118 -9
- package/src/compiler/internal/build/ResidentCheckWatchSession.ts +41 -24
- package/src/compiler/internal/build/RunBuildOptions.ts +9 -5
- package/src/compiler/internal/build/WatchInputPublication.ts +106 -0
- package/src/compiler/internal/build/runBuild.ts +58 -34
- package/src/compiler/internal/buildNativeCompiler.ts +4 -0
- package/src/compiler/internal/compileProjectInMemory.ts +8 -2
- package/src/compiler/internal/project/resolveOwningProjectConfig.ts +16 -5
- package/src/compiler/internal/readEffectiveCompilerOptions.ts +1 -1
- package/src/compiler/internal/resolveBinary.ts +4 -1
- package/src/compiler/internal/resolveTsgo.ts +7 -0
- package/src/compiler/internal/runSingleFileEmit.ts +139 -99
- package/src/compiler/internal/startResidentTransform.ts +9 -2
- package/src/compiler/internal/transformProjectInMemory.ts +12 -3
- package/src/compiler/internal/transformProjectInWorker.ts +10 -2
- package/src/internal/workerModuleUrl.ts +34 -0
- package/src/launcher/internal/buildSingleRootProject.ts +2 -0
- package/src/launcher/internal/getCompilerVersionText.ts +3 -2
- package/src/launcher/internal/prepareExecution.ts +2 -0
- package/src/launcher/internal/runTtsc.ts +123 -45
- package/src/launcher/internal/runtime/CommonJsRuntimeSource.ts +11 -9
- package/src/launcher/internal/runtime/advertiseTypeScriptExtensions.ts +25 -0
- package/src/launcher/internal/runtime/installRuntimeHooks.ts +7 -3
- package/src/launcher/internal/watch/AsyncWatchTopology.ts +267 -0
- package/src/launcher/internal/watch/WatchSettlement.ts +67 -0
- package/src/launcher/internal/watch/WatchTopology.ts +84 -13
- package/src/launcher/internal/watch/WatchTopologyWorkerProtocol.ts +106 -0
- package/src/launcher/internal/watch/serveWatchTopologyWorker.ts +75 -0
- package/src/launcher/internal/watch/watchTopologyWorker.ts +29 -0
- package/src/plugin/CapabilityPluginResolver.ts +9 -2
- package/src/plugin/internal/load/PluginDescriptorEvaluationCache.ts +17 -12
- package/src/plugin/internal/load/PluginPackageResolution.ts +14 -9
- package/src/plugin/internal/load/ProjectPluginEntries.ts +40 -15
- package/src/plugin/internal/load/collectProjectHostInputs.ts +7 -1
- package/src/plugin/internal/load/hasProjectPluginEntries.ts +8 -3
- package/src/plugin/internal/load/loadProjectPlugins.ts +672 -594
- package/src/plugin/internal/load/pluginResolutionFailure.ts +37 -0
- package/src/plugin/internal/source/PluginBuildEnvironmentReadings.ts +8 -2
- package/src/structures/ITtscCompilerContext.ts +12 -12
- package/src/structures/ITtscPlugin.ts +7 -5
- package/src/structures/ITtscPluginFactoryContext.ts +9 -8
- package/src/structures/internal/TtscSingleFileEmitOptions.ts +22 -11
|
@@ -1,162 +1,133 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { Worker } from "node:worker_threads";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { workerModuleUrl } from "../../internal/workerModuleUrl";
|
|
5
5
|
import type { ResidentCheckProcessOptions } from "./ResidentCheckProcessOptions";
|
|
6
|
+
import type { ResidentCheckProcessProtocol } from "./ResidentCheckProcessProtocol";
|
|
6
7
|
import type { ResidentCheckRequest } from "./ResidentCheckRequest";
|
|
7
8
|
import type { ResidentCheckResult } from "./ResidentCheckResult";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
* FIFO
|
|
11
|
+
* FIFO check client whose worker owns the child and its complete pipe lifetime.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Watch topology can block the launcher's loop after shutdown timers start.
|
|
14
|
+
* Keeping spawn, EOF, signals, output draining and close on one independent
|
|
15
|
+
* loop prevents that work from spending a live child's shutdown grace or
|
|
16
|
+
* overtaking its pending close. An exit code alone never proves pipe closure: a
|
|
17
|
+
* living descendant can retain inherited stdout or stderr (#1758).
|
|
17
18
|
*
|
|
18
|
-
* The caller owns disposal.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* pipes, request listeners, timers and event-loop process reference while
|
|
25
|
-
* keeping joining failed; the client object still holds its ChildProcess. It
|
|
26
|
-
* neither kills unrelated descendants nor certifies their termination. A forced
|
|
27
|
-
* or unjoined process is not a graceful shutdown. These delays are event-loop
|
|
28
|
-
* timer policies, not hard elapsed-time bounds; synchronous blocking or
|
|
29
|
-
* callback starvation can postpone their delivery.
|
|
19
|
+
* The caller owns disposal. Retirement uses the host's one-second input flush,
|
|
20
|
+
* EOF grace and forced-join policies. Only its actual-close receipt authorizes
|
|
21
|
+
* replacement; worker failure leaves joining unknown and rejected. Strict close
|
|
22
|
+
* additionally requires a graceful zero exit. Neither worker nor client claims
|
|
23
|
+
* termination of unrelated descendants. Abrupt process exit can still interrupt
|
|
24
|
+
* cleanup.
|
|
30
25
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* @evidence contracts/
|
|
37
|
-
* @evidence contracts/
|
|
38
|
-
* @evidence contracts/
|
|
26
|
+
* One worker serves one child across requests. Messages copy serialized request
|
|
27
|
+
* and reply bytes; pending request count and reply size have no fixed cap.
|
|
28
|
+
* Retirement rejects pending callers immediately and joins its worker after the
|
|
29
|
+
* terminal receipt. The client retains the Worker object.
|
|
30
|
+
*
|
|
31
|
+
* @evidence contracts/common.md#principled-implementation The independent loop owns every observation needed for retirement, and only its actual child-close receipt permits replacement, keeping parent event-loop blocking outside shutdown timing.
|
|
32
|
+
* @evidence contracts/common.md#clear-and-simple-design The client owns caller serialization and settlement; its worker owns the native transport and shutdown state machine.
|
|
33
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts No elapsed parent deadline, worker exit, signal attempt or exit code fabricates a joined child; unknown worker failure rejects joining.
|
|
34
|
+
* @evidence contracts/common.md#meaningful-documentation Native prose documents the blocking hazard, inherited-pipe authority, disposal, failure effects and unbounded request population.
|
|
35
|
+
* @evidence contracts/portability.md#os-neutral-implementation Node loads a sibling worker using the current source or emitted extension and inherited loader arguments; the host uses native executable argv and pipes without shell commands.
|
|
36
|
+
* @evidence contracts/performance.md#efficient-algorithms Serialization and structured cloning visit message bytes; a request-id map gives constant-time settlement without scanning earlier requests. Worker startup and one thread per active child are additional costs; no speedup is claimed.
|
|
37
|
+
* @evidence contracts/performance.md#reuse-equivalent-work One worker and child serve the startup-selected check process across FIFO requests; each request remains effectful and receives its own result.
|
|
38
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources The client owns one worker and one resolver pair per pending request. Results or retirement remove resolvers; joining waits for the terminal receipt and worker exit. Unexpected worker failure rejects callers and joining and removes worker liveness. Request count and bytes are uncapped, and the client object retains its Worker reference.
|
|
39
39
|
*/
|
|
40
40
|
export class ResidentCheckProcess {
|
|
41
|
-
private readonly
|
|
42
|
-
private readonly pending
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
private readonly worker: Worker;
|
|
42
|
+
private readonly pending = new Map<
|
|
43
|
+
number,
|
|
44
|
+
{
|
|
45
|
+
resolve(result: ResidentCheckResult): void;
|
|
46
|
+
reject(error: Error): void;
|
|
47
|
+
}
|
|
48
|
+
>();
|
|
49
|
+
private ordinal = 0;
|
|
45
50
|
private failure: Error | undefined;
|
|
46
|
-
private
|
|
51
|
+
private closeError: Error | undefined;
|
|
52
|
+
private joinError: Error | undefined;
|
|
53
|
+
private terminal = false;
|
|
47
54
|
private readonly exited: Promise<void>;
|
|
48
|
-
private
|
|
49
|
-
private
|
|
50
|
-
private forced = false;
|
|
51
|
-
private flushDeadline: NodeJS.Timeout | undefined;
|
|
52
|
-
private terminationTimer: NodeJS.Timeout | undefined;
|
|
53
|
-
private terminationDeadline: NodeJS.Timeout | undefined;
|
|
54
|
-
private releaseTransport: (() => void) | undefined;
|
|
55
|
+
private resolveExit!: () => void;
|
|
56
|
+
private rejectExit!: (error: Error) => void;
|
|
55
57
|
|
|
56
58
|
public constructor(options: ResidentCheckProcessOptions) {
|
|
57
|
-
const nativeArgs = [...options.args];
|
|
58
|
-
const trace = E2ETrace.begin(
|
|
59
|
-
options.binary,
|
|
60
|
-
nativeArgs,
|
|
61
|
-
options,
|
|
62
|
-
"resident-check",
|
|
63
|
-
);
|
|
64
|
-
this.child = spawn(options.binary, nativeArgs, {
|
|
65
|
-
cwd: options.cwd,
|
|
66
|
-
env: options.env,
|
|
67
|
-
windowsHide: true,
|
|
68
|
-
});
|
|
69
|
-
E2ETrace.asynchronous(trace, this.child);
|
|
70
|
-
let onChildClose: (() => void) | undefined;
|
|
71
59
|
this.exited = new Promise<void>((resolve, reject) => {
|
|
72
|
-
|
|
73
|
-
this.didClose = true;
|
|
74
|
-
clearTimeout(this.flushDeadline);
|
|
75
|
-
clearTimeout(this.terminationTimer);
|
|
76
|
-
clearTimeout(this.terminationDeadline);
|
|
77
|
-
this.releaseTransport = undefined;
|
|
78
|
-
this.reader?.close();
|
|
79
|
-
resolve();
|
|
80
|
-
};
|
|
81
|
-
this.child.once("close", onChildClose);
|
|
60
|
+
this.resolveExit = resolve;
|
|
82
61
|
this.rejectExit = reject;
|
|
83
62
|
});
|
|
84
|
-
// Legacy dispose initiates retirement without returning a promise. Keep its
|
|
85
|
-
// rejected join observable to awaiters without an unhandled rejection.
|
|
86
63
|
void this.exited.catch(() => {});
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
stream?.off("error", onError);
|
|
123
|
-
stream?.off("error", ResidentCheckProcess.ignoreTransportError);
|
|
124
|
-
stream?.on("error", ResidentCheckProcess.ignoreTransportError);
|
|
125
|
-
stream?.destroy();
|
|
64
|
+
this.worker = new Worker(
|
|
65
|
+
workerModuleUrl(
|
|
66
|
+
path.join(
|
|
67
|
+
__dirname,
|
|
68
|
+
`ResidentCheckProcessHost${path.extname(__filename)}`,
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
{
|
|
72
|
+
workerData: {
|
|
73
|
+
...options,
|
|
74
|
+
args: [...options.args],
|
|
75
|
+
env: { ...options.env },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
this.worker.on("message", (reply: ResidentCheckProcessProtocol.Reply) => {
|
|
80
|
+
if (this.terminal) return;
|
|
81
|
+
if (reply.kind === "terminal") {
|
|
82
|
+
this.terminal = true;
|
|
83
|
+
this.joinError = reply.joinError;
|
|
84
|
+
this.closeError = reply.closeError;
|
|
85
|
+
this.fail(
|
|
86
|
+
reply.joinError ??
|
|
87
|
+
reply.closeError ??
|
|
88
|
+
new Error("ttsc: resident check host exited"),
|
|
89
|
+
);
|
|
90
|
+
} else {
|
|
91
|
+
const pending = this.pending.get(reply.id);
|
|
92
|
+
if (pending === undefined) return;
|
|
93
|
+
this.pending.delete(reply.id);
|
|
94
|
+
if (reply.kind === "result") pending.resolve(reply.result);
|
|
95
|
+
else {
|
|
96
|
+
pending.reject(reply.error);
|
|
97
|
+
this.fail(reply.error);
|
|
98
|
+
}
|
|
126
99
|
}
|
|
127
|
-
|
|
128
|
-
|
|
100
|
+
});
|
|
101
|
+
this.worker.on("error", (error) => this.workerFailed(asError(error)));
|
|
102
|
+
this.worker.once("exit", (code) => {
|
|
103
|
+
if (!this.terminal)
|
|
104
|
+
this.workerFailed(
|
|
105
|
+
new Error(
|
|
106
|
+
`ttsc: resident check process owner exited before child retirement (code ${code})`,
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
else if (this.joinError === undefined) this.resolveExit();
|
|
110
|
+
else this.rejectExit(this.joinError);
|
|
111
|
+
});
|
|
129
112
|
}
|
|
130
113
|
|
|
131
114
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* that retired the process, so the caller can fall back to a one-shot check
|
|
137
|
-
* for that cycle.
|
|
115
|
+
* Serialize one cycle before admission and pair its reply with this caller.
|
|
116
|
+
* Serialization failure rejects only this call when its thrown value can be
|
|
117
|
+
* normalized to an Error; normalization itself can throw synchronously. A
|
|
118
|
+
* retired transport rejects all admitted calls and every later request.
|
|
138
119
|
*
|
|
139
|
-
* Serialization precedes
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* @evidence contracts/
|
|
144
|
-
* @evidence contracts/
|
|
145
|
-
* @evidence contracts/
|
|
146
|
-
* @evidence contracts/
|
|
147
|
-
* @evidence contracts/portability.md#os-neutral-implementation JSON lines pass native change paths as data over Node's stdin stream, without constructing a shell command or converting native separators.
|
|
148
|
-
* @evidence contracts/performance.md#efficient-algorithms Encoding visits supplied properties and output bytes, with caller-defined conversion not bounded by final JSON size; enqueueing and settlement have amortized constant queue work plus reply parsing. Native check work is delegated rather than bounded by transport cost.
|
|
149
|
-
* @evidence contracts/performance.md#reuse-equivalent-work Each cycle is sent to the existing fixed-configuration Program owner; changes are effectful transitions and are not coalesced merely because request values match.
|
|
150
|
-
* @evidence contracts/performance.md#bound-retention-and-release-resources The client retains one resolver pair per outstanding call until reply or retirement; serialization allocates one request line, and live requests wait without an implicit timeout or population limit.
|
|
120
|
+
* @evidence contracts/common.md#principled-implementation Serialization precedes worker admission; identifiers pair results with their admitted callers while the host preserves FIFO check execution.
|
|
121
|
+
* @evidence contracts/common.md#clear-and-simple-design Caller serialization stays in the client, and the host alone validates native replies and retires a failed transport.
|
|
122
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts A failed conversion adds no request slot; worker failure rejects rather than fabricating a check result.
|
|
123
|
+
* @evidence contracts/common.md#meaningful-documentation Native prose distinguishes serialization rejection and synchronous normalization failure from terminal transport failure.
|
|
124
|
+
* @evidence contracts/portability.md#os-neutral-implementation Native input paths remain JSON data across the message and pipe boundaries without shell interpolation.
|
|
125
|
+
* @evidence contracts/performance.md#efficient-algorithms JSON conversion and message copying visit input bytes, caller-defined conversion can add arbitrary work, and map admission and settlement use constant-time key operations.
|
|
126
|
+
* @evidence contracts/performance.md#reuse-equivalent-work Each effectful cycle owns a request and reply even when its payload matches an earlier cycle.
|
|
127
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources Each admitted call retains resolver callbacks until its result or shared retirement; request population and serialized bytes have no fixed cap.
|
|
151
128
|
*/
|
|
152
129
|
public request(payload: ResidentCheckRequest): Promise<ResidentCheckResult> {
|
|
153
130
|
if (this.failure !== undefined) return Promise.reject(this.failure);
|
|
154
|
-
const stdin = this.child.stdin;
|
|
155
|
-
if (stdin === null || stdin.destroyed) {
|
|
156
|
-
const error = new Error("ttsc: resident check host stdin is closed");
|
|
157
|
-
this.fail(error);
|
|
158
|
-
return Promise.reject(error);
|
|
159
|
-
}
|
|
160
131
|
let line: string;
|
|
161
132
|
try {
|
|
162
133
|
line = `${JSON.stringify(payload)}\n`;
|
|
@@ -164,280 +135,94 @@ export class ResidentCheckProcess {
|
|
|
164
135
|
return Promise.reject(asError(error));
|
|
165
136
|
}
|
|
166
137
|
return new Promise<ResidentCheckResult>((resolve, reject) => {
|
|
167
|
-
const
|
|
168
|
-
this.pending.
|
|
138
|
+
const id = ++this.ordinal;
|
|
139
|
+
this.pending.set(id, { resolve, reject });
|
|
169
140
|
try {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
});
|
|
141
|
+
this.worker.postMessage({
|
|
142
|
+
kind: "request",
|
|
143
|
+
id,
|
|
144
|
+
line,
|
|
145
|
+
} satisfies ResidentCheckProcessProtocol.Command);
|
|
175
146
|
} catch (error) {
|
|
176
|
-
|
|
177
|
-
this.settle(pending, reason);
|
|
178
|
-
this.fail(reason);
|
|
147
|
+
this.workerFailed(asError(error));
|
|
179
148
|
}
|
|
180
149
|
});
|
|
181
150
|
}
|
|
182
151
|
|
|
183
152
|
/**
|
|
184
|
-
* Retire
|
|
185
|
-
* Idempotent; a process that already failed is left as is.
|
|
153
|
+
* Retire once and reject pending callers without waiting for native close.
|
|
186
154
|
*
|
|
187
|
-
* @evidence contracts/common.md#principled-implementation
|
|
188
|
-
* @evidence contracts/common.md#clear-and-simple-design Disposal
|
|
189
|
-
* @evidence contracts/common.md#prohibited-implementation-shortcuts
|
|
190
|
-
* @evidence contracts/common.md#meaningful-documentation
|
|
191
|
-
* @evidence contracts/portability.md#os-neutral-implementation
|
|
192
|
-
* @evidence contracts/performance.md#efficient-algorithms
|
|
193
|
-
*
|
|
194
|
-
* @
|
|
195
|
-
*
|
|
196
|
-
* @evidence contracts/performance.md#bound-retention-and-release-resources Disposal clears pending callbacks and requests stdin EOF while preserving output until actual close or a failed join timer. Failed join destroys/unrefs owned transport and removes request listeners without proving child/descendant termination; timer delivery depends on the event loop, and the client object can still retain its ChildProcess.
|
|
155
|
+
* @evidence contracts/common.md#principled-implementation Terminal caller failure is set before rejection, and one retire command delegates EOF and joining to the owner.
|
|
156
|
+
* @evidence contracts/common.md#clear-and-simple-design Disposal initiates the host's existing retirement state machine rather than maintaining parent-side timers.
|
|
157
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Repeated disposal cannot revive a request or authorize replacement before actual joining.
|
|
158
|
+
* @evidence contracts/common.md#meaningful-documentation Native prose identifies immediate rejection and asynchronous native retirement.
|
|
159
|
+
* @evidence contracts/portability.md#os-neutral-implementation A Node message delegates native EOF and signaling without platform-specific PID commands.
|
|
160
|
+
* @evidence contracts/performance.md#efficient-algorithms Rejection visits the outstanding request map once and posts one constant-size retirement command.
|
|
161
|
+
* @evidenceExclude contracts/performance.md#reuse-equivalent-work Disposal invalidates an owner without creating a reusable computation.
|
|
162
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources Pending callbacks are released immediately; the referenced worker keeps cleanup alive until its terminal receipt and exit, and unknown ownership remains failure.
|
|
197
163
|
*/
|
|
198
164
|
public dispose(): void {
|
|
165
|
+
if (this.failure !== undefined) return;
|
|
199
166
|
this.fail(new Error("ttsc: resident check host disposed"));
|
|
167
|
+
try {
|
|
168
|
+
this.worker.postMessage({
|
|
169
|
+
kind: "retire",
|
|
170
|
+
} satisfies ResidentCheckProcessProtocol.Command);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
this.workerFailed(asError(error));
|
|
173
|
+
}
|
|
200
174
|
}
|
|
201
175
|
|
|
202
176
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* retirement.
|
|
177
|
+
* Await actual child close and the owner thread, requiring graceful zero
|
|
178
|
+
* exit. Repeated calls share retirement but retain strict shutdown failure.
|
|
206
179
|
*
|
|
207
|
-
* @evidence contracts/common.md#principled-implementation
|
|
208
|
-
* @evidence contracts/common.md#clear-and-simple-design
|
|
209
|
-
* @evidence contracts/common.md#prohibited-implementation-shortcuts
|
|
210
|
-
* @evidence contracts/common.md#meaningful-documentation Native prose states
|
|
211
|
-
* @evidence contracts/portability.md#os-neutral-implementation
|
|
212
|
-
* @evidence contracts/performance.md#efficient-algorithms Closing
|
|
213
|
-
* @evidence contracts/performance.md#reuse-equivalent-work Repeated closes
|
|
214
|
-
* @evidence contracts/performance.md#bound-retention-and-release-resources
|
|
180
|
+
* @evidence contracts/common.md#principled-implementation Success requires the host's actual-close receipt, strict status validation and owner-thread exit.
|
|
181
|
+
* @evidence contracts/common.md#clear-and-simple-design Strict shutdown adds status validation to the same joining promise used by recovery.
|
|
182
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Neither worker disappearance nor requested forced termination substitutes for graceful child close.
|
|
183
|
+
* @evidence contracts/common.md#meaningful-documentation Native prose states child and thread joining, strict zero exit and repeated failure behavior.
|
|
184
|
+
* @evidence contracts/portability.md#os-neutral-implementation Node worker messages carry the native host's observed close outcome without interpreting a PID or shell status.
|
|
185
|
+
* @evidence contracts/performance.md#efficient-algorithms Closing reuses retirement and performs constant-time outcome validation after its one pending-request traversal.
|
|
186
|
+
* @evidence contracts/performance.md#reuse-equivalent-work Repeated closes await this owner's single terminal promise while retaining strict status independently from recovery joining.
|
|
187
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources The host releases its pipes and timers on actual close or failed joining; the client awaits worker exit, releases pending callbacks, and preserves unknown ownership as rejection.
|
|
215
188
|
*/
|
|
216
189
|
public async close(): Promise<void> {
|
|
217
190
|
this.dispose();
|
|
218
191
|
await this.exited;
|
|
219
|
-
if (this.
|
|
220
|
-
throw new Error("ttsc: resident check host required forced termination");
|
|
221
|
-
if (this.child.exitCode !== 0 || this.child.signalCode !== null)
|
|
222
|
-
throw this.exitError();
|
|
192
|
+
if (this.closeError !== undefined) throw this.closeError;
|
|
223
193
|
}
|
|
224
194
|
|
|
225
195
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
196
|
+
* Join a known failed child and its owner before replacement. Waiting alone
|
|
197
|
+
* starts no retirement deadline; unknown ownership rejects.
|
|
228
198
|
*
|
|
229
|
-
* @evidence contracts/common.md#principled-implementation A
|
|
230
|
-
* @evidence contracts/common.md#clear-and-simple-design
|
|
231
|
-
* @evidence contracts/common.md#prohibited-implementation-shortcuts
|
|
232
|
-
* @evidence contracts/common.md#meaningful-documentation Native prose
|
|
233
|
-
* @evidence contracts/portability.md#os-neutral-implementation The
|
|
234
|
-
* @evidence contracts/performance.md#efficient-algorithms
|
|
235
|
-
* @evidence contracts/performance.md#reuse-equivalent-work
|
|
236
|
-
* @evidence contracts/performance.md#bound-retention-and-release-resources
|
|
199
|
+
* @evidence contracts/common.md#principled-implementation A known failed child can be replaced only after its actual-close receipt and worker exit.
|
|
200
|
+
* @evidence contracts/common.md#clear-and-simple-design Recovery consumes joining separately from strict shutdown status.
|
|
201
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Unknown worker or pipe ownership remains rejection rather than successful retirement.
|
|
202
|
+
* @evidence contracts/common.md#meaningful-documentation Native prose states replacement authority, passive waiting and unknown failure.
|
|
203
|
+
* @evidence contracts/portability.md#os-neutral-implementation The owner observes native close through Node APIs instead of treating signals or process IDs as portable joining proof.
|
|
204
|
+
* @evidence contracts/performance.md#efficient-algorithms The accessor returns an existing promise without timers, native probes or additional listeners.
|
|
205
|
+
* @evidence contracts/performance.md#reuse-equivalent-work All waiters share the same owner's terminal promise; replacement owns a new worker and promise.
|
|
206
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources Passive waiting can remain pending for a live unretired host; active retirement releases owned resources through its existing deadlines and terminal worker exit.
|
|
237
207
|
*/
|
|
238
208
|
public waitForExit(): Promise<void> {
|
|
239
209
|
return this.exited;
|
|
240
210
|
}
|
|
241
211
|
|
|
242
|
-
private rejectExit: ((error: Error) => void) | undefined;
|
|
243
|
-
|
|
244
|
-
private onLine(line: string): void {
|
|
245
|
-
if (this.failure !== undefined || line.trim().length === 0) return;
|
|
246
|
-
const pending = this.pending[this.pendingHead];
|
|
247
|
-
if (pending === undefined) {
|
|
248
|
-
this.fail(
|
|
249
|
-
new Error("ttsc: resident check host sent an unsolicited reply"),
|
|
250
|
-
);
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
const result = parseResidentCheckResult(line);
|
|
254
|
-
if (result === undefined) {
|
|
255
|
-
const error = new Error(
|
|
256
|
-
`ttsc: resident check host sent a malformed reply: ${echoLine(line)}`,
|
|
257
|
-
);
|
|
258
|
-
this.settle(pending, error);
|
|
259
|
-
this.fail(error);
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
this.settle(pending, result);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
private settle(
|
|
266
|
-
pending: PendingRequest,
|
|
267
|
-
result: Error | ResidentCheckResult,
|
|
268
|
-
): void {
|
|
269
|
-
if (pending.settled) return;
|
|
270
|
-
pending.settled = true;
|
|
271
|
-
if (this.pending[this.pendingHead] === pending) {
|
|
272
|
-
this.pending[this.pendingHead++] = undefined;
|
|
273
|
-
if (this.pendingHead * 2 >= this.pending.length) {
|
|
274
|
-
this.pending.splice(0, this.pendingHead);
|
|
275
|
-
this.pendingHead = 0;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
if (result instanceof Error) pending.reject(result);
|
|
279
|
-
else pending.resolve(result);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
212
|
private fail(error: Error): void {
|
|
283
|
-
|
|
284
|
-
this.failure
|
|
285
|
-
|
|
286
|
-
this.pendingHead = 0;
|
|
287
|
-
for (const request of pending) {
|
|
288
|
-
if (request !== undefined) this.settle(request, error);
|
|
289
|
-
}
|
|
290
|
-
this.terminate();
|
|
213
|
+
this.failure ??= error;
|
|
214
|
+
for (const pending of this.pending.values()) pending.reject(this.failure);
|
|
215
|
+
this.pending.clear();
|
|
291
216
|
}
|
|
292
217
|
|
|
293
|
-
private
|
|
294
|
-
if (this.
|
|
295
|
-
this.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
// A blocked parent event loop must not spend the child's shutdown grace
|
|
301
|
-
// before that delivery can happen.
|
|
302
|
-
stdin.end(() => this.startTerminationGrace());
|
|
303
|
-
// Start delivery's own deadline after the caller gives Node control.
|
|
304
|
-
// This bounds an unread full pipe without charging synchronous caller
|
|
305
|
-
// work against either delivery or the child's later EOF grace.
|
|
306
|
-
queueMicrotask(() => {
|
|
307
|
-
if (this.didClose || this.terminationTimer !== undefined) return;
|
|
308
|
-
this.flushDeadline = setTimeout(
|
|
309
|
-
() => this.forceTerminationAndJoin(),
|
|
310
|
-
TERMINATION_GRACE_MS,
|
|
311
|
-
);
|
|
312
|
-
});
|
|
313
|
-
return;
|
|
314
|
-
} catch {
|
|
315
|
-
stdin.destroy();
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
this.startTerminationGrace();
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
private startTerminationGrace(): void {
|
|
322
|
-
clearTimeout(this.flushDeadline);
|
|
323
|
-
// Keep output pipes readable until close: destroying them would discard the
|
|
324
|
-
// last reply and confuse a requested EOF with transport truncation.
|
|
325
|
-
if (
|
|
326
|
-
this.didClose ||
|
|
327
|
-
this.terminationTimer !== undefined ||
|
|
328
|
-
this.terminationDeadline !== undefined
|
|
329
|
-
)
|
|
330
|
-
return;
|
|
331
|
-
this.terminationTimer = setTimeout(
|
|
332
|
-
() => this.forceTerminationAndJoin(),
|
|
333
|
-
TERMINATION_GRACE_MS,
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
private forceTerminationAndJoin(): void {
|
|
338
|
-
if (this.didClose || this.terminationDeadline !== undefined) return;
|
|
339
|
-
if (this.child.exitCode === null && this.child.signalCode === null) {
|
|
340
|
-
this.forced = true;
|
|
341
|
-
try {
|
|
342
|
-
this.child.kill("SIGKILL");
|
|
343
|
-
} catch {
|
|
344
|
-
// The deadline still requires actual close, even if kill fails.
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
this.terminationDeadline = setTimeout(() => {
|
|
348
|
-
this.rejectExit?.(
|
|
349
|
-
new Error("ttsc: resident check host did not close after termination"),
|
|
350
|
-
);
|
|
351
|
-
// Reject first: destroying inherited pipes can cause Node's close event,
|
|
352
|
-
// but releasing our handles cannot prove a graceful physical join.
|
|
353
|
-
this.releaseTransport?.();
|
|
354
|
-
this.releaseTransport = undefined;
|
|
355
|
-
}, TERMINATION_GRACE_MS);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
private static ignoreTransportError(): void {}
|
|
359
|
-
|
|
360
|
-
private exitError(): Error {
|
|
361
|
-
const detail = this.stderr.trim();
|
|
362
|
-
if (detail.length !== 0) return new Error(detail);
|
|
363
|
-
const signal =
|
|
364
|
-
this.child.signalCode === null ? "" : `, signal ${this.child.signalCode}`;
|
|
365
|
-
return new Error(
|
|
366
|
-
`ttsc: resident check host exited (code ${
|
|
367
|
-
this.child.exitCode ?? "null"
|
|
368
|
-
}${signal})`,
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const STDERR_TAIL_LIMIT = 64 * 1024;
|
|
374
|
-
|
|
375
|
-
const REPLY_ECHO_LIMIT = 200;
|
|
376
|
-
|
|
377
|
-
const TERMINATION_GRACE_MS = 1_000;
|
|
378
|
-
|
|
379
|
-
type PendingRequest = {
|
|
380
|
-
reject(reason: Error): void;
|
|
381
|
-
resolve(result: ResidentCheckResult): void;
|
|
382
|
-
settled: boolean;
|
|
383
|
-
};
|
|
384
|
-
function parseResidentCheckResult(
|
|
385
|
-
line: string,
|
|
386
|
-
): ResidentCheckResult | undefined {
|
|
387
|
-
let value: unknown;
|
|
388
|
-
try {
|
|
389
|
-
value = JSON.parse(line);
|
|
390
|
-
} catch {
|
|
391
|
-
return undefined;
|
|
218
|
+
private workerFailed(error: Error): void {
|
|
219
|
+
if (this.terminal) return;
|
|
220
|
+
this.terminal = true;
|
|
221
|
+
this.joinError = error;
|
|
222
|
+
this.fail(error);
|
|
223
|
+
this.rejectExit(error);
|
|
224
|
+
this.worker.unref();
|
|
392
225
|
}
|
|
393
|
-
if (
|
|
394
|
-
!isRecord(value) ||
|
|
395
|
-
typeof value.status !== "number" ||
|
|
396
|
-
!Number.isInteger(value.status)
|
|
397
|
-
) {
|
|
398
|
-
return undefined;
|
|
399
|
-
}
|
|
400
|
-
if (typeof value.stdout !== "string" || typeof value.stderr !== "string") {
|
|
401
|
-
return undefined;
|
|
402
|
-
}
|
|
403
|
-
const telemetry = value.telemetry;
|
|
404
|
-
if (
|
|
405
|
-
!isRecord(telemetry) ||
|
|
406
|
-
typeof telemetry.pid !== "number" ||
|
|
407
|
-
!Number.isSafeInteger(telemetry.pid) ||
|
|
408
|
-
typeof telemetry.programLoads !== "number" ||
|
|
409
|
-
!Number.isSafeInteger(telemetry.programLoads) ||
|
|
410
|
-
typeof telemetry.programUpdates !== "number" ||
|
|
411
|
-
!Number.isSafeInteger(telemetry.programUpdates) ||
|
|
412
|
-
typeof telemetry.reused !== "boolean"
|
|
413
|
-
) {
|
|
414
|
-
return undefined;
|
|
415
|
-
}
|
|
416
|
-
return {
|
|
417
|
-
diagnostics: [],
|
|
418
|
-
status: value.status,
|
|
419
|
-
stderr: value.stderr,
|
|
420
|
-
stdout: value.stdout,
|
|
421
|
-
telemetry: {
|
|
422
|
-
pid: telemetry.pid,
|
|
423
|
-
programLoads: telemetry.programLoads,
|
|
424
|
-
programUpdates: telemetry.programUpdates,
|
|
425
|
-
reused: telemetry.reused,
|
|
426
|
-
},
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
431
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function echoLine(line: string): string {
|
|
435
|
-
const trimmed = line.trim();
|
|
436
|
-
return JSON.stringify(
|
|
437
|
-
trimmed.length <= REPLY_ECHO_LIMIT
|
|
438
|
-
? trimmed
|
|
439
|
-
: `${trimmed.slice(0, REPLY_ECHO_LIMIT)}…`,
|
|
440
|
-
);
|
|
441
226
|
}
|
|
442
227
|
|
|
443
228
|
function asError(error: unknown): Error {
|