@zq-silk/yui 0.5.3 → 0.6.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.
Files changed (157) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/agentConfigurationPicker.js +1 -1
  4. package/dist/cli/commandCatalog.js +251 -13
  5. package/dist/cli/updateOrchestrator.js +8 -0
  6. package/dist/cli/updatePorts.js +76 -22
  7. package/dist/cli.js +264 -20
  8. package/dist/commands/configCommands.js +83 -9
  9. package/dist/commands/controllerCommands.js +103 -0
  10. package/dist/commands/deliveryGuardPreflight.js +35 -0
  11. package/dist/commands/durableJobCommands.js +231 -0
  12. package/dist/commands/executionAuditCommands.js +193 -0
  13. package/dist/commands/grantCommands.js +374 -0
  14. package/dist/commands/projectCommands.js +119 -81
  15. package/dist/commands/releaseCommands.js +444 -0
  16. package/dist/commands/resourcesCommands.js +274 -0
  17. package/dist/commands/sessionCommands.js +104 -0
  18. package/dist/commands/taskActor.js +117 -0
  19. package/dist/commands/taskChangeSetCommands.js +60 -0
  20. package/dist/commands/taskCommands.js +610 -201
  21. package/dist/commands/taskCompletionGate.js +78 -1
  22. package/dist/commands/taskContextCommand.js +24 -6
  23. package/dist/commands/taskInputCommands.js +1 -1
  24. package/dist/commands/taskIntegrationCommands.js +136 -33
  25. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  26. package/dist/commands/taskNextActionCommand.js +85 -0
  27. package/dist/commands/taskOverlapCommands.js +120 -0
  28. package/dist/commands/taskOverviewCommand.js +36 -8
  29. package/dist/commands/telemetryCommands.js +330 -0
  30. package/dist/commands/workflowCommands.js +415 -0
  31. package/dist/config/yuiConfig.js +60 -0
  32. package/dist/controller/clientRuntime.js +42 -1
  33. package/dist/controller/controller.js +413 -61
  34. package/dist/controller/controllerMain.js +25 -2
  35. package/dist/controller/domainIdentity.js +16 -8
  36. package/dist/controller/ephemeralResourceReaper.js +2 -1
  37. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  38. package/dist/controller/handoverCandidate.js +168 -0
  39. package/dist/controller/jobClient.js +102 -0
  40. package/dist/controller/jobControl.js +613 -0
  41. package/dist/controller/jobSupervisor.js +498 -0
  42. package/dist/controller/providerHookRunFence.js +34 -5
  43. package/dist/controller/resourceCleanupLinux.js +18 -9
  44. package/dist/controller/resourceInventoryLinux.js +90 -39
  45. package/dist/controller/resourceInventoryRpc.js +85 -0
  46. package/dist/controller/resourceInventoryWorker.js +50 -0
  47. package/dist/controller/runtime.js +238 -22
  48. package/dist/controller/runtimeEventInbox.js +234 -57
  49. package/dist/controller/runtimeEventProcessor.js +549 -42
  50. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  51. package/dist/core/boundedRpc.js +475 -0
  52. package/dist/core/controllerServer.js +416 -27
  53. package/dist/core/controllerTelemetry.js +167 -0
  54. package/dist/doctor/doctor.js +113 -16
  55. package/dist/domain/validation.js +9 -0
  56. package/dist/execution/executionGroup.js +40 -3
  57. package/dist/executor/agentExecutor.js +6 -3
  58. package/dist/executor/effectiveLaunch.js +52 -0
  59. package/dist/executor/executorRegistry.js +50 -0
  60. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  61. package/dist/grant/capabilityGrant.js +282 -0
  62. package/dist/integration/changeSet.js +16 -3
  63. package/dist/integration/changeSetManifest.js +46 -0
  64. package/dist/integration/gitIntegrationService.js +528 -147
  65. package/dist/integration/integrationAttempt.js +54 -5
  66. package/dist/integration/integrationQueueEntry.js +221 -0
  67. package/dist/integration/integrationQueueService.js +955 -0
  68. package/dist/integration/manifestTags.js +99 -0
  69. package/dist/integration/overlapDiagnostics.js +211 -0
  70. package/dist/job/durableJob.js +449 -0
  71. package/dist/job/jobRunner.js +350 -0
  72. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  73. package/dist/lifecycle/providerErrorClass.js +126 -0
  74. package/dist/message/message.js +16 -3
  75. package/dist/observability/executionAudit.js +545 -0
  76. package/dist/observability/faultClassification.js +160 -0
  77. package/dist/observability/runtimeIdentity.js +367 -0
  78. package/dist/release/fakeReleasePorts.js +55 -0
  79. package/dist/release/releaseHandover.js +475 -0
  80. package/dist/release/releaseIdempotencyStore.js +165 -0
  81. package/dist/release/releaseWorkflow.js +459 -0
  82. package/dist/release/releaseWorkflowEngine.js +688 -0
  83. package/dist/release/releaseWorkflowPorts.js +1720 -0
  84. package/dist/release/runtimeRelease.js +495 -0
  85. package/dist/release/workflowFileLock.js +218 -0
  86. package/dist/repository/gitWorkspace.js +177 -1
  87. package/dist/repository/projectMaintenanceLock.js +315 -0
  88. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  89. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  90. package/dist/resources/autoResourceGc.js +116 -0
  91. package/dist/resources/liveReferences.js +574 -0
  92. package/dist/resources/resourceDiscovery.js +477 -0
  93. package/dist/resources/resourceGc.js +645 -0
  94. package/dist/resources/resourceRegistrar.js +256 -0
  95. package/dist/resources/resourceRegistry.js +150 -0
  96. package/dist/resources/resourceRegistryStore.js +41 -0
  97. package/dist/resources/resourceTypes.js +42 -0
  98. package/dist/resources/sqliteResourceRegistry.js +111 -0
  99. package/dist/review/reviewConfig.js +10 -0
  100. package/dist/review/reviewFinding.js +240 -0
  101. package/dist/review/reviewFindingLedger.js +545 -0
  102. package/dist/review/reviewOutcomeClassifier.js +61 -0
  103. package/dist/review/reviewRound.js +56 -4
  104. package/dist/run/agentRun.js +80 -4
  105. package/dist/run/providerRetry.js +84 -0
  106. package/dist/run/providerRetryConfig.js +63 -0
  107. package/dist/run/yieldReceipt.js +65 -0
  108. package/dist/runtime/exactControlPlane.js +79 -2
  109. package/dist/runtime/index.js +4 -0
  110. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  111. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  112. package/dist/runtime/sessionReconciliation.js +93 -0
  113. package/dist/runtime/sessionTerminationGuard.js +211 -0
  114. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  115. package/dist/runtime/tmuxAdapters.js +34 -1
  116. package/dist/scheduler/actionability.js +155 -0
  117. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  118. package/dist/scheduler/activeTaskProgress.js +60 -0
  119. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  120. package/dist/scheduler/roleRunStall.js +135 -29
  121. package/dist/scheduler/taskExecutionProjection.js +11 -0
  122. package/dist/setup/setupCommand.js +27 -4
  123. package/dist/storage/compatibleTaskStore.js +112 -5
  124. package/dist/storage/migration/productionRegistry.js +769 -1
  125. package/dist/storage/persistenceWorker.js +194 -0
  126. package/dist/storage/sqliteSchema.js +705 -0
  127. package/dist/storage/sqliteStore.js +1695 -0
  128. package/dist/storage/storageVersions.js +9 -2
  129. package/dist/storage/storeRpc.js +298 -0
  130. package/dist/storage/taskStore.js +982 -21
  131. package/dist/storage/upgrade/homeClassification.js +157 -12
  132. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  133. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  134. package/dist/storage/upgrade/recordVersions.js +10 -1
  135. package/dist/storage/upgrade/sqliteMigrationTarget.js +351 -0
  136. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  137. package/dist/storage/upgrade/sqliteStateMigration.js +713 -0
  138. package/dist/storage/upgrade/upgradeOrchestrator.js +510 -18
  139. package/dist/task/deliveryGuard.js +226 -0
  140. package/dist/task/nextAction.js +343 -0
  141. package/dist/task/repairWave.js +137 -0
  142. package/dist/task/taskRecordReference.js +6 -1
  143. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  144. package/dist/telemetry/telemetryCompaction.js +251 -0
  145. package/dist/telemetry/telemetryConfig.js +64 -0
  146. package/dist/telemetry/telemetryRouter.js +32 -0
  147. package/dist/telemetry/telemetryStore.js +19 -0
  148. package/dist/telemetry/telemetryWiring.js +33 -0
  149. package/dist/tmux/tmuxManager.js +20 -1
  150. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  151. package/dist/verification/gateArtifact.js +216 -0
  152. package/dist/verification/gateArtifactStore.js +87 -0
  153. package/dist/verification/verificationGateService.js +414 -0
  154. package/dist/verification/verificationPlan.js +308 -0
  155. package/dist/workspace/gitChangeSetCapture.js +12 -2
  156. package/dist/workspace/workItemChangeSetManager.js +60 -3
  157. package/package.json +2 -1
