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
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
+
import { type Interface, createInterface } from "node:readline";
|
|
3
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
4
|
+
|
|
5
|
+
import { E2ETrace } from "../../internal/E2ETrace";
|
|
6
|
+
import type { ResidentCheckProcessOptions } from "./ResidentCheckProcessOptions";
|
|
7
|
+
import type { ResidentCheckProcessProtocol } from "./ResidentCheckProcessProtocol";
|
|
8
|
+
import type { ResidentCheckResult } from "./ResidentCheckResult";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Worker-owned FIFO JSON-line transport for a `check-serve` sidecar.
|
|
12
|
+
*
|
|
13
|
+
* A watch session serializes cycles, but the client still queues replies so a
|
|
14
|
+
* caller cannot accidentally pair a late response with the next request. Any
|
|
15
|
+
* framing failure retires the process; the launcher can fall back to an
|
|
16
|
+
* ordinary one-shot command after joining the original child. An unjoined child
|
|
17
|
+
* cannot authorize that replacement.
|
|
18
|
+
*
|
|
19
|
+
* The caller owns disposal. Requests have no deadline, and outstanding request
|
|
20
|
+
* count and reply-line size are not capped; a slow live check remains pending.
|
|
21
|
+
* This loop owns the child independently of launcher callbacks. Retirement
|
|
22
|
+
* stops JSON-line assembly, drains raw output, and waits for actual close. A
|
|
23
|
+
* one-second grace period starts after stdin finishes, followed by forced
|
|
24
|
+
* termination and a one-second join deadline. Pending stdin writes have a
|
|
25
|
+
* one-second flush deadline once the retiring call stack yields. An unjoined
|
|
26
|
+
* deadline releases this client's pipes, request listeners, timers and
|
|
27
|
+
* event-loop process reference while keeping joining failed; the client object
|
|
28
|
+
* still holds its ChildProcess. It neither kills unrelated descendants nor
|
|
29
|
+
* certifies their termination. A forced or unjoined process is not a graceful
|
|
30
|
+
* shutdown. These delays are event-loop timer policies, not hard elapsed-time
|
|
31
|
+
* bounds; synchronous blocking or callback starvation can postpone their
|
|
32
|
+
* delivery.
|
|
33
|
+
*
|
|
34
|
+
* @evidence contracts/common.md#principled-implementation One positional reply consumes one queued cycle; invalid framing or shape retires the stream so a delayed reply cannot answer a different cycle.
|
|
35
|
+
* @evidence contracts/common.md#clear-and-simple-design One client owns its child, line reader, FIFO and failure state; private parsing and settlement keep transport policy separate from the watch coordinator's fallback.
|
|
36
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Protocol validation uses the declared reply fields, and transport failure is rejection rather than a fabricated successful check or a consumer-specific recovery.
|
|
37
|
+
* @evidence contracts/common.md#meaningful-documentation Native paragraphs explain FIFO ownership, retirement, caller disposal and the absence of request bounds, following the documentation skill.
|
|
38
|
+
* @evidence contracts/portability.md#os-neutral-implementation Node spawns the supplied native executable with an argv array and pipe streams, without shell quoting; cwd and environment retain caller-provided native semantics.
|
|
39
|
+
* @evidence contracts/performance.md#efficient-algorithms A head cursor consumes replies with amortized constant queue work and failure drains outstanding requests once. Writing and decoding visit line bytes; caller-defined serialization belongs to the client. Stderr decoding and concatenation scan the new chunk plus retained tail before truncation. Native spawn, check execution and shutdown completion remain delegated and event-loop scheduled.
|
|
40
|
+
* @evidence contracts/performance.md#reuse-equivalent-work One startup-selected child is shared across FIFO cycles, but its native session can update or reconstruct Program generations. Changed/external paths travel with each request and the watch owner replaces the process on configuration/plugin identity changes; the client does not reuse a response or certify every custom host's internal Program cache.
|
|
41
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources Each settled slot releases its request; retirement clears the queue, stops line assembly and drains raw output until actual close or the failed join deadline. That deadline releases owned transport handles and callbacks without claiming the physical child or unrelated descendants joined; live request count and line bytes have no fixed cap.
|
|
42
|
+
*/
|
|
43
|
+
export class ResidentCheckProcessHost {
|
|
44
|
+
private readonly child: ChildProcess;
|
|
45
|
+
private readonly pending: (PendingRequest | undefined)[] = [];
|
|
46
|
+
private pendingHead = 0;
|
|
47
|
+
private readonly reader: Interface;
|
|
48
|
+
private failure: Error | undefined;
|
|
49
|
+
private stderr = "";
|
|
50
|
+
private readonly exited: Promise<void>;
|
|
51
|
+
private closing = false;
|
|
52
|
+
private didClose = false;
|
|
53
|
+
private forced = false;
|
|
54
|
+
private flushDeadline: NodeJS.Timeout | undefined;
|
|
55
|
+
private terminationTimer: NodeJS.Timeout | undefined;
|
|
56
|
+
private terminationDeadline: NodeJS.Timeout | undefined;
|
|
57
|
+
private releaseTransport: (() => void) | undefined;
|
|
58
|
+
|
|
59
|
+
public constructor(options: ResidentCheckProcessOptions) {
|
|
60
|
+
const nativeArgs = [...options.args];
|
|
61
|
+
const trace = E2ETrace.begin(
|
|
62
|
+
options.binary,
|
|
63
|
+
nativeArgs,
|
|
64
|
+
options,
|
|
65
|
+
"resident-check",
|
|
66
|
+
);
|
|
67
|
+
this.child = spawn(options.binary, nativeArgs, {
|
|
68
|
+
cwd: options.cwd,
|
|
69
|
+
env: options.env,
|
|
70
|
+
windowsHide: true,
|
|
71
|
+
});
|
|
72
|
+
E2ETrace.asynchronous(trace, this.child);
|
|
73
|
+
let onChildClose: (() => void) | undefined;
|
|
74
|
+
this.exited = new Promise<void>((resolve, reject) => {
|
|
75
|
+
onChildClose = () => {
|
|
76
|
+
this.didClose = true;
|
|
77
|
+
clearTimeout(this.flushDeadline);
|
|
78
|
+
clearTimeout(this.terminationTimer);
|
|
79
|
+
clearTimeout(this.terminationDeadline);
|
|
80
|
+
this.releaseTransport = undefined;
|
|
81
|
+
this.reader?.close();
|
|
82
|
+
resolve();
|
|
83
|
+
};
|
|
84
|
+
this.child.once("close", onChildClose);
|
|
85
|
+
this.rejectExit = reject;
|
|
86
|
+
});
|
|
87
|
+
// Legacy dispose initiates retirement without returning a promise. Keep its
|
|
88
|
+
// rejected join observable to awaiters without an unhandled rejection.
|
|
89
|
+
void this.exited.catch(() => {});
|
|
90
|
+
const stdin = this.child.stdin;
|
|
91
|
+
const stdout = this.child.stdout;
|
|
92
|
+
if (stdin === null || stdout === null) {
|
|
93
|
+
this.child.kill();
|
|
94
|
+
throw new Error("ttsc: resident check host has no stdio pipes");
|
|
95
|
+
}
|
|
96
|
+
this.reader = createInterface({ input: stdout });
|
|
97
|
+
const onLine = (line: string) => this.onLine(line);
|
|
98
|
+
const onReaderClose = () => {
|
|
99
|
+
if (this.failure === undefined) this.fail(this.exitError());
|
|
100
|
+
};
|
|
101
|
+
const onStderr = (chunk: Buffer | string) => {
|
|
102
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
103
|
+
this.stderr = (this.stderr + text).slice(-STDERR_TAIL_LIMIT);
|
|
104
|
+
};
|
|
105
|
+
const onError = (error: Error) => this.fail(error);
|
|
106
|
+
this.reader.on("line", onLine);
|
|
107
|
+
this.reader.on("close", onReaderClose);
|
|
108
|
+
this.child.stderr?.on("data", onStderr);
|
|
109
|
+
this.child.on("error", onError);
|
|
110
|
+
stdin.on("error", onError);
|
|
111
|
+
stdout.on("error", onError);
|
|
112
|
+
this.child.stderr?.on(
|
|
113
|
+
"error",
|
|
114
|
+
ResidentCheckProcessHost.ignoreTransportError,
|
|
115
|
+
);
|
|
116
|
+
this.releaseTransport = () => {
|
|
117
|
+
clearTimeout(this.flushDeadline);
|
|
118
|
+
clearTimeout(this.terminationTimer);
|
|
119
|
+
clearTimeout(this.terminationDeadline);
|
|
120
|
+
this.reader.off("line", onLine);
|
|
121
|
+
this.reader.off("close", onReaderClose);
|
|
122
|
+
this.reader.close();
|
|
123
|
+
if (onChildClose !== undefined) this.child.off("close", onChildClose);
|
|
124
|
+
this.child.off("error", onError);
|
|
125
|
+
this.child.on("error", ResidentCheckProcessHost.ignoreTransportError);
|
|
126
|
+
this.child.stderr?.off("data", onStderr);
|
|
127
|
+
for (const stream of [stdin, stdout, this.child.stderr]) {
|
|
128
|
+
stream?.off("error", onError);
|
|
129
|
+
stream?.off("error", ResidentCheckProcessHost.ignoreTransportError);
|
|
130
|
+
stream?.on("error", ResidentCheckProcessHost.ignoreTransportError);
|
|
131
|
+
stream?.destroy();
|
|
132
|
+
}
|
|
133
|
+
this.child.unref();
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Write one caller-serialized JSON line and wait for its reply.
|
|
139
|
+
*
|
|
140
|
+
* Replies are paired with requests in FIFO order. A request made after the
|
|
141
|
+
* process failed, or whose line cannot be written, rejects with the failure
|
|
142
|
+
* that retired the process, so the caller can fall back to a one-shot check
|
|
143
|
+
* for that cycle.
|
|
144
|
+
*
|
|
145
|
+
* The client serializes before crossing the worker boundary. A closed input
|
|
146
|
+
* retires the host and rejects its other pending calls.
|
|
147
|
+
*
|
|
148
|
+
* @evidence contracts/common.md#principled-implementation The caller supplies one serialized line, and each successfully enqueued call keeps its own FIFO slot until a validated reply or shared retirement settles it.
|
|
149
|
+
* @evidence contracts/common.md#clear-and-simple-design Pre-write validation remains local; write failures use the same settlement and retirement operations as pipe and protocol failure.
|
|
150
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts A closed pipe retires all pending requests rather than treating the host as healthy with one exceptional caller.
|
|
151
|
+
* @evidence contracts/common.md#meaningful-documentation Separate paragraphs document serialized input, reply ordering, rejection scope and closed-input effects, applying the documentation skill.
|
|
152
|
+
* @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.
|
|
153
|
+
* @evidence contracts/performance.md#efficient-algorithms Writing visits serialized input bytes; enqueueing and settlement have amortized constant queue work plus reply parsing. Native check work is delegated rather than bounded by transport cost.
|
|
154
|
+
* @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.
|
|
155
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources The transport retains one resolver pair and its queued line per outstanding call until reply or retirement; live requests wait without an implicit timeout or population limit.
|
|
156
|
+
*/
|
|
157
|
+
public request(line: string): Promise<ResidentCheckResult> {
|
|
158
|
+
if (this.failure !== undefined) return Promise.reject(this.failure);
|
|
159
|
+
const stdin = this.child.stdin;
|
|
160
|
+
if (stdin === null || stdin.destroyed) {
|
|
161
|
+
const error = new Error("ttsc: resident check host stdin is closed");
|
|
162
|
+
this.fail(error);
|
|
163
|
+
return Promise.reject(error);
|
|
164
|
+
}
|
|
165
|
+
return new Promise<ResidentCheckResult>((resolve, reject) => {
|
|
166
|
+
const pending: PendingRequest = { reject, resolve, settled: false };
|
|
167
|
+
this.pending.push(pending);
|
|
168
|
+
try {
|
|
169
|
+
stdin.write(line, (error) => {
|
|
170
|
+
if (error === null || error === undefined || pending.settled) return;
|
|
171
|
+
this.settle(pending, error);
|
|
172
|
+
this.fail(error);
|
|
173
|
+
});
|
|
174
|
+
} catch (error) {
|
|
175
|
+
const reason = asError(error);
|
|
176
|
+
this.settle(pending, reason);
|
|
177
|
+
this.fail(reason);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Retire the sidecar: reject pending requests and close its request stream.
|
|
184
|
+
* Idempotent; a process that already failed is left as is.
|
|
185
|
+
*
|
|
186
|
+
* @evidence contracts/common.md#principled-implementation Setting failure before rejecting the detached FIFO makes disposal terminal; subsequent replies and requests cannot revive the child.
|
|
187
|
+
* @evidence contracts/common.md#clear-and-simple-design Disposal enters the same retirement path as transport failure instead of maintaining a second cleanup sequence.
|
|
188
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Cleanup ends the actual owned streams and attempts child termination; it does not disguise a pending request as a successful result.
|
|
189
|
+
* @evidence contracts/common.md#meaningful-documentation The native comment states terminal rejection and repeated-call behavior, following the documentation skill's ownership guidance.
|
|
190
|
+
* @evidence contracts/portability.md#os-neutral-implementation Owned stream EOF and forced signaling use Node's supported APIs without shell or POSIX process-tree commands.
|
|
191
|
+
* @evidence contracts/performance.md#efficient-algorithms Retirement visits each outstanding request once and releases the queue in linear time rather than repeatedly shifting all remaining entries.
|
|
192
|
+
*
|
|
193
|
+
* @evidenceExclude contracts/performance.md#reuse-equivalent-work Disposal ends the owner and produces no computation that another request may reuse.
|
|
194
|
+
*
|
|
195
|
+
* @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.
|
|
196
|
+
*/
|
|
197
|
+
public dispose(): void {
|
|
198
|
+
this.fail(new Error("ttsc: resident check host disposed"));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* End the request stream and await the owned child's actual close event. A
|
|
203
|
+
* forced, signalled or nonzero exit rejects; repeated calls share
|
|
204
|
+
* retirement.
|
|
205
|
+
*
|
|
206
|
+
* @evidence contracts/common.md#principled-implementation EOF is sent on the owned input and success requires the child's actual close with status zero, never merely a successful kill request.
|
|
207
|
+
* @evidence contracts/common.md#clear-and-simple-design Retirement and join are shared with dispose; this awaitable boundary adds exit validation for callers that must release process ownership.
|
|
208
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Forced and unknown termination remain failures rather than fabricated graceful completion.
|
|
209
|
+
* @evidence contracts/common.md#meaningful-documentation Native prose states the child-close authority, success condition and repeated-call behavior.
|
|
210
|
+
* @evidence contracts/portability.md#os-neutral-implementation Native stdin EOF and child close events work through Node's process API; forced signalling is a failure rather than a portable graceful-stop assumption.
|
|
211
|
+
* @evidence contracts/performance.md#efficient-algorithms Closing performs one retirement traversal and constant-time exit validation; no polling population grows over the lifetime.
|
|
212
|
+
* @evidence contracts/performance.md#reuse-equivalent-work Repeated closes reuse the single actual-exit promise but never reuse an earlier process's exit as proof for another child.
|
|
213
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources Actual close clears termination timers. A failed event-loop-scheduled join deadline destroys/unrefs transport and removes request listeners while retaining terminal failure; unref releases event-loop liveness, not the client object's ChildProcess reference or proof of physical child/descendant termination.
|
|
214
|
+
*/
|
|
215
|
+
public async close(): Promise<void> {
|
|
216
|
+
this.dispose();
|
|
217
|
+
await this.exited;
|
|
218
|
+
// A kill request can race with a natural exit. Actual zero-status close
|
|
219
|
+
// remains graceful even when the request reached an already exiting child.
|
|
220
|
+
if (this.forced && this.child.signalCode === "SIGKILL")
|
|
221
|
+
throw new Error("ttsc: resident check host required forced termination");
|
|
222
|
+
if (this.child.exitCode !== 0 || this.child.signalCode !== null)
|
|
223
|
+
throw this.exitError();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Await actual retirement before a transport-failure fallback starts. A known
|
|
228
|
+
* failed child may be replaced; an unjoined child may not.
|
|
229
|
+
*
|
|
230
|
+
* @evidence contracts/common.md#principled-implementation A replacement can start only after the original child's close event releases its native ownership.
|
|
231
|
+
* @evidence contracts/common.md#clear-and-simple-design The transport owner exposes joining separately from graceful status validation for the supported recovery policy.
|
|
232
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts An elapsed deadline rejects instead of treating a still-live process as retired.
|
|
233
|
+
* @evidence contracts/common.md#meaningful-documentation Native prose distinguishes recovery's known failed exit from shutdown's graceful success requirement.
|
|
234
|
+
* @evidence contracts/portability.md#os-neutral-implementation The actual close event, rather than a signal result or native PID spelling, establishes retirement on each platform.
|
|
235
|
+
* @evidence contracts/performance.md#efficient-algorithms This accessor returns the existing promise without adding a child listener or polling filesystem/process state; subscriber work belongs to the caller.
|
|
236
|
+
* @evidence contracts/performance.md#reuse-equivalent-work Every waiter shares this child's terminal event; a replacement always owns a new promise.
|
|
237
|
+
* @evidence contracts/performance.md#bound-retention-and-release-resources Waiters share actual close or, after retirement begins, its event-loop-scheduled failed join deadline. Waiting alone starts no termination timer; a live unretired child can remain pending without a deadline, and an unjoined owner remains failure.
|
|
238
|
+
*/
|
|
239
|
+
public waitForExit(): Promise<void> {
|
|
240
|
+
return this.exited;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private rejectExit: ((error: Error) => void) | undefined;
|
|
244
|
+
|
|
245
|
+
private onLine(line: string): void {
|
|
246
|
+
if (this.failure !== undefined || line.trim().length === 0) return;
|
|
247
|
+
const pending = this.pending[this.pendingHead];
|
|
248
|
+
if (pending === undefined) {
|
|
249
|
+
this.fail(
|
|
250
|
+
new Error("ttsc: resident check host sent an unsolicited reply"),
|
|
251
|
+
);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const result = parseResidentCheckResult(line);
|
|
255
|
+
if (result === undefined) {
|
|
256
|
+
const error = new Error(
|
|
257
|
+
`ttsc: resident check host sent a malformed reply: ${echoLine(line)}`,
|
|
258
|
+
);
|
|
259
|
+
this.settle(pending, error);
|
|
260
|
+
this.fail(error);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
this.settle(pending, result);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private settle(
|
|
267
|
+
pending: PendingRequest,
|
|
268
|
+
result: Error | ResidentCheckResult,
|
|
269
|
+
): void {
|
|
270
|
+
if (pending.settled) return;
|
|
271
|
+
pending.settled = true;
|
|
272
|
+
if (this.pending[this.pendingHead] === pending) {
|
|
273
|
+
this.pending[this.pendingHead++] = undefined;
|
|
274
|
+
if (this.pendingHead * 2 >= this.pending.length) {
|
|
275
|
+
this.pending.splice(0, this.pendingHead);
|
|
276
|
+
this.pendingHead = 0;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (result instanceof Error) pending.reject(result);
|
|
280
|
+
else pending.resolve(result);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private fail(error: Error): void {
|
|
284
|
+
if (this.failure !== undefined) return;
|
|
285
|
+
this.failure = error;
|
|
286
|
+
const pending = this.pending.splice(0);
|
|
287
|
+
this.pendingHead = 0;
|
|
288
|
+
for (const request of pending) {
|
|
289
|
+
if (request !== undefined) this.settle(request, error);
|
|
290
|
+
}
|
|
291
|
+
// Retirement has already rejected every reply slot. Drain raw output rather
|
|
292
|
+
// than assembling unbounded JSON lines while shutdown timers are active.
|
|
293
|
+
this.reader.close();
|
|
294
|
+
this.child.stdout?.resume();
|
|
295
|
+
this.terminate();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private terminate(): void {
|
|
299
|
+
if (this.closing) return;
|
|
300
|
+
this.closing = true;
|
|
301
|
+
const stdin = this.child.stdin;
|
|
302
|
+
if (stdin !== null && !stdin.destroyed) {
|
|
303
|
+
try {
|
|
304
|
+
// EOF cannot reach the child until the owned writable pipe finishes.
|
|
305
|
+
// A blocked parent event loop must not spend the child's shutdown grace
|
|
306
|
+
// before that delivery can happen.
|
|
307
|
+
stdin.end(() => this.startTerminationGrace());
|
|
308
|
+
// Start delivery's own deadline after the caller gives Node control.
|
|
309
|
+
// This bounds an unread full pipe without charging synchronous caller
|
|
310
|
+
// work against either delivery or the child's later EOF grace.
|
|
311
|
+
queueMicrotask(() => {
|
|
312
|
+
if (this.didClose || this.terminationTimer !== undefined) return;
|
|
313
|
+
this.flushDeadline = setTimeout(
|
|
314
|
+
() => this.forceTerminationAndJoin(),
|
|
315
|
+
TERMINATION_GRACE_MS,
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
} catch {
|
|
320
|
+
stdin.destroy();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
this.startTerminationGrace();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private startTerminationGrace(): void {
|
|
327
|
+
clearTimeout(this.flushDeadline);
|
|
328
|
+
// Keep output readable until actual close: destroying inherited pipes would
|
|
329
|
+
// conceal a descendant that still owns them and falsely authorize reuse.
|
|
330
|
+
if (
|
|
331
|
+
this.didClose ||
|
|
332
|
+
this.terminationTimer !== undefined ||
|
|
333
|
+
this.terminationDeadline !== undefined
|
|
334
|
+
)
|
|
335
|
+
return;
|
|
336
|
+
this.terminationTimer = setTimeout(
|
|
337
|
+
() => this.forceTerminationAndJoin(),
|
|
338
|
+
TERMINATION_GRACE_MS,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private forceTerminationAndJoin(): void {
|
|
343
|
+
if (this.didClose || this.terminationDeadline !== undefined) return;
|
|
344
|
+
if (this.child.exitCode === null && this.child.signalCode === null) {
|
|
345
|
+
this.forced = true;
|
|
346
|
+
try {
|
|
347
|
+
this.child.kill("SIGKILL");
|
|
348
|
+
} catch {
|
|
349
|
+
// The deadline still requires actual close, even if kill fails.
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
this.terminationDeadline = setTimeout(() => {
|
|
353
|
+
this.rejectExit?.(
|
|
354
|
+
new Error("ttsc: resident check host did not close after termination"),
|
|
355
|
+
);
|
|
356
|
+
// Reject first: destroying inherited pipes can cause Node's close event,
|
|
357
|
+
// but releasing our handles cannot prove a graceful physical join.
|
|
358
|
+
this.releaseTransport?.();
|
|
359
|
+
this.releaseTransport = undefined;
|
|
360
|
+
}, TERMINATION_GRACE_MS);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private static ignoreTransportError(): void {}
|
|
364
|
+
|
|
365
|
+
private exitError(): Error {
|
|
366
|
+
const detail = this.stderr.trim();
|
|
367
|
+
if (detail.length !== 0) return new Error(detail);
|
|
368
|
+
const signal =
|
|
369
|
+
this.child.signalCode === null ? "" : `, signal ${this.child.signalCode}`;
|
|
370
|
+
return new Error(
|
|
371
|
+
`ttsc: resident check host exited (code ${
|
|
372
|
+
this.child.exitCode ?? "null"
|
|
373
|
+
}${signal})`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const STDERR_TAIL_LIMIT = 64 * 1024;
|
|
379
|
+
|
|
380
|
+
const REPLY_ECHO_LIMIT = 200;
|
|
381
|
+
|
|
382
|
+
const TERMINATION_GRACE_MS = 1_000;
|
|
383
|
+
|
|
384
|
+
type PendingRequest = {
|
|
385
|
+
reject(reason: Error): void;
|
|
386
|
+
resolve(result: ResidentCheckResult): void;
|
|
387
|
+
settled: boolean;
|
|
388
|
+
};
|
|
389
|
+
function parseResidentCheckResult(
|
|
390
|
+
line: string,
|
|
391
|
+
): ResidentCheckResult | undefined {
|
|
392
|
+
let value: unknown;
|
|
393
|
+
try {
|
|
394
|
+
value = JSON.parse(line);
|
|
395
|
+
} catch {
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
if (
|
|
399
|
+
!isRecord(value) ||
|
|
400
|
+
typeof value.status !== "number" ||
|
|
401
|
+
!Number.isInteger(value.status)
|
|
402
|
+
) {
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
if (typeof value.stdout !== "string" || typeof value.stderr !== "string") {
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
const telemetry = value.telemetry;
|
|
409
|
+
if (
|
|
410
|
+
!isRecord(telemetry) ||
|
|
411
|
+
typeof telemetry.pid !== "number" ||
|
|
412
|
+
!Number.isSafeInteger(telemetry.pid) ||
|
|
413
|
+
typeof telemetry.programLoads !== "number" ||
|
|
414
|
+
!Number.isSafeInteger(telemetry.programLoads) ||
|
|
415
|
+
typeof telemetry.programUpdates !== "number" ||
|
|
416
|
+
!Number.isSafeInteger(telemetry.programUpdates) ||
|
|
417
|
+
typeof telemetry.reused !== "boolean"
|
|
418
|
+
) {
|
|
419
|
+
return undefined;
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
diagnostics: [],
|
|
423
|
+
status: value.status,
|
|
424
|
+
stderr: value.stderr,
|
|
425
|
+
stdout: value.stdout,
|
|
426
|
+
telemetry: {
|
|
427
|
+
pid: telemetry.pid,
|
|
428
|
+
programLoads: telemetry.programLoads,
|
|
429
|
+
programUpdates: telemetry.programUpdates,
|
|
430
|
+
reused: telemetry.reused,
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
436
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function echoLine(line: string): string {
|
|
440
|
+
const trimmed = line.trim();
|
|
441
|
+
return JSON.stringify(
|
|
442
|
+
trimmed.length <= REPLY_ECHO_LIMIT
|
|
443
|
+
? trimmed
|
|
444
|
+
: `${trimmed.slice(0, REPLY_ECHO_LIMIT)}…`,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function asError(error: unknown): Error {
|
|
449
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Every retirement observation stays on this loop. Moving only timers to a
|
|
453
|
+
// worker would still let a parent-side verdict overtake pending exit and pipe
|
|
454
|
+
// events, and treating worker exit as child close would lose descendant safety.
|
|
455
|
+
if (parentPort !== null) {
|
|
456
|
+
const port = parentPort;
|
|
457
|
+
const host = new ResidentCheckProcessHost(
|
|
458
|
+
workerData as ResidentCheckProcessOptions,
|
|
459
|
+
);
|
|
460
|
+
port.on("message", (command: ResidentCheckProcessProtocol.Command) => {
|
|
461
|
+
if (command.kind === "retire") host.dispose();
|
|
462
|
+
else
|
|
463
|
+
void host.request(command.line).then(
|
|
464
|
+
(result) =>
|
|
465
|
+
port.postMessage({
|
|
466
|
+
kind: "result",
|
|
467
|
+
id: command.id,
|
|
468
|
+
result,
|
|
469
|
+
} satisfies ResidentCheckProcessProtocol.Reply),
|
|
470
|
+
(error: Error) =>
|
|
471
|
+
port.postMessage({
|
|
472
|
+
kind: "error",
|
|
473
|
+
id: command.id,
|
|
474
|
+
error,
|
|
475
|
+
} satisfies ResidentCheckProcessProtocol.Reply),
|
|
476
|
+
);
|
|
477
|
+
});
|
|
478
|
+
port.once("close", () => host.dispose());
|
|
479
|
+
void host.waitForExit().then(
|
|
480
|
+
async () => {
|
|
481
|
+
let closeError: Error | undefined;
|
|
482
|
+
try {
|
|
483
|
+
await host.close();
|
|
484
|
+
} catch (error) {
|
|
485
|
+
closeError = asError(error);
|
|
486
|
+
}
|
|
487
|
+
port.postMessage({
|
|
488
|
+
kind: "terminal",
|
|
489
|
+
closeError,
|
|
490
|
+
} satisfies ResidentCheckProcessProtocol.Reply);
|
|
491
|
+
port.close();
|
|
492
|
+
},
|
|
493
|
+
(joinError: Error) => {
|
|
494
|
+
port.postMessage({
|
|
495
|
+
kind: "terminal",
|
|
496
|
+
joinError,
|
|
497
|
+
closeError: joinError,
|
|
498
|
+
} satisfies ResidentCheckProcessProtocol.Reply);
|
|
499
|
+
port.close();
|
|
500
|
+
},
|
|
501
|
+
);
|
|
502
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ResidentCheckResult } from "./ResidentCheckResult";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Messages between one resident client and its isolated process owner. Request
|
|
5
|
+
* identifiers pair concurrent callers; only the owner's terminal receipt
|
|
6
|
+
* establishes child-close authority. Errors use structured cloning.
|
|
7
|
+
*
|
|
8
|
+
* @evidence contracts/common.md#principled-implementation Discriminated messages separate request results from the actual-close verdict and strict shutdown status.
|
|
9
|
+
* @evidence contracts/common.md#clear-and-simple-design One private protocol connects the client and its one process owner without exposing worker transport to consumers.
|
|
10
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Worker departure is absent from the successful join receipt; it cannot substitute for child close.
|
|
11
|
+
* @evidence contracts/common.md#meaningful-documentation Native prose explains correlation, ownership and error transport.
|
|
12
|
+
* @evidenceExclude contracts/portability.md#os-neutral-implementation These data types define no native boundary; the client and host own worker and child creation.
|
|
13
|
+
* @evidenceExclude contracts/performance.md#efficient-algorithms The namespace describes data rather than choosing an execution algorithm.
|
|
14
|
+
* @evidenceExclude contracts/performance.md#reuse-equivalent-work Messages describe independent operations without caching results.
|
|
15
|
+
* @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Types retain no resources; the client and host own queued requests and worker lifetime.
|
|
16
|
+
*/
|
|
17
|
+
export namespace ResidentCheckProcessProtocol {
|
|
18
|
+
/**
|
|
19
|
+
* Parent commands preserve request order before the terminal retire command.
|
|
20
|
+
* Request lines were serialized by the caller before admission.
|
|
21
|
+
*
|
|
22
|
+
* @evidence contracts/common.md#principled-implementation A discriminant separates serialized request admission from terminal retirement; numeric identifiers correlate independent callers.
|
|
23
|
+
* @evidence contracts/common.md#clear-and-simple-design Two command variants express the only supported parent operations without duplicating native shutdown state.
|
|
24
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts Commands carry actual caller data and retirement intent rather than fabricated native results.
|
|
25
|
+
* @evidence contracts/common.md#meaningful-documentation Native prose explains ordering and serialized-line admission.
|
|
26
|
+
* @evidenceExclude contracts/portability.md#os-neutral-implementation The data declaration performs no filesystem, path or process operation.
|
|
27
|
+
* @evidenceExclude contracts/performance.md#efficient-algorithms A command type selects no algorithm.
|
|
28
|
+
* @evidenceExclude contracts/performance.md#reuse-equivalent-work A command type performs no reusable work.
|
|
29
|
+
* @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The declaration owns no live queue, worker or native handle.
|
|
30
|
+
*/
|
|
31
|
+
export type Command =
|
|
32
|
+
| { kind: "request"; id: number; line: string }
|
|
33
|
+
| { kind: "retire" };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Owner replies separate request outcomes from native lifetime authority.
|
|
37
|
+
* Absent joinError certifies actual child close; closeError independently
|
|
38
|
+
* preserves strict shutdown failure for a joined child.
|
|
39
|
+
*
|
|
40
|
+
* @evidence contracts/common.md#principled-implementation Terminal joining and strict status are independent outcomes, allowing replacement of a joined failed host while refusing unknown ownership.
|
|
41
|
+
* @evidence contracts/common.md#clear-and-simple-design Request results use the request identifier, and one terminal receipt carries the host's two lifetime outcomes.
|
|
42
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The successful receipt requires actual child close; worker disappearance is not represented as success.
|
|
43
|
+
* @evidence contracts/common.md#meaningful-documentation Native prose explains absent join failure and independent strict status.
|
|
44
|
+
* @evidenceExclude contracts/portability.md#os-neutral-implementation The data declaration performs no filesystem, path or process operation.
|
|
45
|
+
* @evidenceExclude contracts/performance.md#efficient-algorithms A reply type selects no algorithm.
|
|
46
|
+
* @evidenceExclude contracts/performance.md#reuse-equivalent-work A reply type performs no reusable work.
|
|
47
|
+
* @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The declaration owns no live reply queue, worker or native handle.
|
|
48
|
+
*/
|
|
49
|
+
export type Reply =
|
|
50
|
+
| { kind: "result"; id: number; result: ResidentCheckResult }
|
|
51
|
+
| { kind: "error"; id: number; error: Error }
|
|
52
|
+
| { kind: "terminal"; joinError?: Error; closeError?: Error };
|
|
53
|
+
}
|