dsh-ptc-plus 0.4.1 → 0.4.3
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 +164 -78
- package/README.md +165 -79
- package/assets/dsh-ptc-plus-banner-en.webp +0 -0
- package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
- package/assets/ptc-plus-icon.svg +5 -0
- package/client.js +854 -336
- package/compiler-core.cjs +161 -157
- package/compiler-core.cjs.map +3 -3
- package/docs/README.md +4 -1
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +8 -7
- package/docs/adr/0008-define-agent-operating-contract.md +2 -2
- package/docs/adr/0010-session-log-derived-recovery-tips.md +7 -0
- package/docs/adr/0016-archive-resolved-review-findings.md +3 -3
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +3 -3
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -0
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +14 -3
- package/docs/adr/0023-global-user-bindings.md +7 -7
- package/docs/adr/0025-use-versioned-logical-binding-identities.md +3 -3
- package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +23 -7
- package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +9 -5
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +26 -6
- package/docs/adr/0031-close-semantic-obligations-by-owner.md +29 -0
- package/docs/adr/README.md +4 -4
- package/docs/architecture.md +15 -10
- package/docs/binding-review-placement-design.md +1 -1
- package/docs/client-ui.md +9 -5
- package/docs/durability-design.md +12 -9
- package/docs/installation.md +27 -9
- package/docs/runtime-reference.md +23 -12
- package/docs/semantic-validation.md +7 -3
- package/docs/stateful-binding-updates-proposal.md +8 -4
- package/docs/user-bindings.en.md +10 -7
- package/docs/user-bindings.md +9 -6
- package/docs/value-wire.md +1 -1
- package/docs/verification-optimization-work.md +41 -0
- package/docs/verification.md +53 -2
- package/index.js +39 -34
- package/internal/async-local-storage-intrinsics.js +43 -0
- package/internal/binding-declaration-contract.js +8 -0
- package/internal/binding-source-analysis.js +381 -64
- package/internal/binding-update-policy.js +7 -1
- package/internal/callable-source-facts.js +37 -1
- package/internal/cell-analysis.js +85 -17
- package/internal/compiler-intrinsics.js +9 -43
- package/internal/compiler-module-links.js +7 -4
- package/internal/config-spec.js +1 -1
- package/internal/cordis-tools-owner.js +20 -10
- package/internal/dynamic-environment-integration.js +8 -4
- package/internal/dynamic-environment-runtime.js +24 -3
- package/internal/dynamic-native-calls.js +4 -1
- package/internal/dynamic-native-runtime.js +15 -1
- package/internal/edit-transport-owner.js +80 -20
- package/internal/execution-seam-compat.js +192 -30
- package/internal/failure-reporting.js +38 -30
- package/internal/isolated-worker.js +17 -10
- package/internal/kernel-worker.js +645 -393
- package/internal/message-sources.js +14 -0
- package/internal/module-compilation.js +7 -5
- package/internal/module-transform-contract.js +32 -2
- package/internal/record-utils.js +24 -15
- package/internal/repl-memory-projection.js +10 -0
- package/internal/repl-scope-normalizer.js +72 -35
- package/internal/repl-value-observer.js +145 -46
- package/internal/runtime-config.js +62 -2
- package/internal/runtime-intrinsics.js +104 -0
- package/internal/runtime-messages.js +6 -4
- package/internal/session-cell-executor.js +26 -2
- package/internal/session-journal-recovery.js +267 -67
- package/internal/session-journal-schema.js +30 -13
- package/internal/session-journal.js +485 -44
- package/internal/session-log-view.js +5 -4
- package/internal/session-runtime.js +46 -21
- package/internal/session-state.js +7 -29
- package/internal/settings-compat.js +66 -1
- package/internal/source-position-map.js +38 -0
- package/internal/stateful-root-compiler.js +3 -1
- package/internal/typescript-transform.js +4 -2
- package/internal/user-binding-console-worker.js +115 -41
- package/internal/user-binding-draft-projection.js +3 -2
- package/internal/user-bindings-owner.js +2 -1
- package/internal/user-bindings.js +16 -3
- package/internal/validated-parse-repair.js +6 -2
- package/internal/value-wire.js +189 -94
- package/internal/worker-client.js +99 -8
- package/internal/worker-cwd-virtualization.js +82 -54
- package/internal/worker-environment.js +20 -0
- package/internal/worker-output-capture.js +187 -0
- package/internal/worker-output-fence.js +26 -0
- package/internal/worker-realm-surfaces.js +197 -0
- package/internal/worker-repl-realm.js +140 -0
- package/package.json +37 -4
- package/src/client-activity.js +12 -2
- package/src/client-authoring-view.js +121 -62
- package/src/client-console.js +4 -2
- package/src/client-copy.js +4 -2
- package/src/client-feature-gates.js +7 -9
- package/src/client-host-compat.js +104 -27
- package/src/client-repl-view.js +3 -1
- package/src/client-styles.js +12 -1
- package/src/client-tool-view.js +3 -1
- package/src/client-transport.js +2 -2
- package/src/client-workbench.js +4 -3
- package/src/client.js +56 -17
|
@@ -238,11 +238,34 @@ export function createEditTransportOwner(ctx, {
|
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
const disposeInstalled = (installed) => {
|
|
242
|
+
const failures = []
|
|
243
|
+
for (const key of ['disposePresentation', 'disposeRegistration']) {
|
|
244
|
+
const dispose = installed[key]
|
|
245
|
+
if (dispose === undefined) continue
|
|
246
|
+
try {
|
|
247
|
+
dispose()
|
|
248
|
+
installed[key] = undefined
|
|
249
|
+
} catch (error) {
|
|
250
|
+
failures.push(error)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (failures.length > 0) {
|
|
254
|
+
throw new AggregateError(failures, 'ptc-plus: edit_run_code scope disposal failed')
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
241
258
|
const uninstall = (agent) => {
|
|
242
259
|
const installed = installedScopes.get(agent)
|
|
243
260
|
if (installed === undefined) return
|
|
244
|
-
|
|
245
|
-
|
|
261
|
+
try {
|
|
262
|
+
disposeInstalled(installed)
|
|
263
|
+
} finally {
|
|
264
|
+
if (installed.disposePresentation === undefined
|
|
265
|
+
&& installed.disposeRegistration === undefined) {
|
|
266
|
+
installedScopes.delete(agent)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
246
269
|
}
|
|
247
270
|
|
|
248
271
|
return Object.freeze({
|
|
@@ -251,11 +274,19 @@ export function createEditTransportOwner(ctx, {
|
|
|
251
274
|
currentDurableReplay = nextConfig.durableReplay
|
|
252
275
|
},
|
|
253
276
|
isInstalled(agent) {
|
|
254
|
-
|
|
277
|
+
const installed = installedScopes.get(agent)
|
|
278
|
+
return installed?.disposePresentation !== undefined
|
|
279
|
+
&& installed.disposeRegistration !== undefined
|
|
255
280
|
},
|
|
256
281
|
ensureInstalled(agent) {
|
|
257
282
|
const installed = installedScopes.get(agent)
|
|
258
|
-
if (installed !== undefined)
|
|
283
|
+
if (installed !== undefined) {
|
|
284
|
+
if (installed.disposePresentation !== undefined
|
|
285
|
+
&& installed.disposeRegistration !== undefined) {
|
|
286
|
+
return installed.definition
|
|
287
|
+
}
|
|
288
|
+
uninstall(agent)
|
|
289
|
+
}
|
|
259
290
|
const liveSchemas = typeof toolSchemasForAgent === 'function'
|
|
260
291
|
? toolSchemasForAgent(agent)
|
|
261
292
|
: []
|
|
@@ -271,24 +302,31 @@ export function createEditTransportOwner(ctx, {
|
|
|
271
302
|
if (typeof disposeRegistration !== 'function') {
|
|
272
303
|
throw new Error('ptc-plus: DSH tools.register did not return a disposer')
|
|
273
304
|
}
|
|
274
|
-
|
|
305
|
+
const nextInstalled = {
|
|
306
|
+
definition,
|
|
307
|
+
disposePresentation: undefined,
|
|
308
|
+
disposeRegistration,
|
|
309
|
+
sessionId: sessionId(agent),
|
|
310
|
+
}
|
|
311
|
+
installedScopes.set(agent, nextInstalled)
|
|
275
312
|
try {
|
|
276
|
-
disposePresentation = scopedTools.presentAs('both')
|
|
313
|
+
const disposePresentation = scopedTools.presentAs('both')
|
|
277
314
|
if (typeof disposePresentation !== 'function') {
|
|
278
315
|
throw new Error('ptc-plus: DSH tools.presentAs did not return a disposer')
|
|
279
316
|
}
|
|
317
|
+
nextInstalled.disposePresentation = disposePresentation
|
|
280
318
|
} catch (error) {
|
|
281
|
-
|
|
319
|
+
try {
|
|
320
|
+
uninstall(agent)
|
|
321
|
+
} catch (cleanupError) {
|
|
322
|
+
throw new AggregateError(
|
|
323
|
+
[error, cleanupError],
|
|
324
|
+
'ptc-plus: edit_run_code installation and rollback failed',
|
|
325
|
+
{ cause: error },
|
|
326
|
+
)
|
|
327
|
+
}
|
|
282
328
|
throw error
|
|
283
329
|
}
|
|
284
|
-
let active = true
|
|
285
|
-
const dispose = () => {
|
|
286
|
-
if (!active) return
|
|
287
|
-
active = false
|
|
288
|
-
disposePresentation()
|
|
289
|
-
disposeRegistration()
|
|
290
|
-
}
|
|
291
|
-
installedScopes.set(agent, { definition, dispose, sessionId: sessionId(agent) })
|
|
292
330
|
return definition
|
|
293
331
|
},
|
|
294
332
|
handleResult(exec, result) {
|
|
@@ -305,28 +343,50 @@ export function createEditTransportOwner(ctx, {
|
|
|
305
343
|
releaseClaim(pending.id, pending.targetCallSeq)
|
|
306
344
|
},
|
|
307
345
|
disposeAgent(agent) {
|
|
308
|
-
|
|
346
|
+
let failure
|
|
347
|
+
try {
|
|
348
|
+
uninstall(agent)
|
|
349
|
+
} catch (error) {
|
|
350
|
+
failure = error
|
|
351
|
+
}
|
|
309
352
|
const id = sessionId(agent)
|
|
310
353
|
if (id !== undefined) {
|
|
311
354
|
discardSettlements(id)
|
|
312
355
|
editClaims.delete(id)
|
|
313
356
|
}
|
|
357
|
+
if (failure !== undefined) throw failure
|
|
314
358
|
},
|
|
315
359
|
disposeSession(session) {
|
|
316
360
|
const id = String(session.id)
|
|
361
|
+
const failures = []
|
|
317
362
|
for (const [agent, installed] of installedScopes) {
|
|
318
363
|
if (installed.sessionId !== id) continue
|
|
319
|
-
|
|
320
|
-
|
|
364
|
+
try {
|
|
365
|
+
uninstall(agent)
|
|
366
|
+
} catch (error) {
|
|
367
|
+
failures.push(error)
|
|
368
|
+
}
|
|
321
369
|
}
|
|
322
370
|
discardSettlements(id)
|
|
323
371
|
editClaims.delete(id)
|
|
372
|
+
if (failures.length > 0) {
|
|
373
|
+
throw new AggregateError(failures, 'ptc-plus: edit_run_code session disposal failed')
|
|
374
|
+
}
|
|
324
375
|
},
|
|
325
376
|
dispose() {
|
|
326
377
|
discardSettlements()
|
|
327
378
|
editClaims.clear()
|
|
328
|
-
|
|
329
|
-
installedScopes.
|
|
379
|
+
const failures = []
|
|
380
|
+
for (const agent of [...installedScopes.keys()]) {
|
|
381
|
+
try {
|
|
382
|
+
uninstall(agent)
|
|
383
|
+
} catch (error) {
|
|
384
|
+
failures.push(error)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (failures.length > 0) {
|
|
388
|
+
throw new AggregateError(failures, 'ptc-plus: edit_run_code owner disposal failed')
|
|
389
|
+
}
|
|
330
390
|
},
|
|
331
391
|
})
|
|
332
392
|
}
|
|
@@ -35,6 +35,19 @@ export const LEGACY_EXECUTION_SEAM_SERVICE = 'codeRuntime'
|
|
|
35
35
|
|
|
36
36
|
const SEAM_SERVICES = Object.freeze([EXECUTION_SEAM_SERVICE, LEGACY_EXECUTION_SEAM_SERVICE])
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* How long the plugin waits for the host's execution seam before it reports the
|
|
40
|
+
* deployment as unsupported.
|
|
41
|
+
*
|
|
42
|
+
* Both seam names belong to the host's code-runtime row, so a composed host
|
|
43
|
+
* publishes one while the loader settles its entries. A profile that never
|
|
44
|
+
* registers either one cannot be served, and its activation promise must settle
|
|
45
|
+
* with a diagnostic: the host awaits loader settlement before it becomes
|
|
46
|
+
* usable, so an unbounded wait would park the whole host instead of failing
|
|
47
|
+
* this entry.
|
|
48
|
+
*/
|
|
49
|
+
const SEAM_ATTACHMENT_TIMEOUT_MS = 30_000
|
|
50
|
+
|
|
38
51
|
/**
|
|
39
52
|
* Descriptors the plugin replaces with the capabilities of its own execution.
|
|
40
53
|
* `executionInstructions` describes the wrapped provider's program model, which
|
|
@@ -105,15 +118,8 @@ export function createExecutionSeam(service, serviceName) {
|
|
|
105
118
|
if (installed !== undefined) throw new Error('ptc-plus: execution seam already taken over')
|
|
106
119
|
const saved = new Map()
|
|
107
120
|
const written = new Map()
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
if (!saved.has(name)) saved.set(name, previous)
|
|
111
|
-
// Preserve the shadowed property's visibility so a consumer that
|
|
112
|
-
// enumerates the provider sees the same member set.
|
|
113
|
-
Object.defineProperty(service, name, { enumerable: previous?.enumerable ?? false, ...descriptor })
|
|
114
|
-
written.set(name, Object.getOwnPropertyDescriptor(service, name))
|
|
115
|
-
}
|
|
116
|
-
define('run', {
|
|
121
|
+
const requested = new Map()
|
|
122
|
+
requested.set('run', {
|
|
117
123
|
configurable: true,
|
|
118
124
|
writable: true,
|
|
119
125
|
value: current
|
|
@@ -125,7 +131,32 @@ export function createExecutionSeam(service, serviceName) {
|
|
|
125
131
|
: request => execute(request),
|
|
126
132
|
})
|
|
127
133
|
for (const name of WITHHELD_DESCRIPTORS) {
|
|
128
|
-
|
|
134
|
+
requested.set(name, { configurable: true, value: name === 'executionInstructions' ? '' : undefined })
|
|
135
|
+
}
|
|
136
|
+
const extensible = Object.isExtensible(service)
|
|
137
|
+
for (const [name, descriptor] of requested) {
|
|
138
|
+
const previous = Object.getOwnPropertyDescriptor(service, name)
|
|
139
|
+
saved.set(name, previous)
|
|
140
|
+
const next = { enumerable: previous?.enumerable ?? false, ...descriptor }
|
|
141
|
+
requested.set(name, next)
|
|
142
|
+
const probe = {}
|
|
143
|
+
if (previous !== undefined) Object.defineProperty(probe, name, previous)
|
|
144
|
+
if (!extensible) Object.preventExtensions(probe)
|
|
145
|
+
Object.defineProperty(probe, name, next)
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
for (const [name, descriptor] of requested) {
|
|
149
|
+
Object.defineProperty(service, name, descriptor)
|
|
150
|
+
written.set(name, Object.getOwnPropertyDescriptor(service, name))
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
for (const name of [...written.keys()].reverse()) {
|
|
154
|
+
if (!descriptorsMatch(Object.getOwnPropertyDescriptor(service, name), written.get(name))) continue
|
|
155
|
+
const descriptor = saved.get(name)
|
|
156
|
+
if (descriptor === undefined) delete service[name]
|
|
157
|
+
else Object.defineProperty(service, name, descriptor)
|
|
158
|
+
}
|
|
159
|
+
throw error
|
|
129
160
|
}
|
|
130
161
|
const restore = () => {
|
|
131
162
|
if (installed !== restore) return
|
|
@@ -145,9 +176,10 @@ export function createExecutionSeam(service, serviceName) {
|
|
|
145
176
|
/**
|
|
146
177
|
* Wait for the host's execution seam and attach the plugin to the live one.
|
|
147
178
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
179
|
+
* The current service is preferred whenever both are live; the preceding
|
|
180
|
+
* generation attaches only when the current one is absent, and a current
|
|
181
|
+
* service registered later retires the live legacy injection before attaching.
|
|
182
|
+
* The injected scope requires the plugin's own core
|
|
151
183
|
* services as well, so the callback receives a context the plugin can work in,
|
|
152
184
|
* and everything the plugin registers there belongs to the seam the host
|
|
153
185
|
* registered: unloading that service unloads the plugin, and registering it
|
|
@@ -157,16 +189,107 @@ export function createExecutionSeam(service, serviceName) {
|
|
|
157
189
|
* @param {object} ctx - the plugin context.
|
|
158
190
|
* @param {object} options - `services` are the plugin's own required services,
|
|
159
191
|
* and `attach` is invoked with the injected scope and the seam handle.
|
|
160
|
-
*
|
|
192
|
+
* `attachmentTimeoutMs` bounds the wait for a seam that never appears.
|
|
193
|
+
* @returns {Promise<void>} settles after attachment with a valid Cordis effect value.
|
|
161
194
|
*/
|
|
162
|
-
export function installExecutionSeam(ctx, {
|
|
195
|
+
export function installExecutionSeam(ctx, {
|
|
196
|
+
services = [],
|
|
197
|
+
attach,
|
|
198
|
+
reportFailure,
|
|
199
|
+
attachmentTimeoutMs = SEAM_ATTACHMENT_TIMEOUT_MS,
|
|
200
|
+
}) {
|
|
163
201
|
let live
|
|
202
|
+
let stopping = false
|
|
203
|
+
let startWatcher
|
|
204
|
+
let unusableCurrent
|
|
205
|
+
const report = (error, name) => {
|
|
206
|
+
try {
|
|
207
|
+
reportFailure?.(error, name)
|
|
208
|
+
} catch {
|
|
209
|
+
// A reporting failure cannot replace the seam failure it describes.
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const registerLifecycle = (entry, name, scope) => scope.effect?.(() => () => {
|
|
213
|
+
if (live !== entry) return
|
|
214
|
+
if (entry.retiring !== undefined) return
|
|
215
|
+
live = undefined
|
|
216
|
+
if (!stopping && name === EXECUTION_SEAM_SERVICE
|
|
217
|
+
&& readService(ctx, EXECUTION_SEAM_SERVICE) === undefined
|
|
218
|
+
&& readService(ctx, LEGACY_EXECUTION_SEAM_SERVICE) !== undefined) {
|
|
219
|
+
startWatcher(LEGACY_EXECUTION_SEAM_SERVICE)
|
|
220
|
+
}
|
|
221
|
+
}, 'ptc-plus execution seam lifecycle')
|
|
164
222
|
const select = (name, scope) => {
|
|
165
|
-
|
|
166
|
-
if (
|
|
223
|
+
const currentService = readService(ctx, EXECUTION_SEAM_SERVICE)
|
|
224
|
+
if (unusableCurrent !== undefined && currentService !== unusableCurrent) {
|
|
225
|
+
unusableCurrent = undefined
|
|
226
|
+
}
|
|
227
|
+
if (name === EXECUTION_SEAM_SERVICE && currentService === unusableCurrent) {
|
|
228
|
+
// A current service whose attach already failed cannot supersede the live
|
|
229
|
+
// legacy attachment. A replacement service object clears the marker above.
|
|
230
|
+
return live?.attached
|
|
231
|
+
}
|
|
232
|
+
if (live !== undefined) {
|
|
233
|
+
if (name !== EXECUTION_SEAM_SERVICE || live.name === EXECUTION_SEAM_SERVICE) return live.attached
|
|
234
|
+
const previous = live
|
|
235
|
+
if (previous.retiring !== undefined) return previous.retiring
|
|
236
|
+
const disposeLegacy = previous.scope?.fiber?.dispose
|
|
237
|
+
if (typeof disposeLegacy !== 'function') {
|
|
238
|
+
throw new Error('ptc-plus: live codeRuntime attachment cannot be superseded without a disposable injected scope')
|
|
239
|
+
}
|
|
240
|
+
let resolveActivation
|
|
241
|
+
let rejectActivation
|
|
242
|
+
const activation = new Promise((resolve, reject) => {
|
|
243
|
+
resolveActivation = resolve
|
|
244
|
+
rejectActivation = reject
|
|
245
|
+
})
|
|
246
|
+
previous.retiring = activation
|
|
247
|
+
let retirement
|
|
248
|
+
try {
|
|
249
|
+
retirement = disposeLegacy.call(previous.scope.fiber)
|
|
250
|
+
} catch (error) {
|
|
251
|
+
previous.retiring = undefined
|
|
252
|
+
rejectActivation(error)
|
|
253
|
+
return activation
|
|
254
|
+
}
|
|
255
|
+
let entry
|
|
256
|
+
const operation = Promise.resolve(retirement).then(() => {
|
|
257
|
+
if (live !== previous) return undefined
|
|
258
|
+
previous.retiring = undefined
|
|
259
|
+
entry = { name, scope }
|
|
260
|
+
live = entry
|
|
261
|
+
registerLifecycle(entry, name, scope)
|
|
262
|
+
const attached = attach(scope, createExecutionSeam(scope?.[name] ?? readService(ctx, name), name))
|
|
263
|
+
entry.attached = attached
|
|
264
|
+
return attached
|
|
265
|
+
})
|
|
266
|
+
operation.then(resolveActivation, error => {
|
|
267
|
+
if (live === previous) previous.retiring = undefined
|
|
268
|
+
else if (live === entry) {
|
|
269
|
+
// The current attach failed after the legacy fiber was retired. Keep
|
|
270
|
+
// the plugin attached by re-arming the preceding generation instead
|
|
271
|
+
// of silently returning run_code to the host provider.
|
|
272
|
+
live = undefined
|
|
273
|
+
unusableCurrent = readService(ctx, EXECUTION_SEAM_SERVICE)
|
|
274
|
+
if (readService(ctx, LEGACY_EXECUTION_SEAM_SERVICE) !== undefined) {
|
|
275
|
+
try {
|
|
276
|
+
startWatcher(LEGACY_EXECUTION_SEAM_SERVICE)
|
|
277
|
+
} catch (fallbackError) {
|
|
278
|
+
report(fallbackError, LEGACY_EXECUTION_SEAM_SERVICE)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
report(error, name)
|
|
283
|
+
rejectActivation(error)
|
|
284
|
+
})
|
|
285
|
+
return activation
|
|
286
|
+
}
|
|
287
|
+
if (name === LEGACY_EXECUTION_SEAM_SERVICE
|
|
288
|
+
&& currentService !== undefined
|
|
289
|
+
&& currentService !== unusableCurrent) {
|
|
167
290
|
return undefined
|
|
168
291
|
}
|
|
169
|
-
const entry = { name }
|
|
292
|
+
const entry = { name, scope }
|
|
170
293
|
live = entry
|
|
171
294
|
try {
|
|
172
295
|
entry.attached = attach(scope, createExecutionSeam(scope?.[name] ?? readService(ctx, name), name))
|
|
@@ -174,20 +297,59 @@ export function installExecutionSeam(ctx, { services = [], attach }) {
|
|
|
174
297
|
live = undefined
|
|
175
298
|
throw error
|
|
176
299
|
}
|
|
177
|
-
|
|
178
|
-
if (live === entry) live = undefined
|
|
179
|
-
}, 'ptc-plus execution seam lifecycle')
|
|
300
|
+
registerLifecycle(entry, name, scope)
|
|
180
301
|
return entry.attached
|
|
181
302
|
}
|
|
303
|
+
startWatcher = (name, settlement) => ctx.inject([...services, name], scope => {
|
|
304
|
+
let selected
|
|
305
|
+
try {
|
|
306
|
+
selected = select(name, scope)
|
|
307
|
+
} catch (error) {
|
|
308
|
+
settlement?.reject(error)
|
|
309
|
+
report(error, name)
|
|
310
|
+
throw error
|
|
311
|
+
}
|
|
312
|
+
if (selected !== null && typeof selected === 'object' && typeof selected.then === 'function') {
|
|
313
|
+
const operation = Promise.resolve(selected)
|
|
314
|
+
operation.then(
|
|
315
|
+
value => settlement?.resolve(value),
|
|
316
|
+
error => settlement?.reject(error),
|
|
317
|
+
)
|
|
318
|
+
return operation.then(() => undefined)
|
|
319
|
+
}
|
|
320
|
+
settlement?.resolve(selected)
|
|
321
|
+
return undefined
|
|
322
|
+
})
|
|
182
323
|
const pending = SEAM_SERVICES.map(name => new Promise((resolve, reject) => {
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
resolve(select(name, scope))
|
|
186
|
-
} catch (error) {
|
|
187
|
-
reject(error)
|
|
188
|
-
throw error
|
|
189
|
-
}
|
|
190
|
-
})
|
|
324
|
+
startWatcher(name, { resolve, reject })
|
|
191
325
|
}))
|
|
192
|
-
|
|
326
|
+
ctx.effect?.(() => () => { stopping = true }, 'ptc-plus execution seam watcher lifecycle')
|
|
327
|
+
const bound = seamWaitBound(ctx, attachmentTimeoutMs)
|
|
328
|
+
return Promise.race([Promise.race(pending), bound.promise])
|
|
329
|
+
.then(() => undefined)
|
|
330
|
+
.finally(bound.cancel)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Bound the wait for a seam the deployment never registers.
|
|
335
|
+
*
|
|
336
|
+
* @param {object} ctx - the plugin context owning the timer.
|
|
337
|
+
* @param {number} timeoutMs - positive bound, or a non-positive value for none.
|
|
338
|
+
* @returns the rejection promise and the disposer that clears its timer.
|
|
339
|
+
*/
|
|
340
|
+
function seamWaitBound(ctx, timeoutMs) {
|
|
341
|
+
if (!(timeoutMs > 0)) return { promise: new Promise(() => {}), cancel: () => {} }
|
|
342
|
+
let timer
|
|
343
|
+
const promise = new Promise((_resolve, reject) => {
|
|
344
|
+
timer = setTimeout(() => reject(new Error(
|
|
345
|
+
`ptc-plus: no host execution seam (${SEAM_SERVICES.join(' or ')}) is registered; this deployment does not publish a code runtime`,
|
|
346
|
+
)), timeoutMs)
|
|
347
|
+
})
|
|
348
|
+
// The timer is not unref'd: this rejection is the only thing that settles the
|
|
349
|
+
// activation promise on a deployment that never registers a seam, and an
|
|
350
|
+
// unref'd timer would let the process drain before it fires. It is cleared as
|
|
351
|
+
// soon as the race settles so an attached seam never holds the loop open.
|
|
352
|
+
const cancel = () => clearTimeout(timer)
|
|
353
|
+
ctx.effect?.(() => () => cancel(), 'ptc-plus execution seam wait bound')
|
|
354
|
+
return { promise, cancel }
|
|
193
355
|
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { diagnostic } from './diagnostic.js'
|
|
2
|
-
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
3
2
|
import { createHash } from 'node:crypto'
|
|
4
|
-
import {
|
|
3
|
+
import { createPrivateAsyncLocalStorage } from './async-local-storage-intrinsics.js'
|
|
4
|
+
import { runtimeIntrinsics as internal } from './runtime-intrinsics.js'
|
|
5
5
|
|
|
6
|
+
const { Object, RegExp, isArray, numberIsSafeInteger, toNumber, toString,
|
|
7
|
+
splitString, startsWith, endsWith, sliceString, searchString, replaceString,
|
|
8
|
+
trimString, regexpExec, prependArray, join, bufferByteLength, bufferIsBuffer,
|
|
9
|
+
bufferSubarray, bufferToString } = internal
|
|
6
10
|
export const MAX_ERROR_LOG_BYTES = 4 * 1024
|
|
7
11
|
export const FAILURE_HINT_THRESHOLD = 3
|
|
8
12
|
export const LONG_CELL_CODE_UNITS = 2_000
|
|
9
13
|
const BINDING_FAILURE = Symbol('binding failure')
|
|
10
14
|
const PROGRAM_FAILURES = new internal.WeakMap()
|
|
11
15
|
const MAX_CAUSE_CODE_UNITS = 2048
|
|
12
|
-
const exceptionOrigins =
|
|
16
|
+
const exceptionOrigins = createPrivateAsyncLocalStorage()
|
|
13
17
|
const runExceptionScope = exceptionOrigins.run.bind(exceptionOrigins)
|
|
14
18
|
const currentExceptionScope = exceptionOrigins.getStore.bind(exceptionOrigins)
|
|
15
19
|
|
|
@@ -36,14 +40,14 @@ export function recordExceptionOrigin(error, origin, { reset = false, sourceFail
|
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
const SOURCE_LINE_BREAK = /\r\n|[\n\r\u2028\u2029]/
|
|
39
|
-
const sourceLines = source => source
|
|
43
|
+
const sourceLines = source => splitString(source, SOURCE_LINE_BREAK)
|
|
40
44
|
|
|
41
45
|
/** A cell position is a 1-based line and column inside its source. The column
|
|
42
46
|
* may address the position just after the last character of a line. */
|
|
43
47
|
function boundedPosition(position, lines) {
|
|
44
48
|
if (position === null || typeof position !== 'object'
|
|
45
|
-
|| !
|
|
46
|
-
|| !
|
|
49
|
+
|| !numberIsSafeInteger(position.line) || position.line < 1
|
|
50
|
+
|| !numberIsSafeInteger(position.column) || position.column < 1
|
|
47
51
|
|| position.line > lines.length || position.column > lines[position.line - 1].length + 1) return undefined
|
|
48
52
|
return { line: position.line, column: position.column }
|
|
49
53
|
}
|
|
@@ -55,14 +59,15 @@ export function cellPosition(position, source) {
|
|
|
55
59
|
|
|
56
60
|
/** Only a fact belonging to the current original source can bypass its map. */
|
|
57
61
|
export function exceptionOriginPosition(origins, source) {
|
|
58
|
-
if (!
|
|
62
|
+
if (!isArray(origins)) return undefined
|
|
59
63
|
const prefix = `eval:${createHash('sha256').update(source).digest('hex')}:`
|
|
60
64
|
const lines = sourceLines(source)
|
|
61
|
-
for (
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
for (let index = 0; index < origins.length; index += 1) {
|
|
66
|
+
const origin = origins[index]
|
|
67
|
+
if (typeof origin !== 'string' || !startsWith(origin, prefix)) continue
|
|
68
|
+
const match = regexpExec(/^(\d+):(\d+)$/, sliceString(origin, prefix.length))
|
|
64
69
|
if (match === null) continue
|
|
65
|
-
const position = boundedPosition({ line:
|
|
70
|
+
const position = boundedPosition({ line: toNumber(match[1]), column: toNumber(match[2]) }, lines)
|
|
66
71
|
if (position !== undefined) return position
|
|
67
72
|
}
|
|
68
73
|
return undefined
|
|
@@ -124,7 +129,7 @@ export function messageOf(error, fallback = 'Unprintable error') {
|
|
|
124
129
|
const message = safeProperty(error, 'message')
|
|
125
130
|
if (typeof message === 'string') return message
|
|
126
131
|
try {
|
|
127
|
-
return
|
|
132
|
+
return toString(error)
|
|
128
133
|
} catch {
|
|
129
134
|
return fallback
|
|
130
135
|
}
|
|
@@ -139,20 +144,21 @@ export function processExitDescription(code, signal) {
|
|
|
139
144
|
|
|
140
145
|
export function firstLine(value, fallback = undefined) {
|
|
141
146
|
if (typeof value !== 'string') return fallback
|
|
142
|
-
const line = value
|
|
147
|
+
const line = splitString(value, /[\r\n]/, 1)[0]
|
|
143
148
|
return line.length > 0 ? line : fallback
|
|
144
149
|
}
|
|
145
150
|
|
|
146
151
|
export function oneLineMessage(error) {
|
|
147
|
-
return firstLine(messageOf(error), 'Unknown error')
|
|
152
|
+
return replaceString(firstLine(messageOf(error), 'Unknown error'), /\s+\(\d+:\d+\)$/, '')
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
/** Return DSH's reported missing-description property path when it is actionable. */
|
|
151
156
|
export function missingDescriptionPath(error) {
|
|
152
|
-
const
|
|
153
|
-
|
|
157
|
+
const pattern = /missing required property ["']([^"']+)["']/g
|
|
158
|
+
const message = messageOf(error)
|
|
159
|
+
for (let match = regexpExec(pattern, message); match !== null; match = regexpExec(pattern, message)) {
|
|
154
160
|
const path = match[1]
|
|
155
|
-
if (path === 'description' ||
|
|
161
|
+
if (path === 'description' || endsWith(path, '.description')) return path
|
|
156
162
|
}
|
|
157
163
|
return undefined
|
|
158
164
|
}
|
|
@@ -167,18 +173,18 @@ export function limitLogs(logs) {
|
|
|
167
173
|
let bytes = 0
|
|
168
174
|
const tail = []
|
|
169
175
|
for (let index = logs.length - 1; index >= 0; index -= 1) {
|
|
170
|
-
const size =
|
|
176
|
+
const size = bufferByteLength(logs[index], 'utf8') + 1
|
|
171
177
|
if (bytes + size > MAX_ERROR_LOG_BYTES) break
|
|
172
178
|
bytes += size
|
|
173
|
-
tail
|
|
179
|
+
prependArray(tail, logs[index])
|
|
174
180
|
}
|
|
175
181
|
return tail
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
/** A frame is only useful when its 1-based line and column are safe integers. */
|
|
179
185
|
function structuralPosition(position) {
|
|
180
|
-
return
|
|
181
|
-
&&
|
|
186
|
+
return numberIsSafeInteger(position.line) && position.line >= 1
|
|
187
|
+
&& numberIsSafeInteger(position.column) && position.column >= 1 ? position : undefined
|
|
182
188
|
}
|
|
183
189
|
|
|
184
190
|
/** Extract the first frame for the active cell. The frame is reported in the
|
|
@@ -187,10 +193,10 @@ function structuralPosition(position) {
|
|
|
187
193
|
export function errorPosition(error, filename, source) {
|
|
188
194
|
const stack = safeProperty(error, 'stack')
|
|
189
195
|
if (typeof stack !== 'string') return undefined
|
|
190
|
-
const escaped = filename
|
|
191
|
-
const match = new RegExp(`${escaped}:(\\d+):(\\d+)`)
|
|
196
|
+
const escaped = replaceString(filename, /[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
197
|
+
const match = regexpExec(new RegExp(`${escaped}:(\\d+):(\\d+)`), stack)
|
|
192
198
|
if (match === null) return undefined
|
|
193
|
-
const position = { line:
|
|
199
|
+
const position = { line: toNumber(match[1]), column: toNumber(match[2]) }
|
|
194
200
|
return source === undefined ? structuralPosition(position) : cellPosition(position, source)
|
|
195
201
|
}
|
|
196
202
|
|
|
@@ -206,12 +212,14 @@ export function errorDetails(error, filename, source) {
|
|
|
206
212
|
const causeMessage = firstLine(safeProperty(candidate, 'message'))
|
|
207
213
|
const causeCode = firstLine(safeProperty(candidate, 'code'))
|
|
208
214
|
const stderr = safeProperty(error, 'stderr')
|
|
209
|
-
const stderrText = typeof stderr === 'string' ? stderr
|
|
210
|
-
:
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
+
const stderrText = typeof stderr === 'string' ? sliceString(stderr, 0, MAX_CAUSE_CODE_UNITS)
|
|
216
|
+
: bufferIsBuffer(stderr)
|
|
217
|
+
? bufferToString(bufferSubarray(stderr, 0, MAX_CAUSE_CODE_UNITS), 'utf8') : ''
|
|
218
|
+
const boundedMessage = sliceString(message, 0, MAX_CAUSE_CODE_UNITS)
|
|
219
|
+
const continuation = searchString(boundedMessage, /[\r\n]/)
|
|
220
|
+
const detailParts = [continuation < 0 ? '' : sliceString(boundedMessage, continuation + 1), stderrText]
|
|
221
|
+
const detail = trimString(replaceString(
|
|
222
|
+
sliceString(join(detailParts, ' '), 0, MAX_CAUSE_CODE_UNITS), /\s+/g, ' '))
|
|
215
223
|
const cause = causeMessage === undefined
|
|
216
224
|
? detail.length === 0 ? undefined : { message: detail }
|
|
217
225
|
: {
|
|
@@ -3,6 +3,7 @@ import { EventEmitter } from 'node:events'
|
|
|
3
3
|
import { resolve } from 'node:path'
|
|
4
4
|
import { PassThrough } from 'node:stream'
|
|
5
5
|
import { pathToFileURL } from 'node:url'
|
|
6
|
+
import { helperProcessEnvironment, isElectronHost } from './worker-environment.js'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Fallback bound for a helper that answers the stop request and then keeps the
|
|
@@ -43,6 +44,7 @@ export class IsolatedWorker extends EventEmitter {
|
|
|
43
44
|
this.stderr = new PassThrough()
|
|
44
45
|
this.protocol = protocol
|
|
45
46
|
this.exited = false
|
|
47
|
+
this.exitedPromise = new Promise(resolve => { this.resolveExited = resolve })
|
|
46
48
|
this.exitCode = undefined
|
|
47
49
|
this.exitSignal = undefined
|
|
48
50
|
this.released = false
|
|
@@ -63,12 +65,15 @@ export class IsolatedWorker extends EventEmitter {
|
|
|
63
65
|
}
|
|
64
66
|
// child_process.fork mutates the env object it receives by copying
|
|
65
67
|
// NODE_V8_COVERAGE into it. Snapshot the caller's projection first so the
|
|
66
|
-
// inner Worker receives exactly the projected user environment
|
|
67
|
-
// helper
|
|
68
|
-
const
|
|
68
|
+
// inner Worker receives exactly the projected user environment. An Electron
|
|
69
|
+
// Host also needs a helper-only startup override; it must not reach user code.
|
|
70
|
+
const electronHost = isElectronHost()
|
|
71
|
+
const projectedEnvironment = env === undefined && electronHost ? process.env : env
|
|
72
|
+
const workerEnvironment = projectedEnvironment === undefined ? undefined : { ...projectedEnvironment }
|
|
73
|
+
const forkEnvironment = helperProcessEnvironment(projectedEnvironment, process.platform, electronHost)
|
|
69
74
|
this.child = fork(helper, [], {
|
|
70
75
|
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
71
|
-
env,
|
|
76
|
+
env: forkEnvironment,
|
|
72
77
|
serialization: 'advanced',
|
|
73
78
|
// The helper starts with the host's own startup arguments removed, matching
|
|
74
79
|
// the isolation the in-host worker had with execArgv: [].
|
|
@@ -80,6 +85,7 @@ export class IsolatedWorker extends EventEmitter {
|
|
|
80
85
|
this.exited = true
|
|
81
86
|
this.exitCode = code
|
|
82
87
|
this.exitSignal = signal
|
|
88
|
+
this.resolveExited()
|
|
83
89
|
this.#rejectUtilizationRequests(new Error('kernel helper exited before answering utilization sampling'))
|
|
84
90
|
})
|
|
85
91
|
// A ChildProcess 'close' follows 'exit' once its stdio has really ended, which
|
|
@@ -264,7 +270,7 @@ export class IsolatedWorker extends EventEmitter {
|
|
|
264
270
|
const started = Date.now()
|
|
265
271
|
const killAt = started + ISOLATED_RELEASE_MS
|
|
266
272
|
const confirmBy = started + ISOLATED_STOP_MS
|
|
267
|
-
const exited =
|
|
273
|
+
const exited = this.exitedPromise
|
|
268
274
|
const onReleased = message => {
|
|
269
275
|
if (message?.type === 'worker-message' && message.value?.type === 'shutdown-released') this.released = true
|
|
270
276
|
}
|
|
@@ -278,18 +284,19 @@ export class IsolatedWorker extends EventEmitter {
|
|
|
278
284
|
// The channel is closed; the kill path below owns the instance.
|
|
279
285
|
}
|
|
280
286
|
if (!await leftWithin(this, remaining(killAt))) {
|
|
287
|
+
let killFailure
|
|
281
288
|
if (!this.exited) {
|
|
282
289
|
try {
|
|
283
290
|
await this.kill()
|
|
284
291
|
} catch (error) {
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
this
|
|
288
|
-
|
|
292
|
+
// On Windows a process that has already left can report a refused
|
|
293
|
+
// kill before Node delivers its exit event. The real exit observation
|
|
294
|
+
// decides whether this instance was reclaimed.
|
|
295
|
+
killFailure = error
|
|
289
296
|
}
|
|
290
297
|
}
|
|
291
298
|
if (!this.exited && !await leftWithin(this, remaining(confirmBy))) {
|
|
292
|
-
const failure = new Error('kernel helper did not exit before the close deadline')
|
|
299
|
+
const failure = killFailure ?? new Error('kernel helper did not exit before the close deadline')
|
|
293
300
|
this.reclamationFailure = failure
|
|
294
301
|
throw failure
|
|
295
302
|
}
|