@@ -0,0 +1,475 @@
1
+ /**
2
+ * Generic bounded RPC seam for Worker Threads (task-21, §3.1).
3
+ *
4
+ * The main thread talks to a worker over a `MessageChannel` port. This module
5
+ * provides the shared primitives used by every worker RPC in the control plane
6
+ * (the persistence worker, `storage/storeRpc`, and the resource inventory
7
+ * worker, `controller/resourceInventoryRpc`):
8
+ *
9
+ * - {@link BoundedSlotPool} ..... an async semaphore with a bounded waiter
10
+ * queue: bounds in-flight requests (default 64) and queue depth, applies
11
+ * backpressure (callers await; the socket keeps draining), so the main
12
+ * event loop is never blocked.
13
+ * - {@link BoundedRpcClient} .... the main-thread client: spawns the worker,
14
+ * completes the `ready` handshake, bounds in-flight requests, tracks
15
+ * pending requests, honours `AbortSignal` cancellation (posts a cancel
16
+ * notice, rejects promptly), and restarts the worker + replays
17
+ * unacknowledged requests on crash (the §3.1 fault boundary).
18
+ * - {@link runRpcWorker} ......... the worker-side host: owns the port
19
+ * handshake, dispatches requests (optionally through a FIFO queue),
20
+ * observes cancels, and serializes errors back across the thread boundary.
21
+ *
22
+ * Each worker supplies a small protocol adapter ({@link BoundedRpcProtocol} /
23
+ * {@link RpcWorkerHost}) describing its own request/response dialect; the
24
+ * backpressure, cancellation, and fault-boundary logic lives here once.
25
+ */
26
+ import { parentPort } from "node:worker_threads";
27
+ import { Worker, MessageChannel } from "node:worker_threads";
28
+ export function serializeError(error) {
29
+ if (error instanceof Error) {
30
+ return {
31
+ name: error.name,
32
+ message: error.message,
33
+ ...(error.stack === undefined ? {} : { stack: error.stack }),
34
+ ...("code" in error && typeof error.code === "string"
35
+ ? { code: error.code }
36
+ : {})
37
+ };
38
+ }
39
+ return { name: "Error", message: String(error) };
40
+ }
41
+ /**
42
+ * Default error deserializer. Worker RPCs with domain-specific error classes
43
+ * override this in their protocol's `settle` (the persistence client maps
44
+ * `Storage*Error` names back to their classes).
45
+ */
46
+ export function deserializeError(serialized) {
47
+ const error = new Error(serialized.message);
48
+ error.name = serialized.name;
49
+ return error;
50
+ }
51
+ // -- Bounded slot pool (backpressure) ----------------------------------------
52
+ /**
53
+ * An async semaphore with a bounded waiter queue (§3.1). `acquire` waits when
54
+ * all permits are in flight; when the waiter queue is full, callers wait on the
55
+ * backpressure condition instead. Callers always await (they already return
56
+ * promises), so the socket keeps accepting and draining — the main event loop
57
+ * is never blocked.
58
+ */
59
+ export class BoundedSlotPool {
60
+ #maxInFlight;
61
+ #permits;
62
+ #maxQueue;
63
+ #waiters = [];
64
+ #backpressure = [];
65
+ constructor(maxInFlight, maxQueue) {
66
+ if (!Number.isSafeInteger(maxInFlight) || maxInFlight < 1) {
67
+ throw new Error(`maxInFlight must be a positive integer: ${maxInFlight}`);
68
+ }
69
+ if (!Number.isSafeInteger(maxQueue) || maxQueue < 0) {
70
+ throw new Error(`maxQueue must be a non-negative integer: ${maxQueue}`);
71
+ }
72
+ this.#maxInFlight = maxInFlight;
73
+ this.#permits = maxInFlight;
74
+ this.#maxQueue = maxQueue;
75
+ }
76
+ async acquire() {
77
+ // Backpressure: the waiter queue is full. Wait for it to drain before
78
+ // queueing (the socket keeps draining; callers await without blocking).
79
+ while (this.#waiters.length >= this.#maxQueue) {
80
+ await new Promise((resolve) => this.#backpressure.push(resolve));
81
+ }
82
+ if (this.#permits > 0) {
83
+ this.#permits -= 1;
84
+ return;
85
+ }
86
+ await new Promise((resolve) => this.#waiters.push(resolve));
87
+ // A released permit was handed directly to this waiter.
88
+ }
89
+ release() {
90
+ const waiter = this.#waiters.shift();
91
+ if (waiter !== undefined) {
92
+ waiter();
93
+ return;
94
+ }
95
+ this.#permits += 1;
96
+ const drained = this.#backpressure.shift();
97
+ if (drained !== null && drained !== undefined)
98
+ drained();
99
+ }
100
+ /** Current queue depth (waiters), for tests/metrics. */
101
+ get queueDepth() {
102
+ return this.#waiters.length;
103
+ }
104
+ /** Currently in-flight permits, for tests/metrics. */
105
+ get inFlight() {
106
+ return this.#maxInFlight - this.#permits;
107
+ }
108
+ }
109
+ // -- Request ids --------------------------------------------------------------
110
+ let requestCounter = 0;
111
+ export function nextRequestId() {
112
+ requestCounter += 1;
113
+ return `rpc-${process.pid}-${Date.now().toString(36)}-${requestCounter.toString(36)}`;
114
+ }
115
+ /**
116
+ * The main-thread client for a worker RPC. Spawns the worker, completes the
117
+ * ready handshake, bounds in-flight requests with a {@link BoundedSlotPool},
118
+ * tracks pending requests, honours `AbortSignal` cancellation, and restarts
119
+ * the worker + replays unacknowledged requests on crash (§3.1 fault boundary).
120
+ */
121
+ export class BoundedRpcClient {
122
+ #protocol;
123
+ #workerScript;
124
+ #restartBackoffMs;
125
+ #slots;
126
+ #pending = new Map();
127
+ #worker;
128
+ #port;
129
+ #ready;
130
+ #readyResolve;
131
+ #readyReject;
132
+ #readyFired = false;
133
+ #closed = false;
134
+ #restarting = false;
135
+ #generation = 0;
136
+ constructor(protocol, options) {
137
+ this.#protocol = protocol;
138
+ this.#workerScript = options.workerScript;
139
+ this.#restartBackoffMs = options.restartBackoffMs ?? 10;
140
+ this.#slots = new BoundedSlotPool(options.maxInFlight ?? 64, options.maxQueue ?? 256);
141
+ this.#ready = this.#newReadyPromise();
142
+ this.#spawnWorker();
143
+ }
144
+ #newReadyPromise() {
145
+ this.#readyFired = false;
146
+ return new Promise((resolve, reject) => {
147
+ this.#readyResolve = resolve;
148
+ this.#readyReject = reject;
149
+ });
150
+ }
151
+ #workerUrl() {
152
+ return this.#workerScript instanceof URL
153
+ ? this.#workerScript
154
+ : new URL(this.#workerScript);
155
+ }
156
+ #spawnWorker() {
157
+ const generation = this.#generation;
158
+ const worker = new Worker(this.#workerUrl());
159
+ const channel = new MessageChannel();
160
+ worker.postMessage({ port: channel.port2 }, [channel.port2]);
161
+ const port = channel.port1;
162
+ this.#worker = worker;
163
+ this.#port = port;
164
+ port.on("message", (response) => {
165
+ if (this.#protocol.isReady(response)) {
166
+ if (!this.#readyFired) {
167
+ this.#readyFired = true;
168
+ this.#readyResolve?.();
169
+ }
170
+ return;
171
+ }
172
+ this.#handleResponse(response);
173
+ });
174
+ worker.on("error", (error) => {
175
+ // A worker-level error (e.g. uncaught exception). Before ready, fail the
176
+ // ready handshake; after ready, the exit handler owns restart.
177
+ if (!this.#readyFired) {
178
+ this.#readyFired = true;
179
+ this.#readyReject?.(error);
180
+ }
181
+ });
182
+ worker.on("exit", (code) => {
183
+ if (this.#closed || code === 0)
184
+ return;
185
+ if (generation !== this.#generation)
186
+ return; // stale worker
187
+ void this.#restart();
188
+ });
189
+ // Send init once the port is connected.
190
+ port.postMessage(this.#protocol.initRequest());
191
+ }
192
+ #handleResponse(response) {
193
+ const requestId = this.#protocol.responseRequestId(response);
194
+ const pending = this.#pending.get(requestId);
195
+ if (pending === undefined)
196
+ return; // stale/unknown (e.g. aborted)
197
+ this.#pending.delete(requestId);
198
+ if (!pending.slotReleased) {
199
+ pending.slotReleased = true;
200
+ this.#slots.release();
201
+ }
202
+ this.#protocol.settle(response, {
203
+ resolve: pending.resolve,
204
+ reject: pending.reject
205
+ });
206
+ }
207
+ async #restart() {
208
+ if (this.#restarting || this.#closed)
209
+ return;
210
+ this.#restarting = true;
211
+ try {
212
+ // If the worker died before becoming ready, fail the old ready handshake
213
+ // so awaiting send() calls reject instead of hanging on an orphaned promise.
214
+ if (!this.#readyFired) {
215
+ this.#readyFired = true;
216
+ this.#readyReject?.(new Error("Worker exited before becoming ready."));
217
+ }
218
+ // Brief backoff to avoid a hot crash loop.
219
+ await new Promise((resolve) => setTimeout(resolve, this.#restartBackoffMs));
220
+ this.#generation += 1;
221
+ this.#port?.close();
222
+ this.#ready = this.#newReadyPromise();
223
+ this.#spawnWorker();
224
+ await this.#ready;
225
+ // Replay unacknowledged requests (§3.1 fault boundary). Idempotent
226
+ // effects are deduped by the worker; read-only effects re-execute. The
227
+ // original promises are still pending and resolve when the new responses
228
+ // arrive.
229
+ for (const pending of this.#pending.values()) {
230
+ this.#port?.postMessage(pending.request);
231
+ }
232
+ }
233
+ catch (error) {
234
+ // Give up: fail all pending requests and release their slots.
235
+ const failure = error instanceof Error ? error : new Error(String(error));
236
+ for (const [id, pending] of this.#pending) {
237
+ this.#pending.delete(id);
238
+ if (!pending.slotReleased) {
239
+ pending.slotReleased = true;
240
+ this.#slots.release();
241
+ }
242
+ pending.reject(failure);
243
+ }
244
+ }
245
+ finally {
246
+ this.#restarting = false;
247
+ }
248
+ }
249
+ /**
250
+ * Send one request and await its response. The request must carry
251
+ * `requestId` (used for the pending map, cancel notices, and restart replay).
252
+ */
253
+ async send(requestId, request, options = {}) {
254
+ if (this.#closed)
255
+ return Promise.reject(new Error("BoundedRpcClient is closed."));
256
+ await this.#ready;
257
+ await this.#slots.acquire();
258
+ if (this.#closed) {
259
+ this.#slots.release();
260
+ return Promise.reject(new Error("BoundedRpcClient is closed."));
261
+ }
262
+ const abortError = (beforeSend) => this.#protocol.abortError?.(beforeSend)
263
+ ?? new Error(beforeSend ? "Request aborted before it was sent." : "Request aborted.");
264
+ return new Promise((resolve, reject) => {
265
+ let abortListener;
266
+ const signal = options.signal;
267
+ if (signal !== undefined) {
268
+ if (signal.aborted) {
269
+ this.#slots.release();
270
+ reject(abortError(true));
271
+ return;
272
+ }
273
+ abortListener = () => {
274
+ // Best-effort cancel; the worker observes the notice and suppresses
275
+ // a late result. Already-completed effects are not undone.
276
+ try {
277
+ this.#port?.postMessage(this.#protocol.cancelRequest(requestId));
278
+ }
279
+ catch {
280
+ // Port may be gone.
281
+ }
282
+ const pending = this.#pending.get(requestId);
283
+ if (pending !== undefined && !pending.slotReleased) {
284
+ pending.slotReleased = true;
285
+ this.#slots.release();
286
+ }
287
+ this.#pending.delete(requestId);
288
+ reject(abortError(false));
289
+ };
290
+ signal.addEventListener("abort", abortListener, { once: true });
291
+ }
292
+ this.#pending.set(requestId, {
293
+ resolve: (value) => {
294
+ if (abortListener !== undefined && signal !== undefined) {
295
+ signal.removeEventListener("abort", abortListener);
296
+ }
297
+ resolve(value);
298
+ },
299
+ reject: (error) => {
300
+ if (abortListener !== undefined && signal !== undefined) {
301
+ signal.removeEventListener("abort", abortListener);
302
+ }
303
+ reject(error);
304
+ },
305
+ request,
306
+ slotReleased: false
307
+ });
308
+ this.#port?.postMessage(request);
309
+ });
310
+ }
311
+ /** Close the worker and release its resources. */
312
+ async close() {
313
+ if (this.#closed)
314
+ return;
315
+ this.#closed = true;
316
+ // Fail any requests still waiting on the ready handshake.
317
+ if (!this.#readyFired) {
318
+ this.#readyFired = true;
319
+ this.#readyReject?.(new Error("BoundedRpcClient closed before ready."));
320
+ }
321
+ // Reject all in-flight requests; their slots are released.
322
+ for (const [id, pending] of this.#pending) {
323
+ this.#pending.delete(id);
324
+ if (!pending.slotReleased) {
325
+ pending.slotReleased = true;
326
+ this.#slots.release();
327
+ }
328
+ pending.reject(new Error("BoundedRpcClient closed."));
329
+ }
330
+ try {
331
+ this.#port?.postMessage(this.#protocol.shutdownRequest());
332
+ }
333
+ catch {
334
+ // Worker may already be gone.
335
+ }
336
+ // Give the worker a moment to exit cleanly.
337
+ await new Promise((resolve) => setTimeout(resolve, 20));
338
+ try {
339
+ await this.#worker?.terminate();
340
+ }
341
+ catch {
342
+ // Already terminated.
343
+ }
344
+ this.#port?.close();
345
+ }
346
+ /** Currently in-flight requests (metrics/tests). */
347
+ get inFlight() {
348
+ return this.#slots.inFlight;
349
+ }
350
+ /** Currently queued requests waiting for a slot (metrics/tests). */
351
+ get queueDepth() {
352
+ return this.#slots.queueDepth;
353
+ }
354
+ /**
355
+ * Test-only fault injection: abruptly terminate the worker (simulating a
356
+ * crash) so the exit handler restarts it and replays unacknowledged requests
357
+ * (§3.1 fault boundary). The pending requests stay pending; they resolve
358
+ * after the restart + replay.
359
+ */
360
+ async crashForTest() {
361
+ await this.#worker?.terminate();
362
+ }
363
+ }
364
+ /**
365
+ * Run a worker RPC host. Called once at the top level of a worker script.
366
+ * Completes the port handshake, then dispatches requests per the host's
367
+ * {@link RpcWorkerHost.kindOf}. `cancel` is processed immediately (so it can
368
+ * interrupt queued work); `shutdown` cleans up and exits; `init` and `request`
369
+ * are queued (FIFO) when `serial` is set, otherwise run immediately.
370
+ *
371
+ * A request whose cancel notice arrived before its result is posted is
372
+ * suppressed (the main thread already rejected the call).
373
+ */
374
+ export function runRpcWorker(host) {
375
+ const port = parentPort;
376
+ if (port === null) {
377
+ throw new Error("RPC worker must be run as a worker thread.");
378
+ }
379
+ const cancelled = new Set();
380
+ let queue = Promise.resolve();
381
+ // Responses go back on the handshake MessageChannel port, not parentPort:
382
+ // the main thread listens on the channel, not on the worker's parent port.
383
+ let messagePort;
384
+ const post = (message) => {
385
+ messagePort?.postMessage(message);
386
+ };
387
+ const runRequest = async (request) => {
388
+ const requestId = host.requestIdOf(request);
389
+ try {
390
+ const value = await host.handle(request);
391
+ if (requestId !== undefined && cancelled.has(requestId)) {
392
+ cancelled.delete(requestId);
393
+ return; // the main thread already gave up; don't post a late result
394
+ }
395
+ if (requestId === undefined) {
396
+ throw new Error("RPC worker request has no requestId.");
397
+ }
398
+ post(host.result(requestId, value));
399
+ }
400
+ catch (error) {
401
+ if (requestId !== undefined)
402
+ cancelled.delete(requestId);
403
+ if (requestId === undefined) {
404
+ console.error("[rpcWorker] request failed without a requestId:", error);
405
+ return;
406
+ }
407
+ post(host.error(requestId, serializeError(error)));
408
+ }
409
+ };
410
+ const enqueue = (task) => {
411
+ queue = queue.then(task).catch((error) => {
412
+ // Backstop: a handler threw without posting a response. Fail the worker
413
+ // rather than hang the caller.
414
+ console.error("[rpcWorker] unhandled handler error:", error);
415
+ });
416
+ };
417
+ port.once("message", (value) => {
418
+ messagePort = value.port;
419
+ messagePort.on("message", (message) => {
420
+ try {
421
+ const kind = host.kindOf(message);
422
+ if (kind === "cancel") {
423
+ // Process immediately so it can interrupt queued work.
424
+ const requestId = host.requestIdOf(message);
425
+ if (requestId !== undefined) {
426
+ cancelled.add(requestId);
427
+ host.cancel(requestId);
428
+ }
429
+ return;
430
+ }
431
+ if (kind === "shutdown") {
432
+ void host.shutdown()
433
+ .catch((error) => {
434
+ console.error("[rpcWorker] shutdown error:", error);
435
+ })
436
+ .finally(() => {
437
+ process.exit(0);
438
+ });
439
+ return;
440
+ }
441
+ if (kind === "init") {
442
+ const initAndReady = async () => {
443
+ await host.init(message);
444
+ post(host.ready());
445
+ };
446
+ if (host.serial === true) {
447
+ enqueue(initAndReady);
448
+ }
449
+ else {
450
+ void initAndReady().catch((error) => {
451
+ console.error("[rpcWorker] init failed:", error);
452
+ process.exit(1);
453
+ });
454
+ }
455
+ return;
456
+ }
457
+ // A regular work request.
458
+ if (host.serial === true) {
459
+ enqueue(() => runRequest(message));
460
+ }
461
+ else {
462
+ void runRequest(message);
463
+ }
464
+ }
465
+ catch (error) {
466
+ // A synchronous dispatch failure: surface it. Init failures crash the
467
+ // worker (the client restarts); request failures are posted per-handler.
468
+ console.error("[rpcWorker] dispatch error:", error);
469
+ }
470
+ });
471
+ messagePort.on("messageerror", (error) => {
472
+ console.error("[rpcWorker] message deserialization error:", error);
473
+ });
474
+ });
475
+ }