dsh-ptc-plus 0.3.2 → 0.4.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/README.en.md +65 -78
- package/README.md +65 -78
- package/assets/ptc-plus-bindings-en.png +0 -0
- package/assets/ptc-plus-bindings-zh.png +0 -0
- package/assets/ptc-plus-repair-en.png +0 -0
- package/assets/ptc-plus-repair-zh.png +0 -0
- package/assets/ptc-plus-repl-workspace-en.png +0 -0
- package/assets/ptc-plus-repl-workspace-zh.png +0 -0
- package/assets/ptc-plus-settings-en.png +0 -0
- package/assets/ptc-plus-settings-zh.png +0 -0
- package/client.js +30596 -742
- package/compiler-amaro.cjs +5 -0
- package/compiler-amaro.cjs.map +6 -0
- package/compiler-asset-124f80c1bba92ff5fe79cfec6e8ad518f476de4a42514cae3919538887f510e8.txt +1 -0
- package/compiler-assets.json +3 -0
- package/compiler-core.cjs +554 -0
- package/compiler-core.cjs.map +6 -0
- package/compiler-platform.cjs +2 -0
- package/compiler-platform.cjs.map +6 -0
- package/compiler-typescript.cjs +388 -0
- package/compiler-typescript.cjs.map +6 -0
- package/docs/README.md +6 -0
- package/docs/adr/0004-declare-program-bindings-at-the-host.md +1 -1
- package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +5 -3
- package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +1 -1
- package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -0
- package/docs/adr/0010-session-log-derived-recovery-tips.md +59 -5
- package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +1 -1
- package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +3 -1
- package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +3 -1
- package/docs/adr/0014-persist-cell-rewrite-policy.md +6 -2
- package/docs/adr/0015-preserve-source-positions-through-rewrites.md +1 -1
- package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +49 -0
- package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +3 -3
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +5 -3
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +12 -0
- package/docs/adr/0023-global-user-bindings.md +104 -0
- package/docs/adr/0024-repl-console-observation.md +33 -0
- package/docs/adr/0025-use-versioned-logical-binding-identities.md +119 -0
- package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +21 -0
- package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +37 -0
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +103 -0
- package/docs/adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md +27 -0
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +27 -0
- package/docs/architecture.md +73 -21
- package/docs/binding-review-placement-design.md +200 -0
- package/docs/capability-projection.md +1 -1
- package/docs/client-ui.md +95 -19
- package/docs/durability-design.md +25 -10
- package/docs/evaluation.md +78 -6
- package/docs/installation.md +41 -6
- package/docs/publishing.md +5 -3
- package/docs/runtime-reference.md +72 -25
- package/docs/semantic-validation.md +29 -0
- package/docs/stateful-binding-updates-proposal.md +755 -0
- package/docs/user-bindings.en.md +64 -0
- package/docs/user-bindings.md +64 -0
- package/docs/value-wire.md +3 -3
- package/docs/verification.md +57 -0
- package/index.js +369 -92
- package/internal/ast-traversal.js +5 -2
- package/internal/binding-descriptors.js +1 -0
- package/internal/binding-pattern-traversal.js +25 -0
- package/internal/binding-pattern.js +26 -29
- package/internal/binding-source-analysis.js +391 -0
- package/internal/binding-update-policy.js +66 -0
- package/internal/bounded-worker-round.js +56 -0
- package/internal/callable-definition-environment.js +42 -0
- package/internal/callable-source-catalog.js +129 -0
- package/internal/callable-source-encoding.js +47 -0
- package/internal/callable-source-facts.js +459 -0
- package/internal/callable-source-format.js +2 -0
- package/internal/callable-source-lowering.js +123 -0
- package/internal/cell-analysis-contract.js +25 -0
- package/internal/cell-analysis.js +240 -290
- package/internal/cell-completion.js +15 -0
- package/internal/cell-error.js +7 -0
- package/internal/cell-parser.js +66 -0
- package/internal/cell-rewriter.js +3 -677
- package/internal/commonjs-export-evidence.js +63 -0
- package/internal/commonjs-source-evidence.js +72 -0
- package/internal/compiler-amaro.cjs +2 -0
- package/internal/compiler-compression.js +12 -0
- package/internal/compiler-data.js +58 -0
- package/internal/compiler-descriptors.js +54 -0
- package/internal/compiler-dynamic-cache.js +48 -0
- package/internal/compiler-entry.js +35 -0
- package/internal/compiler-generated-names.js +17 -0
- package/internal/compiler-intrinsics.js +84 -0
- package/internal/compiler-module-links.js +34 -0
- package/internal/compiler-operations.js +61 -0
- package/internal/compiler-platform-factory.js +84 -0
- package/internal/compiler-platform.js +136 -0
- package/internal/compiler-record-roles.js +53 -0
- package/internal/compiler-region-output.js +259 -0
- package/internal/compiler-resource-lowering.js +103 -0
- package/internal/compiler-scope-facts.js +115 -0
- package/internal/compiler-service.js +109 -0
- package/internal/compiler-source-cache.js +32 -0
- package/internal/compiler-source-pieces.js +93 -0
- package/internal/compiler-source-regions.js +132 -0
- package/internal/compiler-storage-source.js +54 -0
- package/internal/compiler-text.js +15 -0
- package/internal/compiler-typescript.cjs +2 -0
- package/internal/config-spec.js +83 -27
- package/internal/cordis-tools-owner.js +130 -21
- package/internal/diagnostic.js +27 -1
- package/internal/direct-surface-owner.js +76 -23
- package/internal/dynamic-binding-evidence.js +16 -0
- package/internal/dynamic-environment-compiler.js +779 -0
- package/internal/dynamic-environment-integration.js +87 -0
- package/internal/dynamic-environment-runtime.js +362 -0
- package/internal/dynamic-native-calls.js +150 -0
- package/internal/dynamic-native-runtime.js +248 -0
- package/internal/dynamic-scope-analysis.js +90 -0
- package/internal/edit-transport-owner.js +27 -2
- package/internal/execution-seam-compat.js +193 -0
- package/internal/failure-reporting.js +118 -18
- package/internal/host-rpc.js +66 -0
- package/internal/isolated-worker.js +548 -0
- package/internal/kernel-child.js +139 -0
- package/internal/kernel-worker.js +904 -120
- package/internal/language-semantics.js +14 -0
- package/internal/legacy-binding-catalog.js +48 -0
- package/internal/legacy-cell-analysis.js +600 -0
- package/internal/legacy-cell-parser.js +50 -0
- package/internal/legacy-cell-rewriter.js +631 -0
- package/internal/legacy-repl-convenience.js +291 -0
- package/internal/managed-module-operations.js +170 -0
- package/internal/module-compilation.js +321 -0
- package/internal/module-runtime-platform.js +19 -0
- package/internal/module-transform-contract.js +12 -0
- package/internal/native-await-declarations.js +68 -0
- package/internal/native-root-compilation.js +54 -0
- package/internal/native-root-dynamic.js +67 -0
- package/internal/recovery-tips.js +27 -15
- package/internal/rejected-cell-editor.js +13 -8
- package/internal/repl-convenience.js +2 -289
- package/internal/repl-memory-projection.js +81 -12
- package/internal/repl-observation-interest.js +70 -0
- package/internal/repl-rewrite-contract.js +6 -0
- package/internal/repl-scope-normalizer.js +2091 -0
- package/internal/repl-value-observer.js +114 -0
- package/internal/rpc-contract.js +52 -0
- package/internal/runtime-bridge-owner.js +172 -28
- package/internal/runtime-config.js +48 -52
- package/internal/runtime-contexts.js +82 -10
- package/internal/runtime-messages.js +111 -0
- package/internal/session-cell-executor.js +401 -87
- package/internal/session-journal-recovery.js +684 -0
- package/internal/session-journal-schema.js +106 -0
- package/internal/session-journal.js +184 -654
- package/internal/session-log-view.js +42 -9
- package/internal/session-runtime.js +256 -95
- package/internal/session-state.js +247 -71
- package/internal/source-position-map.js +211 -71
- package/internal/stateful-module-compiler.js +154 -0
- package/internal/stateful-module-runtime.js +303 -0
- package/internal/stateful-root-compiler.js +598 -0
- package/internal/stateful-root-runtime.js +340 -0
- package/internal/typert-codec-compat.js +28 -0
- package/internal/typescript-transform.js +29 -0
- package/internal/user-binding-authoring.js +70 -0
- package/internal/user-binding-console-worker.js +184 -0
- package/internal/user-binding-console.js +148 -0
- package/internal/user-binding-draft-projection.js +335 -0
- package/internal/user-binding-model-context.js +31 -0
- package/internal/user-binding-runner.js +56 -0
- package/internal/user-bindings-owner.js +1065 -0
- package/internal/user-bindings-store.js +293 -0
- package/internal/user-bindings.js +353 -0
- package/internal/value-wire-schema.js +28 -0
- package/internal/value-wire.js +29 -20
- package/internal/worker-client.js +57 -67
- package/internal/worker-environment.js +45 -0
- package/internal/worker-output-relay.js +29 -0
- package/internal/worker-shutdown.js +14 -0
- package/package.json +75 -7
- package/src/client-activity.js +88 -53
- package/src/client-authoring-view.js +580 -0
- package/src/client-binding-review.js +230 -0
- package/src/client-bindings-data.js +29 -0
- package/src/client-catalog.js +127 -0
- package/src/client-code-editor.js +84 -0
- package/src/client-console.js +112 -0
- package/src/client-copy.js +370 -0
- package/src/client-feature-gates.js +62 -0
- package/src/client-host-compat.js +150 -0
- package/src/client-indicator-view.js +267 -0
- package/src/client-repl-view.js +292 -0
- package/src/client-rpc.js +20 -0
- package/src/client-settings-view.js +221 -0
- package/src/client-styles.js +106 -0
- package/src/client-tool-view.js +108 -0
- package/src/client-transport.js +159 -0
- package/src/client-workbench.js +521 -0
- package/src/client.js +233 -615
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { fork } from 'node:child_process'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { PassThrough } from 'node:stream'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fallback bound for a helper that answers the stop request and then keeps the
|
|
9
|
+
* process alive with unrelated work of its own.
|
|
10
|
+
*/
|
|
11
|
+
const ISOLATED_RELEASE_MS = 1000
|
|
12
|
+
/** Absolute bound after which an unresponsive helper is killed. */
|
|
13
|
+
const ISOLATED_STOP_MS = 5000
|
|
14
|
+
/** Bound on the helper's output pipes really ending after its process exited. */
|
|
15
|
+
const ISOLATED_OUTPUT_MS = 5000
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A worker-shaped transport whose worker lives in a killable helper process.
|
|
19
|
+
*
|
|
20
|
+
* Callers keep the worker_threads surface they already use: message, error and
|
|
21
|
+
* exit events, stdout/stderr platform pipes, postMessage to the worker's parent
|
|
22
|
+
* port, and terminate. The private kernel channel is exposed as the ready
|
|
23
|
+
* handshake's port adapter. Killing this transport targets the helper process,
|
|
24
|
+
* so a kernel that never answers is reclaimed without ending the host.
|
|
25
|
+
*/
|
|
26
|
+
export class IsolatedWorker extends EventEmitter {
|
|
27
|
+
constructor({ helper, entry, workerData, resourceLimits, env, protocol = 'kernel' }) {
|
|
28
|
+
super()
|
|
29
|
+
// The entry follows the worker constructor's contract: a URL or a string
|
|
30
|
+
// filename. Anything else fails here, before a helper process exists.
|
|
31
|
+
if (!(entry instanceof URL) && (typeof entry !== 'string' || entry.length === 0)) {
|
|
32
|
+
throw new TypeError('isolated worker entry must be a URL or a non-empty string filename')
|
|
33
|
+
}
|
|
34
|
+
// The init message crosses advanced IPC, which cannot rebuild a URL object, so
|
|
35
|
+
// the boundary normalizes it to one absolute file URL string. A path follows
|
|
36
|
+
// the worker constructor's rule: resolved from the current working directory.
|
|
37
|
+
const entrySpec = entry instanceof URL
|
|
38
|
+
? entry.href
|
|
39
|
+
: entry.slice(0, 5).toLowerCase() === 'file:'
|
|
40
|
+
? new URL(entry).href
|
|
41
|
+
: pathToFileURL(resolve(entry)).href
|
|
42
|
+
this.stdout = new PassThrough()
|
|
43
|
+
this.stderr = new PassThrough()
|
|
44
|
+
this.protocol = protocol
|
|
45
|
+
this.exited = false
|
|
46
|
+
this.exitCode = undefined
|
|
47
|
+
this.exitSignal = undefined
|
|
48
|
+
this.released = false
|
|
49
|
+
this.stop = undefined
|
|
50
|
+
this.reclamationFailure = undefined
|
|
51
|
+
this.kernel = new EventEmitter()
|
|
52
|
+
this.utilization = { idle: 0, active: 0 }
|
|
53
|
+
this.utilizationSequence = 0
|
|
54
|
+
this.utilizationRequests = new Map()
|
|
55
|
+
this.pendingErrors = []
|
|
56
|
+
this.performance = {
|
|
57
|
+
/** Utilization reported by the helper, in the shape callers already use. */
|
|
58
|
+
eventLoopUtilization: (first) => {
|
|
59
|
+
const current = this.utilization
|
|
60
|
+
if (first === undefined) return { ...current }
|
|
61
|
+
return { idle: current.idle - first.idle, active: current.active - first.active }
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
// child_process.fork mutates the env object it receives by copying
|
|
65
|
+
// NODE_V8_COVERAGE into it. Snapshot the caller's projection first so the
|
|
66
|
+
// inner Worker receives exactly the projected user environment, while the
|
|
67
|
+
// helper process can still carry the host's coverage variable.
|
|
68
|
+
const workerEnvironment = env === undefined ? undefined : { ...env }
|
|
69
|
+
this.child = fork(helper, [], {
|
|
70
|
+
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
71
|
+
env,
|
|
72
|
+
serialization: 'advanced',
|
|
73
|
+
// The helper starts with the host's own startup arguments removed, matching
|
|
74
|
+
// the isolation the in-host worker had with execArgv: [].
|
|
75
|
+
execArgv: [],
|
|
76
|
+
})
|
|
77
|
+
this.child.stdout.pipe(this.stdout)
|
|
78
|
+
this.child.stderr.pipe(this.stderr)
|
|
79
|
+
this.child.on('exit', (code, signal) => {
|
|
80
|
+
this.exited = true
|
|
81
|
+
this.exitCode = code
|
|
82
|
+
this.exitSignal = signal
|
|
83
|
+
this.#rejectUtilizationRequests(new Error('kernel helper exited before answering utilization sampling'))
|
|
84
|
+
})
|
|
85
|
+
// A ChildProcess 'close' follows 'exit' once its stdio has really ended, which
|
|
86
|
+
// is the only fact that proves the pipes are drained.
|
|
87
|
+
this.closed = new Promise(resolve => { this.resolveClosed = resolve })
|
|
88
|
+
this.child.on('close', () => {
|
|
89
|
+
this.ioClosed = true
|
|
90
|
+
this.resolveClosed()
|
|
91
|
+
this.stdout.end()
|
|
92
|
+
this.stderr.end()
|
|
93
|
+
for (const error of this.pendingErrors) this.emit('error', error)
|
|
94
|
+
this.pendingErrors.length = 0
|
|
95
|
+
// Consumers publish process failures from exit. Delay that worker-shaped
|
|
96
|
+
// event until close proves every platform output pipe has drained, while
|
|
97
|
+
// retaining the real child exit fact above for stop deadlines.
|
|
98
|
+
if (this.exited) this.emit('exit', this.exitCode, this.exitSignal)
|
|
99
|
+
this.emit('close')
|
|
100
|
+
})
|
|
101
|
+
this.child.on('error', error => {
|
|
102
|
+
this.#rejectUtilizationRequests(error)
|
|
103
|
+
this.pendingErrors.push(error)
|
|
104
|
+
this.emit('fault', error)
|
|
105
|
+
})
|
|
106
|
+
// A helper that loses its IPC channel while still running is an instance the
|
|
107
|
+
// owner must reclaim, so the disconnect is forwarded as its own fact.
|
|
108
|
+
this.child.on('disconnect', () => {
|
|
109
|
+
// A helper that already exited disconnects as part of its normal end; that is
|
|
110
|
+
// its exit fact, not a transport failure.
|
|
111
|
+
if (!this.exited) {
|
|
112
|
+
this.#rejectUtilizationRequests(new Error('kernel helper disconnected before answering utilization sampling'))
|
|
113
|
+
this.emit('disconnect')
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
this.child.on('message', message => {
|
|
117
|
+
if (message?.type === 'child-error') {
|
|
118
|
+
const error = new Error(message.message)
|
|
119
|
+
if (message.name !== undefined) error.name = message.name
|
|
120
|
+
if (message.code !== undefined) error.code = message.code
|
|
121
|
+
if (message.stack !== undefined) error.stack = message.stack
|
|
122
|
+
this.#rejectUtilizationRequests(error)
|
|
123
|
+
this.pendingErrors.push(error)
|
|
124
|
+
// The owner needs the control failure now so it can reclaim a helper
|
|
125
|
+
// that remains alive. Consumers receive error only after child close,
|
|
126
|
+
// once the platform output pipes have drained.
|
|
127
|
+
this.emit('fault', error)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
if (message?.type === 'inner-worker-exit') { this.emit('inner-worker-exit', message.code); return }
|
|
131
|
+
if (message?.type === 'utilization-sample') {
|
|
132
|
+
const pending = this.utilizationRequests.get(message.id)
|
|
133
|
+
if (pending !== undefined) {
|
|
134
|
+
this.utilizationRequests.delete(message.id)
|
|
135
|
+
clearTimeout(pending.timer)
|
|
136
|
+
this.utilization = message.utilization
|
|
137
|
+
pending.resolve(message.utilization)
|
|
138
|
+
}
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
if (message?.type === 'child-utilization') { this.utilization = message.utilization; return }
|
|
142
|
+
if (message?.type === 'child-limits') { this.resourceLimits = message.limits; return }
|
|
143
|
+
if (message?.type === 'ready') {
|
|
144
|
+
this.ready = true
|
|
145
|
+
// The helper samples the inner worker when it becomes ready. Adopt that
|
|
146
|
+
// fresh baseline so a later compute-budget delta never counts startup
|
|
147
|
+
// work that already happened before the worker was ready.
|
|
148
|
+
if (message.utilization !== undefined) this.utilization = message.utilization
|
|
149
|
+
// Only the private-channel protocol carries a port to the caller; a
|
|
150
|
+
// parent-port caller must never see the transport handshake as its first
|
|
151
|
+
// own result message.
|
|
152
|
+
if (this.protocol === 'kernel') this.emit('message', { type: 'ready', port: this.#portAdapter() })
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
if (message?.type === 'worker-message') {
|
|
156
|
+
// The stop acknowledgement belongs to this transport, not to the caller's
|
|
157
|
+
// own protocol, which only sees its own request/result messages.
|
|
158
|
+
if (message.value?.type === 'shutdown-released') { this.released = true; return }
|
|
159
|
+
this.emit('message', message.value)
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
if (message?.type === 'kernel-message') {
|
|
163
|
+
if (message.utilization !== undefined) this.utilization = message.utilization
|
|
164
|
+
this.kernel.emit('message', message.value)
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
try {
|
|
168
|
+
this.child.send({ type: 'init', entry: entrySpec, workerData, resourceLimits, protocol, env: workerEnvironment })
|
|
169
|
+
} catch (error) {
|
|
170
|
+
// The forked helper already exists here, so the failure carries its handle:
|
|
171
|
+
// the owner has to reclaim that child instead of treating it as never started.
|
|
172
|
+
// Keep the transport itself: it owns the child handle, the exit listeners
|
|
173
|
+
// and terminate(), so the owner can reclaim this instance through the same
|
|
174
|
+
// close path instead of a parallel raw-child implementation.
|
|
175
|
+
error.transport = this
|
|
176
|
+
error.child = this.child
|
|
177
|
+
throw error
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Port-shaped adapter over this process's IPC channel to the helper. */
|
|
182
|
+
#portAdapter() {
|
|
183
|
+
return {
|
|
184
|
+
postMessage: value => {
|
|
185
|
+
if (this.exited || this.child.connected === false) return
|
|
186
|
+
this.child.send({ type: 'kernel-message', value })
|
|
187
|
+
},
|
|
188
|
+
on: (event, handler) => { if (event === 'message') this.kernel.on('message', handler) },
|
|
189
|
+
once: (event, handler) => { if (event === 'message') this.kernel.once('message', handler) },
|
|
190
|
+
off: (event, handler) => { if (event === 'message') this.kernel.off('message', handler) },
|
|
191
|
+
close: () => {},
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Send one message to the kernel worker's own parent port. */
|
|
196
|
+
postMessage(value) {
|
|
197
|
+
if (this.exited || this.child.connected === false) return
|
|
198
|
+
this.child.send({ type: 'worker-message', value })
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Read the inner worker's current event-loop utilization from the helper.
|
|
203
|
+
*
|
|
204
|
+
* Periodic helper samples are not a request-start baseline: user callbacks
|
|
205
|
+
* can finish after the last sample and before the next cell is submitted.
|
|
206
|
+
* The helper answers this request from its own loop without waiting for the
|
|
207
|
+
* inner worker, so the returned sample includes work that has already run.
|
|
208
|
+
*
|
|
209
|
+
* @returns {Promise<{idle: number, active: number}>} Current utilization.
|
|
210
|
+
* Rejects when the helper cannot answer before its bounded deadline.
|
|
211
|
+
*/
|
|
212
|
+
sampleUtilization() {
|
|
213
|
+
if (this.exited || this.child.connected === false) {
|
|
214
|
+
return Promise.reject(new Error('kernel helper is not available for utilization sampling'))
|
|
215
|
+
}
|
|
216
|
+
const id = ++this.utilizationSequence
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
const timer = setTimeout(() => {
|
|
219
|
+
this.utilizationRequests.delete(id)
|
|
220
|
+
reject(new Error('kernel helper did not answer utilization sampling before its deadline'))
|
|
221
|
+
}, ISOLATED_RELEASE_MS)
|
|
222
|
+
this.utilizationRequests.set(id, { resolve, reject, timer })
|
|
223
|
+
try {
|
|
224
|
+
this.child.send({ type: 'sample-utilization', id })
|
|
225
|
+
} catch (error) {
|
|
226
|
+
clearTimeout(timer)
|
|
227
|
+
this.utilizationRequests.delete(id)
|
|
228
|
+
reject(error)
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Fail every request that still needs the helper's utilization answer. */
|
|
234
|
+
#rejectUtilizationRequests(error) {
|
|
235
|
+
for (const pending of this.utilizationRequests.values()) {
|
|
236
|
+
clearTimeout(pending.timer)
|
|
237
|
+
pending.reject(error)
|
|
238
|
+
}
|
|
239
|
+
this.utilizationRequests.clear()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Stop the kernel worker and wait for the helper process to exit.
|
|
244
|
+
*
|
|
245
|
+
* An answering helper gets the release bound to leave on its own; a helper
|
|
246
|
+
* that keeps running anyway, or that never answers, is killed at the absolute
|
|
247
|
+
* bound. Only the real child exit is reported, and a kill that the platform
|
|
248
|
+
* refuses stays observable as a rejected stop.
|
|
249
|
+
*
|
|
250
|
+
* @returns {Promise<void>} Resolves after the helper process exited.
|
|
251
|
+
*/
|
|
252
|
+
terminate() {
|
|
253
|
+
// One close per transport: concurrent callers share the same outcome, so a
|
|
254
|
+
// failure is reported to every caller instead of leaving one of them pending.
|
|
255
|
+
this.stop ??= this.#stop()
|
|
256
|
+
return this.stop
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async #stop() {
|
|
260
|
+
if (this.exited) return
|
|
261
|
+
// Both cutoffs are anchored at the moment the close starts: an acknowledgement
|
|
262
|
+
// never postpones the kill, and a kill still leaves a positive bounded window
|
|
263
|
+
// for the helper's real exit to arrive.
|
|
264
|
+
const started = Date.now()
|
|
265
|
+
const killAt = started + ISOLATED_RELEASE_MS
|
|
266
|
+
const confirmBy = started + ISOLATED_STOP_MS
|
|
267
|
+
const exited = new Promise(resolve => { this.child.once('exit', resolve) })
|
|
268
|
+
const onReleased = message => {
|
|
269
|
+
if (message?.type === 'worker-message' && message.value?.type === 'shutdown-released') this.released = true
|
|
270
|
+
}
|
|
271
|
+
this.child.on('message', onReleased)
|
|
272
|
+
try {
|
|
273
|
+
// A channel that refuses the stop request must not skip the kill and exit
|
|
274
|
+
// confirmation: the helper is still this instance's responsibility.
|
|
275
|
+
try {
|
|
276
|
+
if (this.child.connected !== false) this.child.send({ type: 'worker-message', value: { type: 'shutdown' } })
|
|
277
|
+
} catch {
|
|
278
|
+
// The channel is closed; the kill path below owns the instance.
|
|
279
|
+
}
|
|
280
|
+
if (!await leftWithin(this, remaining(killAt))) {
|
|
281
|
+
if (!this.exited) {
|
|
282
|
+
try {
|
|
283
|
+
await this.kill()
|
|
284
|
+
} catch (error) {
|
|
285
|
+
// Reclamation failed: keep the failure observable on the transport so
|
|
286
|
+
// the owner can retain responsibility for this instance.
|
|
287
|
+
this.reclamationFailure = error
|
|
288
|
+
throw error
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (!this.exited && !await leftWithin(this, remaining(confirmBy))) {
|
|
292
|
+
const failure = new Error('kernel helper did not exit before the close deadline')
|
|
293
|
+
this.reclamationFailure = failure
|
|
294
|
+
throw failure
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
await exited
|
|
298
|
+
} finally {
|
|
299
|
+
this.child.off('message', onReleased)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Kill the helper process; a refused kill stays a visible failure. */
|
|
304
|
+
async kill() {
|
|
305
|
+
const killed = this.child.kill()
|
|
306
|
+
if (killed === false && !this.exited) throw new Error('kernel helper could not be killed')
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Own every plugin transport instance until its real exit and cleanup.
|
|
312
|
+
*
|
|
313
|
+
* Instances are registered before they start, so a fork failure, a control
|
|
314
|
+
* error, a close refusal and a late real exit all keep their responsibility
|
|
315
|
+
* here instead of disappearing behind a swallowed rejection.
|
|
316
|
+
*/
|
|
317
|
+
export class IsolatedOwner {
|
|
318
|
+
/**
|
|
319
|
+
* @param {object} [options] - Transport factory, injectable for tests.
|
|
320
|
+
* @param {(options: object) => object} [options.create] - Builds one transport.
|
|
321
|
+
*/
|
|
322
|
+
constructor({ create = options => new IsolatedWorker(options) } = {}) {
|
|
323
|
+
this.create = create
|
|
324
|
+
this.instances = new Map()
|
|
325
|
+
this.nextId = 0
|
|
326
|
+
this.disposal = undefined
|
|
327
|
+
this.disposed = false
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Register one instance before it starts and observe its lifecycle facts. */
|
|
331
|
+
start(options) {
|
|
332
|
+
if (this.disposed) throw new Error('isolated owner is disposing')
|
|
333
|
+
const id = String(++this.nextId)
|
|
334
|
+
const record = { id, state: 'starting', failure: undefined, stopping: undefined, transport: undefined, exitObserved: false, neverStarted: false, cleanup: options.cleanup }
|
|
335
|
+
this.instances.set(id, record)
|
|
336
|
+
let transport
|
|
337
|
+
try {
|
|
338
|
+
transport = this.create(options)
|
|
339
|
+
} catch (error) {
|
|
340
|
+
record.failure = error
|
|
341
|
+
if (error?.transport !== undefined) {
|
|
342
|
+
// The helper process exists even though startup threw, and the transport
|
|
343
|
+
// that owns it is adopted so the close path can still reclaim it.
|
|
344
|
+
record.transport = error.transport
|
|
345
|
+
record.state = 'failed'
|
|
346
|
+
error.transport.on('error', transportError => this.#onInstanceError(record, transportError))
|
|
347
|
+
error.transport.on('fault', transportError => this.#onInstanceError(record, transportError))
|
|
348
|
+
error.transport.once('exit', () => this.#onExit(record))
|
|
349
|
+
error.transport.once('disconnect', () => this.#onInstanceError(record, new Error('helper disconnected')))
|
|
350
|
+
error.transport.once('close', () => { void this.#settle(record) })
|
|
351
|
+
// A startup that threw after the fork already owns a live helper, so the
|
|
352
|
+
// owner reclaims it now instead of waiting for an external dispose.
|
|
353
|
+
void this.stop(record.id).catch(() => {})
|
|
354
|
+
} else {
|
|
355
|
+
// No child was created: this instance's own terminal fact, released after
|
|
356
|
+
// its cleanup, without inventing an exit.
|
|
357
|
+
record.neverStarted = true
|
|
358
|
+
record.state = 'never-started'
|
|
359
|
+
this.#settle(record)
|
|
360
|
+
}
|
|
361
|
+
throw error
|
|
362
|
+
}
|
|
363
|
+
record.transport = transport
|
|
364
|
+
record.state = 'running'
|
|
365
|
+
// Persistent, so a second error event can never become an uncaught exception
|
|
366
|
+
// once the first listener was consumed.
|
|
367
|
+
transport.on('error', error => this.#onInstanceError(record, error))
|
|
368
|
+
transport.on('fault', error => this.#onInstanceError(record, error))
|
|
369
|
+
transport.once('exit', () => this.#onExit(record))
|
|
370
|
+
transport.once('disconnect', () => this.#onInstanceError(record, new Error('helper disconnected')))
|
|
371
|
+
// The real stream close is a terminal fact of its own: an instance retained
|
|
372
|
+
// because its output had not drained yet must be released when it does.
|
|
373
|
+
transport.once('close', () => { void this.#settle(record) })
|
|
374
|
+
return { id, transport, record }
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#onInstanceError(record, error) {
|
|
378
|
+
// Once the instance really exited, a later transport error cannot replace that
|
|
379
|
+
// fact; the exit observation owns the release.
|
|
380
|
+
if (record.exitObserved === true || record.transport?.exited === true) return
|
|
381
|
+
record.failure ??= error
|
|
382
|
+
record.state = 'failed'
|
|
383
|
+
// A control error still leaves an instance to reclaim, so the owner takes over
|
|
384
|
+
// its close instead of waiting for an external dispose.
|
|
385
|
+
void this.stop(record.id).catch(() => {})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
#onExit(record) {
|
|
389
|
+
record.exitObserved = true
|
|
390
|
+
void this.#settle(record)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
#settle(record) {
|
|
394
|
+
// Release needs this instance's own terminal fact (a real exit, or the
|
|
395
|
+
// never-started outcome) and this instance's own cleanup to succeed.
|
|
396
|
+
if (record.neverStarted !== true && record.exitObserved !== true) return
|
|
397
|
+
if (record.settling !== undefined) return record.settling
|
|
398
|
+
// Queue user cleanup so the shared attempt is installed before cleanup can
|
|
399
|
+
// synchronously finish or throw and clear the marker for a later retry.
|
|
400
|
+
const settling = Promise.resolve().then(async () => {
|
|
401
|
+
if (record.neverStarted !== true && record.transport !== undefined) {
|
|
402
|
+
const closed = await outputClosed(record.transport, ISOLATED_OUTPUT_MS)
|
|
403
|
+
if (!closed) {
|
|
404
|
+
// Output never drained: keep the instance and say so instead of
|
|
405
|
+
// pretending the pipes ended naturally.
|
|
406
|
+
record.failure = new Error('helper output did not end before the release deadline')
|
|
407
|
+
record.state = 'output-pending'
|
|
408
|
+
// A retained failure must not latch the attempt: a later real terminal
|
|
409
|
+
// fact (or a later disposal) has to try again and stay visible.
|
|
410
|
+
record.settling = undefined
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
if (typeof record.cleanup === 'function') await record.cleanup()
|
|
416
|
+
} catch (error) {
|
|
417
|
+
// Cleanup failure keeps the instance and its responsibility visible.
|
|
418
|
+
record.failure = error
|
|
419
|
+
record.state = 'cleanup-failed'
|
|
420
|
+
record.settling = undefined
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
record.state = 'reclaimed'
|
|
424
|
+
this.instances.delete(record.id)
|
|
425
|
+
})
|
|
426
|
+
record.settling = settling
|
|
427
|
+
return settling
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Stop one instance; concurrent calls share an in-flight attempt. */
|
|
431
|
+
stop(id) {
|
|
432
|
+
const record = this.instances.get(id)
|
|
433
|
+
if (record === undefined) return Promise.resolve()
|
|
434
|
+
if (record.stopping !== undefined) return record.stopping
|
|
435
|
+
const operation = this.#stop(record)
|
|
436
|
+
record.stopping = operation
|
|
437
|
+
// A retained cleanup failure or pending output must not latch stop: after
|
|
438
|
+
// the operation settles, a later call has to retry #settle. Concurrent
|
|
439
|
+
// callers still share the in-flight promise above.
|
|
440
|
+
operation.then(
|
|
441
|
+
() => { if (record.stopping === operation) record.stopping = undefined },
|
|
442
|
+
() => { if (record.stopping === operation) record.stopping = undefined },
|
|
443
|
+
)
|
|
444
|
+
return operation
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async #stop(record) {
|
|
448
|
+
if (record.neverStarted === true || record.transport === undefined) {
|
|
449
|
+
await this.#settle(record)
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
if (record.transport.exited === true) record.exitObserved = true
|
|
453
|
+
// A retained cleanup/output attempt already owns the transport's exit. Do
|
|
454
|
+
// not terminate it again; just retry its cleanup/output settlement.
|
|
455
|
+
if (record.state === 'cleanup-failed'
|
|
456
|
+
|| record.state === 'output-pending'
|
|
457
|
+
|| record.transport.exited === true) {
|
|
458
|
+
await this.#settle(record)
|
|
459
|
+
return
|
|
460
|
+
}
|
|
461
|
+
record.state = 'closing'
|
|
462
|
+
try {
|
|
463
|
+
await record.transport.terminate()
|
|
464
|
+
} catch (error) {
|
|
465
|
+
// The instance stays registered: a refusal is visible again on the next
|
|
466
|
+
// stop or dispose, and a late real exit still releases it.
|
|
467
|
+
record.failure = error
|
|
468
|
+
record.state = 'unreclaimed'
|
|
469
|
+
throw error
|
|
470
|
+
}
|
|
471
|
+
if (record.transport.exited === true) record.exitObserved = true
|
|
472
|
+
// Await the instance's own cleanup so stop() never reports done before its
|
|
473
|
+
// cleanup outcome (success or retained failure) is known.
|
|
474
|
+
await this.#settle(record)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Stop every owned instance but keep the owner usable for later starts.
|
|
479
|
+
*
|
|
480
|
+
* Reconfiguration releases the current instances without ending the owner's
|
|
481
|
+
* life, while dispose() stays the terminal operation that refuses new starts.
|
|
482
|
+
*
|
|
483
|
+
* @returns {Promise<Error[]>} Every instance that could not be reclaimed.
|
|
484
|
+
*/
|
|
485
|
+
async releaseAll() {
|
|
486
|
+
return this.#releaseOwned()
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Stop every owned instance and report each one that could not be reclaimed. */
|
|
490
|
+
dispose() {
|
|
491
|
+
this.disposed = true
|
|
492
|
+
if (this.disposal !== undefined) return this.disposal
|
|
493
|
+
const operation = this.#dispose()
|
|
494
|
+
this.disposal = operation
|
|
495
|
+
// Keep concurrent dispose calls on one operation, but let a later call
|
|
496
|
+
// retry instances retained by cleanup-failed or output-pending states.
|
|
497
|
+
operation.then(
|
|
498
|
+
() => { if (this.disposal === operation) this.disposal = undefined },
|
|
499
|
+
() => { if (this.disposal === operation) this.disposal = undefined },
|
|
500
|
+
)
|
|
501
|
+
return operation
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async #dispose() {
|
|
505
|
+
return this.#releaseOwned()
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async #releaseOwned() {
|
|
509
|
+
const selected = [...this.instances.keys()].map((id) => {
|
|
510
|
+
const record = this.instances.get(id)
|
|
511
|
+
return { record, operation: this.stop(id) }
|
|
512
|
+
})
|
|
513
|
+
const results = await Promise.allSettled(selected.map(instance => instance.operation))
|
|
514
|
+
const failures = []
|
|
515
|
+
for (const [index, selectedInstance] of selected.entries()) {
|
|
516
|
+
const result = results[index]
|
|
517
|
+
if (result.status === 'rejected') failures.push(result.reason)
|
|
518
|
+
const { record } = selectedInstance
|
|
519
|
+
// Only #settle releases an instance; a retained cleanup failure is reported
|
|
520
|
+
// here once, without a second release decision.
|
|
521
|
+
if ((record?.state === 'cleanup-failed' || record?.state === 'output-pending') && record.failure !== undefined) {
|
|
522
|
+
failures.push(record.failure)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return failures
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function outputClosed(transport, ms) {
|
|
530
|
+
if (transport.ioClosed === true) return Promise.resolve(true)
|
|
531
|
+
return new Promise(resolve => {
|
|
532
|
+
const timer = setTimeout(() => { resolve(false) }, ms)
|
|
533
|
+
Promise.resolve(transport.closed).then(() => { clearTimeout(timer); resolve(true) }, () => { clearTimeout(timer); resolve(false) })
|
|
534
|
+
})
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function remaining(deadline) {
|
|
538
|
+
return Math.max(0, deadline - Date.now())
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function leftWithin(worker, ms) {
|
|
542
|
+
if (worker.exited) return Promise.resolve(true)
|
|
543
|
+
return new Promise(resolve => {
|
|
544
|
+
const timer = setTimeout(() => { worker.child.off('exit', onExit); resolve(false) }, ms)
|
|
545
|
+
const onExit = () => { clearTimeout(timer); resolve(true) }
|
|
546
|
+
worker.child.once('exit', onExit)
|
|
547
|
+
})
|
|
548
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads'
|
|
2
|
+
import { relayWorkerOutput } from './worker-output-relay.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Host one session kernel worker inside a killable helper process.
|
|
6
|
+
*
|
|
7
|
+
* Boundary: the host reaches this process over the process IPC channel, this
|
|
8
|
+
* process owns the kernel worker and its private MessagePort, and that port
|
|
9
|
+
* never crosses the process boundary. Kernel output reaches the host through the
|
|
10
|
+
* helper's own platform stdio pipes, so no byte is duplicated into IPC messages
|
|
11
|
+
* and counting stays with the host's output budget owner.
|
|
12
|
+
*/
|
|
13
|
+
const send = message => { if (typeof process.send === 'function') process.send(message) }
|
|
14
|
+
|
|
15
|
+
function sendBeforeDisconnect(message) {
|
|
16
|
+
return new Promise(resolve => {
|
|
17
|
+
if (typeof process.send !== 'function' || !process.connected) { resolve(); return }
|
|
18
|
+
process.send(message, () => resolve())
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function errorWire(error) {
|
|
23
|
+
return {
|
|
24
|
+
type: 'child-error',
|
|
25
|
+
message: error?.message ?? String(error),
|
|
26
|
+
name: error?.name,
|
|
27
|
+
code: error?.code,
|
|
28
|
+
stack: error?.stack,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let kernelPort
|
|
33
|
+
let worker
|
|
34
|
+
let stopping = false
|
|
35
|
+
let utilizationTimer
|
|
36
|
+
|
|
37
|
+
function releaseHelper() {
|
|
38
|
+
if (stopping) return
|
|
39
|
+
stopping = true
|
|
40
|
+
clearInterval(utilizationTimer)
|
|
41
|
+
utilizationTimer = undefined
|
|
42
|
+
kernelPort?.close()
|
|
43
|
+
kernelPort = undefined
|
|
44
|
+
process.removeAllListeners('message')
|
|
45
|
+
if (typeof process.disconnect === 'function' && process.connected) process.disconnect()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
process.on('message', message => {
|
|
49
|
+
if (worker !== undefined) {
|
|
50
|
+
// Host budget baselines must be sampled after any work that finished before
|
|
51
|
+
// this request. The helper answers directly without waiting for the worker,
|
|
52
|
+
// so a busy inner loop cannot hide the current utilization.
|
|
53
|
+
if (message?.type === 'sample-utilization') {
|
|
54
|
+
send({ type: 'utilization-sample', id: message.id, utilization: worker.performance.eventLoopUtilization() })
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
// Routed by ownership, not by handshake shape: parent-port requests reach the
|
|
58
|
+
// worker whenever it exists, and private messages need the port to be ready.
|
|
59
|
+
if (message?.type === 'kernel-message') kernelPort?.postMessage(message.value)
|
|
60
|
+
if (message?.type === 'worker-message') worker.postMessage(message.value)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
if (message?.type !== 'init' || typeof message.workerData !== 'object' || message.workerData === null) {
|
|
64
|
+
send({ type: 'child-error', message: 'helper requires one init message before the kernel worker starts' })
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
// The host boundary already normalized the entry to an absolute file URL and
|
|
68
|
+
// supplied an explicit, pre-fork snapshot of the user worker's environment.
|
|
69
|
+
// That projection is the user Worker contract; the helper's own coverage
|
|
70
|
+
// variable is deliberately not reused here.
|
|
71
|
+
worker = new Worker(message.entry === undefined ? new URL('./kernel-worker.js', import.meta.url) : new URL(message.entry), {
|
|
72
|
+
workerData: message.workerData,
|
|
73
|
+
resourceLimits: message.resourceLimits,
|
|
74
|
+
execArgv: [],
|
|
75
|
+
stdout: true,
|
|
76
|
+
stderr: true,
|
|
77
|
+
env: message.env,
|
|
78
|
+
})
|
|
79
|
+
// The inner worker reports its own effective limits; the host never echoes back
|
|
80
|
+
// what it requested.
|
|
81
|
+
send({ type: 'child-limits', limits: worker.resourceLimits })
|
|
82
|
+
const relayedOutput = Promise.allSettled([
|
|
83
|
+
relayWorkerOutput(worker.stdout, process.stdout),
|
|
84
|
+
relayWorkerOutput(worker.stderr, process.stderr),
|
|
85
|
+
])
|
|
86
|
+
let workerFailure
|
|
87
|
+
const failStartup = error => {
|
|
88
|
+
workerFailure ??= error
|
|
89
|
+
// A protocol-level startup failure can leave the Worker alive under
|
|
90
|
+
// coverage or another runtime hook. End it here so direct transport users
|
|
91
|
+
// also reach process close and receive the drained public error.
|
|
92
|
+
void worker.terminate().catch(terminationError => { workerFailure ??= terminationError })
|
|
93
|
+
}
|
|
94
|
+
worker.on('error', error => { workerFailure = error })
|
|
95
|
+
worker.on('exit', async code => {
|
|
96
|
+
const outputResults = await relayedOutput
|
|
97
|
+
const outputFailure = outputResults.find(result => result.status === 'rejected')?.reason
|
|
98
|
+
if (workerFailure !== undefined) await sendBeforeDisconnect(errorWire(workerFailure))
|
|
99
|
+
else if (outputFailure !== undefined) await sendBeforeDisconnect(errorWire(outputFailure))
|
|
100
|
+
await sendBeforeDisconnect({ type: 'inner-worker-exit', code })
|
|
101
|
+
releaseHelper()
|
|
102
|
+
process.exitCode = code ?? 0
|
|
103
|
+
})
|
|
104
|
+
// Battery of samples taken here, on this process's main thread: the inner
|
|
105
|
+
// worker's utilization stays readable while its own loop is blocked, which is
|
|
106
|
+
// what the host's compute budget needs.
|
|
107
|
+
utilizationTimer = setInterval(() => {
|
|
108
|
+
send({ type: 'child-utilization', utilization: worker.performance.eventLoopUtilization() })
|
|
109
|
+
}, 50)
|
|
110
|
+
if (message.protocol === 'parent-port') {
|
|
111
|
+
// Workbench and candidate workers speak on their own parent port instead of
|
|
112
|
+
// opening a private channel; both directions are relayed unchanged.
|
|
113
|
+
worker.on('message', value => send({ type: 'worker-message', value }))
|
|
114
|
+
send({ type: 'ready', utilization: worker.performance.eventLoopUtilization() })
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
worker.once('message', message => {
|
|
118
|
+
if (message?.type === 'startup-error' && typeof message.error === 'string') {
|
|
119
|
+
failStartup(new Error(message.error))
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
if (message?.type !== 'ready' || typeof message.port?.postMessage !== 'function') {
|
|
123
|
+
failStartup(new Error('kernel worker returned an invalid private channel'))
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
kernelPort = message.port
|
|
127
|
+
kernelPort.on('message', value => send({
|
|
128
|
+
type: 'kernel-message',
|
|
129
|
+
value,
|
|
130
|
+
// Fresh baseline for the inner worker at every relayed message, on top of
|
|
131
|
+
// the main thread's periodic sampling below.
|
|
132
|
+
utilization: worker.performance.eventLoopUtilization(),
|
|
133
|
+
}))
|
|
134
|
+
worker.on('message', value => {
|
|
135
|
+
if (value?.type !== 'ready') send({ type: 'worker-message', value })
|
|
136
|
+
})
|
|
137
|
+
send({ type: 'ready', utilization: worker.performance.eventLoopUtilization() })
|
|
138
|
+
})
|
|
139
|
+
})
|