dsh-ptc-plus 0.3.2 → 0.4.2

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 (206) hide show
  1. package/README.en.md +63 -80
  2. package/README.md +63 -80
  3. package/assets/ptc-plus-bindings-en.png +0 -0
  4. package/assets/ptc-plus-bindings-zh.png +0 -0
  5. package/assets/ptc-plus-repair-en.png +0 -0
  6. package/assets/ptc-plus-repair-zh.png +0 -0
  7. package/assets/ptc-plus-repl-workspace-en.png +0 -0
  8. package/assets/ptc-plus-repl-workspace-zh.png +0 -0
  9. package/assets/ptc-plus-settings-en.png +0 -0
  10. package/assets/ptc-plus-settings-zh.png +0 -0
  11. package/client.js +30827 -742
  12. package/compiler-amaro.cjs +5 -0
  13. package/compiler-amaro.cjs.map +6 -0
  14. package/compiler-asset-124f80c1bba92ff5fe79cfec6e8ad518f476de4a42514cae3919538887f510e8.txt +1 -0
  15. package/compiler-assets.json +3 -0
  16. package/compiler-core.cjs +556 -0
  17. package/compiler-core.cjs.map +6 -0
  18. package/compiler-platform.cjs +2 -0
  19. package/compiler-platform.cjs.map +6 -0
  20. package/compiler-typescript.cjs +388 -0
  21. package/compiler-typescript.cjs.map +6 -0
  22. package/docs/README.md +7 -0
  23. package/docs/adr/0004-declare-program-bindings-at-the-host.md +1 -1
  24. package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +5 -3
  25. package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +1 -1
  26. package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -0
  27. package/docs/adr/0010-session-log-derived-recovery-tips.md +59 -5
  28. package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +1 -1
  29. package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +3 -1
  30. package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +3 -1
  31. package/docs/adr/0014-persist-cell-rewrite-policy.md +6 -2
  32. package/docs/adr/0015-preserve-source-positions-through-rewrites.md +1 -1
  33. package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
  34. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +49 -0
  35. package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +3 -3
  36. package/docs/adr/0019-plugin-settings-and-kill-switch.md +5 -3
  37. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  38. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +12 -0
  39. package/docs/adr/0023-global-user-bindings.md +104 -0
  40. package/docs/adr/0024-repl-console-observation.md +33 -0
  41. package/docs/adr/0025-use-versioned-logical-binding-identities.md +119 -0
  42. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +21 -0
  43. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +37 -0
  44. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +103 -0
  45. package/docs/adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md +27 -0
  46. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +27 -0
  47. package/docs/architecture.md +77 -21
  48. package/docs/binding-review-placement-design.md +200 -0
  49. package/docs/capability-projection.md +1 -1
  50. package/docs/client-ui.md +95 -19
  51. package/docs/durability-design.md +28 -10
  52. package/docs/evaluation.md +78 -6
  53. package/docs/installation.md +41 -6
  54. package/docs/publishing.md +5 -3
  55. package/docs/runtime-reference.md +76 -25
  56. package/docs/semantic-validation.md +29 -0
  57. package/docs/stateful-binding-updates-proposal.md +759 -0
  58. package/docs/user-bindings.en.md +66 -0
  59. package/docs/user-bindings.md +66 -0
  60. package/docs/value-wire.md +3 -3
  61. package/docs/verification-optimization-work.md +41 -0
  62. package/docs/verification.md +71 -0
  63. package/index.js +369 -92
  64. package/internal/ast-traversal.js +5 -2
  65. package/internal/async-local-storage-intrinsics.js +38 -0
  66. package/internal/binding-declaration-contract.js +8 -0
  67. package/internal/binding-descriptors.js +1 -0
  68. package/internal/binding-pattern-traversal.js +25 -0
  69. package/internal/binding-pattern.js +26 -29
  70. package/internal/binding-source-analysis.js +708 -0
  71. package/internal/binding-update-policy.js +72 -0
  72. package/internal/bounded-worker-round.js +56 -0
  73. package/internal/callable-definition-environment.js +42 -0
  74. package/internal/callable-source-catalog.js +129 -0
  75. package/internal/callable-source-encoding.js +47 -0
  76. package/internal/callable-source-facts.js +459 -0
  77. package/internal/callable-source-format.js +2 -0
  78. package/internal/callable-source-lowering.js +123 -0
  79. package/internal/cell-analysis-contract.js +25 -0
  80. package/internal/cell-analysis.js +253 -290
  81. package/internal/cell-completion.js +15 -0
  82. package/internal/cell-error.js +7 -0
  83. package/internal/cell-parser.js +66 -0
  84. package/internal/cell-rewriter.js +3 -677
  85. package/internal/commonjs-export-evidence.js +63 -0
  86. package/internal/commonjs-source-evidence.js +72 -0
  87. package/internal/compiler-amaro.cjs +2 -0
  88. package/internal/compiler-compression.js +12 -0
  89. package/internal/compiler-data.js +58 -0
  90. package/internal/compiler-descriptors.js +54 -0
  91. package/internal/compiler-dynamic-cache.js +48 -0
  92. package/internal/compiler-entry.js +35 -0
  93. package/internal/compiler-generated-names.js +17 -0
  94. package/internal/compiler-intrinsics.js +50 -0
  95. package/internal/compiler-module-links.js +37 -0
  96. package/internal/compiler-operations.js +61 -0
  97. package/internal/compiler-platform-factory.js +84 -0
  98. package/internal/compiler-platform.js +136 -0
  99. package/internal/compiler-record-roles.js +53 -0
  100. package/internal/compiler-region-output.js +259 -0
  101. package/internal/compiler-resource-lowering.js +103 -0
  102. package/internal/compiler-scope-facts.js +115 -0
  103. package/internal/compiler-service.js +109 -0
  104. package/internal/compiler-source-cache.js +32 -0
  105. package/internal/compiler-source-pieces.js +93 -0
  106. package/internal/compiler-source-regions.js +132 -0
  107. package/internal/compiler-storage-source.js +54 -0
  108. package/internal/compiler-text.js +15 -0
  109. package/internal/compiler-typescript.cjs +2 -0
  110. package/internal/config-spec.js +83 -27
  111. package/internal/cordis-tools-owner.js +130 -21
  112. package/internal/diagnostic.js +27 -1
  113. package/internal/direct-surface-owner.js +76 -23
  114. package/internal/dynamic-binding-evidence.js +16 -0
  115. package/internal/dynamic-environment-compiler.js +779 -0
  116. package/internal/dynamic-environment-integration.js +91 -0
  117. package/internal/dynamic-environment-runtime.js +383 -0
  118. package/internal/dynamic-native-calls.js +153 -0
  119. package/internal/dynamic-native-runtime.js +262 -0
  120. package/internal/dynamic-scope-analysis.js +90 -0
  121. package/internal/edit-transport-owner.js +27 -2
  122. package/internal/execution-seam-compat.js +193 -0
  123. package/internal/failure-reporting.js +136 -28
  124. package/internal/host-rpc.js +66 -0
  125. package/internal/isolated-worker.js +555 -0
  126. package/internal/kernel-child.js +139 -0
  127. package/internal/kernel-worker.js +1179 -233
  128. package/internal/language-semantics.js +14 -0
  129. package/internal/legacy-binding-catalog.js +48 -0
  130. package/internal/legacy-cell-analysis.js +600 -0
  131. package/internal/legacy-cell-parser.js +50 -0
  132. package/internal/legacy-cell-rewriter.js +631 -0
  133. package/internal/legacy-repl-convenience.js +291 -0
  134. package/internal/managed-module-operations.js +170 -0
  135. package/internal/module-compilation.js +322 -0
  136. package/internal/module-runtime-platform.js +19 -0
  137. package/internal/module-transform-contract.js +42 -0
  138. package/internal/native-await-declarations.js +68 -0
  139. package/internal/native-root-compilation.js +54 -0
  140. package/internal/native-root-dynamic.js +67 -0
  141. package/internal/record-utils.js +24 -15
  142. package/internal/recovery-tips.js +27 -15
  143. package/internal/rejected-cell-editor.js +13 -8
  144. package/internal/repl-convenience.js +2 -289
  145. package/internal/repl-memory-projection.js +81 -12
  146. package/internal/repl-observation-interest.js +70 -0
  147. package/internal/repl-rewrite-contract.js +6 -0
  148. package/internal/repl-scope-normalizer.js +2091 -0
  149. package/internal/repl-value-observer.js +117 -0
  150. package/internal/rpc-contract.js +52 -0
  151. package/internal/runtime-bridge-owner.js +172 -28
  152. package/internal/runtime-config.js +48 -52
  153. package/internal/runtime-contexts.js +82 -10
  154. package/internal/runtime-intrinsics.js +101 -0
  155. package/internal/runtime-messages.js +111 -0
  156. package/internal/session-cell-executor.js +402 -87
  157. package/internal/session-journal-recovery.js +684 -0
  158. package/internal/session-journal-schema.js +123 -0
  159. package/internal/session-journal.js +199 -654
  160. package/internal/session-log-view.js +42 -9
  161. package/internal/session-runtime.js +257 -95
  162. package/internal/session-state.js +247 -71
  163. package/internal/source-position-map.js +230 -71
  164. package/internal/stateful-module-compiler.js +154 -0
  165. package/internal/stateful-module-runtime.js +303 -0
  166. package/internal/stateful-root-compiler.js +600 -0
  167. package/internal/stateful-root-runtime.js +340 -0
  168. package/internal/typert-codec-compat.js +28 -0
  169. package/internal/typescript-transform.js +31 -0
  170. package/internal/user-binding-authoring.js +70 -0
  171. package/internal/user-binding-console-worker.js +244 -0
  172. package/internal/user-binding-console.js +148 -0
  173. package/internal/user-binding-draft-projection.js +335 -0
  174. package/internal/user-binding-model-context.js +31 -0
  175. package/internal/user-binding-runner.js +56 -0
  176. package/internal/user-bindings-owner.js +1065 -0
  177. package/internal/user-bindings-store.js +293 -0
  178. package/internal/user-bindings.js +361 -0
  179. package/internal/value-wire-schema.js +28 -0
  180. package/internal/value-wire.js +130 -91
  181. package/internal/worker-client.js +57 -67
  182. package/internal/worker-cwd-virtualization.js +80 -53
  183. package/internal/worker-environment.js +65 -0
  184. package/internal/worker-output-relay.js +29 -0
  185. package/internal/worker-repl-realm.js +134 -0
  186. package/internal/worker-shutdown.js +14 -0
  187. package/package.json +76 -7
  188. package/src/client-activity.js +95 -53
  189. package/src/client-authoring-view.js +580 -0
  190. package/src/client-binding-review.js +230 -0
  191. package/src/client-bindings-data.js +29 -0
  192. package/src/client-catalog.js +127 -0
  193. package/src/client-code-editor.js +84 -0
  194. package/src/client-console.js +112 -0
  195. package/src/client-copy.js +370 -0
  196. package/src/client-feature-gates.js +62 -0
  197. package/src/client-host-compat.js +150 -0
  198. package/src/client-indicator-view.js +267 -0
  199. package/src/client-repl-view.js +292 -0
  200. package/src/client-rpc.js +20 -0
  201. package/src/client-settings-view.js +221 -0
  202. package/src/client-styles.js +106 -0
  203. package/src/client-tool-view.js +108 -0
  204. package/src/client-transport.js +159 -0
  205. package/src/client-workbench.js +521 -0
  206. package/src/client.js +233 -615
@@ -1,28 +1,36 @@
1
1
  import { assertFields, isRecord } from './record-utils.js'
2
+ import { runtimeIntrinsics as internal } from './runtime-intrinsics.js'
3
+ import {
4
+ VALUE_CODEC,
5
+ DEFAULT_VALUE_LIMITS,
6
+ VALUE_ENVELOPE_FIELDS as ENVELOPE_FIELDS,
7
+ VALUE_OBJECT_FIELDS as OBJECT_NODE_FIELDS,
8
+ VALUE_ARRAY_FIELDS as ARRAY_NODE_FIELDS,
9
+ VALUE_UNDEFINED_FIELDS as UNDEFINED_FIELDS,
10
+ VALUE_NUMBER_FIELDS as NUMBER_FIELDS,
11
+ VALUE_BIGINT_FIELDS as BIGINT_FIELDS,
12
+ VALUE_REFERENCE_FIELDS as REFERENCE_FIELDS,
13
+ } from './value-wire-schema.js'
2
14
 
3
- export const VALUE_CODEC = 'ptc-value-graph/v1'
4
-
5
- export const DEFAULT_VALUE_LIMITS = Object.freeze({
6
- maxNodes: 100_000,
7
- maxEdges: 1_000_000,
8
- maxArrayLength: 1_000_000,
9
- maxBigIntDigits: 100_000,
10
- maxStringBytes: 64 * 1024 * 1024,
11
- })
12
-
13
- const ENVELOPE_FIELDS = new Set(['codec', 'root', 'nodes'])
14
- const OBJECT_NODE_FIELDS = new Set(['type', 'prototype', 'entries'])
15
- const ARRAY_NODE_FIELDS = new Set(['type', 'length', 'entries'])
16
- const UNDEFINED_FIELDS = new Set(['tag'])
17
- const NUMBER_FIELDS = new Set(['tag', 'value'])
18
- const BIGINT_FIELDS = new Set(['tag', 'value'])
19
- const REFERENCE_FIELDS = new Set(['tag', 'index'])
15
+ export { VALUE_CODEC, DEFAULT_VALUE_LIMITS } from './value-wire-schema.js'
16
+ const { Object, Reflect, Array, Map, Set, WeakMap, WeakSet, TypeError,
17
+ isArray, numberIsFinite, numberIsNaN, numberIsSafeInteger, toBigInt,
18
+ toNumber, toString, regexpTest, mapGet, mapHas, mapSet, setHas, setAdd,
19
+ setDelete, setSize, weakMapGet, weakMapSet, weakSetHas, weakSetAdd,
20
+ appendArray, popArray, join } = internal
20
21
  const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/
22
+ const intrinsicFunctionToString = Function.prototype.toString
23
+ const reflectApply = Reflect.apply
24
+ const bufferByteLength = internal.bufferByteLength
25
+ const jsonStringify = JSON.stringify
21
26
 
22
27
  function limitsOf(options = {}) {
23
28
  const limits = { ...DEFAULT_VALUE_LIMITS, ...options }
24
- for (const [name, value] of Object.entries(limits)) {
25
- if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`invalid PTC value limit ${name}`)
29
+ const entries = Object.entries(limits)
30
+ for (let index = 0; index < entries.length; index += 1) {
31
+ const name = entries[index][0]
32
+ const value = entries[index][1]
33
+ if (!numberIsSafeInteger(value) || value < 1) throw new TypeError(`invalid PTC value limit ${name}`)
26
34
  }
27
35
  return limits
28
36
  }
@@ -33,7 +41,7 @@ function hasIntrinsicConstructor(prototype, name) {
33
41
  try {
34
42
  return constructor.name === name
35
43
  && constructor.prototype === prototype
36
- && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`
44
+ && reflectApply(intrinsicFunctionToString, constructor, []) === `function ${name}() { [native code] }`
37
45
  } catch {
38
46
  return false
39
47
  }
@@ -52,7 +60,7 @@ function plainObjectPrototype(value) {
52
60
 
53
61
  function hasPlainArrayPrototype(value) {
54
62
  const prototype = Object.getPrototypeOf(value)
55
- if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array')) return false
63
+ if (!isArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array')) return false
56
64
  const objectPrototype = Object.getPrototypeOf(prototype)
57
65
  return objectPrototype !== null && typeof objectPrototype === 'object'
58
66
  && isIntrinsicObjectPrototype(objectPrototype)
@@ -63,7 +71,7 @@ function invalid(path, detail) {
63
71
  }
64
72
 
65
73
  function stringBytes(value) {
66
- return Buffer.byteLength(value, 'utf8')
74
+ return bufferByteLength(value, 'utf8')
67
75
  }
68
76
 
69
77
  /** Encode one supported JavaScript graph into the project-owned JSON-safe wire. */
@@ -87,7 +95,7 @@ export function encodeValue(value, options = {}) {
87
95
  return current
88
96
  }
89
97
  if (typeof current === 'number') {
90
- if (Number.isNaN(current)) return { tag: 'number', value: 'nan' }
98
+ if (numberIsNaN(current)) return { tag: 'number', value: 'nan' }
91
99
  if (current === Infinity) return { tag: 'number', value: 'infinity' }
92
100
  if (current === -Infinity) return { tag: 'number', value: '-infinity' }
93
101
  if (Object.is(current, -0)) return { tag: 'number', value: '-0' }
@@ -95,20 +103,20 @@ export function encodeValue(value, options = {}) {
95
103
  }
96
104
  if (current === undefined) return { tag: 'undefined' }
97
105
  if (typeof current === 'bigint') {
98
- const text = String(current)
106
+ const text = toString(current)
99
107
  const digits = text[0] === '-' ? text.length - 1 : text.length
100
108
  if (digits > limits.maxBigIntDigits) invalid(path, `BigInt exceeds ${limits.maxBigIntDigits} digits`)
101
109
  accountText(text, path)
102
110
  return { tag: 'bigint', value: text }
103
111
  }
104
112
  if (typeof current !== 'object') invalid(path, typeof current)
105
- const prior = seen.get(current)
113
+ const prior = weakMapGet(seen, current)
106
114
  if (prior !== undefined) return { tag: 'reference', index: prior }
107
115
  if (nodes.length >= limits.maxNodes) invalid(path, `node budget exceeds ${limits.maxNodes}`)
108
116
  const index = nodes.length
109
- seen.set(current, index)
110
- nodes.push(undefined)
111
- pending.push({ value: current, index, path })
117
+ weakMapSet(seen, current, index)
118
+ appendArray(nodes, undefined)
119
+ appendArray(pending, { value: current, index, path })
112
120
  return { tag: 'reference', index }
113
121
  }
114
122
 
@@ -116,24 +124,26 @@ export function encodeValue(value, options = {}) {
116
124
  for (let cursor = 0; cursor < pending.length; cursor += 1) {
117
125
  const item = pending[cursor]
118
126
  const current = item.value
119
- if (Array.isArray(current)) {
127
+ if (isArray(current)) {
120
128
  if (!hasPlainArrayPrototype(current)) invalid(item.path, 'non-plain array')
121
129
  if (current.length > limits.maxArrayLength) invalid(item.path, `array length exceeds ${limits.maxArrayLength}`)
122
130
  const entries = []
123
- for (const key of Reflect.ownKeys(current)) {
131
+ const keys = Reflect.ownKeys(current)
132
+ for (let keyIndex = 0; keyIndex < keys.length; keyIndex += 1) {
133
+ const key = keys[keyIndex]
124
134
  if (key === 'length') continue
125
- if (typeof key !== 'string' || !/^(0|[1-9][0-9]*)$/.test(key)) {
135
+ if (typeof key !== 'string' || !regexpTest(/^(0|[1-9][0-9]*)$/, key)) {
126
136
  invalid(item.path, 'array has a symbol or non-index property')
127
137
  }
128
- const index = Number(key)
129
- if (!Number.isSafeInteger(index) || index >= current.length) invalid(`${item.path}[${key}]`, 'invalid array index')
138
+ const index = toNumber(key)
139
+ if (!numberIsSafeInteger(index) || index >= current.length) invalid(`${item.path}[${key}]`, 'invalid array index')
130
140
  const descriptor = Object.getOwnPropertyDescriptor(current, key)
131
141
  if (descriptor === undefined || !Object.hasOwn(descriptor, 'value') || descriptor.enumerable !== true) {
132
142
  invalid(`${item.path}[${key}]`, 'array index must be an enumerable data property')
133
143
  }
134
144
  edgeCount += 1
135
145
  if (edgeCount > limits.maxEdges) invalid(item.path, `edge budget exceeds ${limits.maxEdges}`)
136
- entries.push([index, atom(descriptor.value, `${item.path}[${key}]`)])
146
+ appendArray(entries, [index, atom(descriptor.value, `${item.path}[${key}]`)])
137
147
  }
138
148
  nodes[item.index] = { type: 'array', length: current.length, entries }
139
149
  continue
@@ -142,7 +152,9 @@ export function encodeValue(value, options = {}) {
142
152
  const prototype = plainObjectPrototype(current)
143
153
  if (prototype === undefined) invalid(item.path, 'non-plain object')
144
154
  const entries = []
145
- for (const key of Reflect.ownKeys(current)) {
155
+ const keys = Reflect.ownKeys(current)
156
+ for (let keyIndex = 0; keyIndex < keys.length; keyIndex += 1) {
157
+ const key = keys[keyIndex]
146
158
  if (typeof key !== 'string') invalid(item.path, 'object has a symbol key')
147
159
  const descriptor = Object.getOwnPropertyDescriptor(current, key)
148
160
  if (descriptor === undefined || !Object.hasOwn(descriptor, 'value') || descriptor.enumerable !== true) {
@@ -151,7 +163,7 @@ export function encodeValue(value, options = {}) {
151
163
  accountText(key, `${item.path}.${key}`)
152
164
  edgeCount += 1
153
165
  if (edgeCount > limits.maxEdges) invalid(item.path, `edge budget exceeds ${limits.maxEdges}`)
154
- entries.push([key, atom(descriptor.value, `${item.path}.${key}`)])
166
+ appendArray(entries, [key, atom(descriptor.value, `${item.path}.${key}`)])
155
167
  }
156
168
  nodes[item.index] = { type: 'object', prototype, entries }
157
169
  }
@@ -166,7 +178,7 @@ function decodeAtom(atom, nodes, limits, budget, path) {
166
178
  return atom
167
179
  }
168
180
  if (typeof atom === 'number') {
169
- if (!Number.isFinite(atom) || Object.is(atom, -0)) throw new TypeError(`invalid PTC value number at ${path}`)
181
+ if (!numberIsFinite(atom) || Object.is(atom, -0)) throw new TypeError(`invalid PTC value number at ${path}`)
170
182
  return atom
171
183
  }
172
184
  if (!isRecord(atom) || typeof atom.tag !== 'string') throw new TypeError(`invalid PTC value atom at ${path}`)
@@ -184,31 +196,31 @@ function decodeAtom(atom, nodes, limits, budget, path) {
184
196
  }
185
197
  if (atom.tag === 'bigint') {
186
198
  assertFields(atom, BIGINT_FIELDS, 'PTC BigInt atom')
187
- if (typeof atom.value !== 'string' || !/^(0|-[1-9][0-9]*|[1-9][0-9]*)$/.test(atom.value)) {
199
+ if (typeof atom.value !== 'string' || !regexpTest(/^(0|-[1-9][0-9]*|[1-9][0-9]*)$/, atom.value)) {
188
200
  throw new TypeError('invalid PTC BigInt')
189
201
  }
190
202
  const digits = atom.value[0] === '-' ? atom.value.length - 1 : atom.value.length
191
203
  if (digits > limits.maxBigIntDigits) throw new TypeError('PTC BigInt digit budget exceeded')
192
204
  budget.textBytes += stringBytes(atom.value)
193
205
  if (budget.textBytes > limits.maxStringBytes) throw new TypeError('PTC value string budget exceeded')
194
- return BigInt(atom.value)
206
+ return toBigInt(atom.value)
195
207
  }
196
208
  if (atom.tag === 'reference') {
197
209
  assertFields(atom, REFERENCE_FIELDS, 'PTC reference atom')
198
- if (!Number.isSafeInteger(atom.index) || atom.index < 0 || atom.index >= nodes.length) {
210
+ if (!numberIsSafeInteger(atom.index) || atom.index < 0 || atom.index >= nodes.length) {
199
211
  throw new TypeError('dangling PTC value reference')
200
212
  }
201
- budget.reachable.add(atom.index)
213
+ setAdd(budget.reachable, atom.index)
202
214
  return nodes[atom.index]
203
215
  }
204
- throw new TypeError(`unknown PTC value atom tag ${JSON.stringify(atom.tag)}`)
216
+ throw new TypeError(`unknown PTC value atom tag ${jsonStringify(atom.tag)}`)
205
217
  }
206
218
 
207
219
  /** Validate, hydrate, and own one canonical PTC value envelope without recursive stack growth. */
208
220
  function decodeCanonicalValue(wire, options = {}) {
209
221
  const limits = limitsOf(options)
210
222
  assertFields(wire, ENVELOPE_FIELDS, 'PTC value envelope')
211
- if (wire.codec !== VALUE_CODEC || !Array.isArray(wire.nodes)) throw new TypeError('invalid PTC value codec')
223
+ if (wire.codec !== VALUE_CODEC || !isArray(wire.nodes)) throw new TypeError('invalid PTC value codec')
212
224
  if (wire.nodes.length > limits.maxNodes) throw new TypeError('PTC value node budget exceeded')
213
225
  const targets = new Array(wire.nodes.length)
214
226
  for (let index = 0; index < wire.nodes.length; index += 1) {
@@ -216,14 +228,14 @@ function decodeCanonicalValue(wire, options = {}) {
216
228
  if (!isRecord(node)) throw new TypeError(`invalid PTC value node ${index}`)
217
229
  if (node.type === 'array') {
218
230
  assertFields(node, ARRAY_NODE_FIELDS, `PTC array node ${index}`)
219
- if (!Number.isSafeInteger(node.length) || node.length < 0 || node.length > limits.maxArrayLength) {
231
+ if (!numberIsSafeInteger(node.length) || node.length < 0 || node.length > limits.maxArrayLength) {
220
232
  throw new TypeError(`invalid PTC array length at node ${index}`)
221
233
  }
222
- if (!Array.isArray(node.entries)) throw new TypeError(`invalid PTC array entries at node ${index}`)
234
+ if (!isArray(node.entries)) throw new TypeError(`invalid PTC array entries at node ${index}`)
223
235
  targets[index] = new Array(node.length)
224
236
  } else if (node.type === 'object') {
225
237
  assertFields(node, OBJECT_NODE_FIELDS, `PTC object node ${index}`)
226
- if (!['object', 'null'].includes(node.prototype) || !Array.isArray(node.entries)) {
238
+ if ((node.prototype !== 'object' && node.prototype !== 'null') || !isArray(node.entries)) {
227
239
  throw new TypeError(`invalid PTC object node ${index}`)
228
240
  }
229
241
  targets[index] = node.prototype === 'null' ? Object.create(null) : {}
@@ -239,8 +251,9 @@ function decodeCanonicalValue(wire, options = {}) {
239
251
  const target = targets[nodeIndex]
240
252
  if (node.type === 'array') {
241
253
  let previous = -1
242
- for (const entry of node.entries) {
243
- if (!Array.isArray(entry) || entry.length !== 2 || !Number.isSafeInteger(entry[0])
254
+ for (let entryIndex = 0; entryIndex < node.entries.length; entryIndex += 1) {
255
+ const entry = node.entries[entryIndex]
256
+ if (!isArray(entry) || entry.length !== 2 || !numberIsSafeInteger(entry[0])
244
257
  || entry[0] <= previous || entry[0] < 0 || entry[0] >= node.length) {
245
258
  throw new TypeError(`invalid PTC array entry at node ${nodeIndex}`)
246
259
  }
@@ -255,11 +268,12 @@ function decodeCanonicalValue(wire, options = {}) {
255
268
  continue
256
269
  }
257
270
  const keys = new Set()
258
- for (const entry of node.entries) {
259
- if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== 'string' || keys.has(entry[0])) {
271
+ for (let entryIndex = 0; entryIndex < node.entries.length; entryIndex += 1) {
272
+ const entry = node.entries[entryIndex]
273
+ if (!isArray(entry) || entry.length !== 2 || typeof entry[0] !== 'string' || setHas(keys, entry[0])) {
260
274
  throw new TypeError(`invalid PTC object entry at node ${nodeIndex}`)
261
275
  }
262
- keys.add(entry[0])
276
+ setAdd(keys, entry[0])
263
277
  budget.textBytes += stringBytes(entry[0])
264
278
  if (budget.textBytes > limits.maxStringBytes) throw new TypeError('PTC value string budget exceeded')
265
279
  budget.edges += 1
@@ -270,10 +284,10 @@ function decodeCanonicalValue(wire, options = {}) {
270
284
  })
271
285
  }
272
286
  }
273
- if (budget.reachable.size !== wire.nodes.length) throw new TypeError('PTC value envelope contains unreachable nodes')
287
+ if (setSize(budget.reachable) !== wire.nodes.length) throw new TypeError('PTC value envelope contains unreachable nodes')
274
288
  const canonical = encodeValue(root, limits)
275
- const serialization = JSON.stringify(canonical)
276
- if (serialization !== JSON.stringify(wire)) throw new TypeError('non-canonical PTC value envelope')
289
+ const serialization = jsonStringify(canonical)
290
+ if (serialization !== jsonStringify(wire)) throw new TypeError('non-canonical PTC value envelope')
277
291
  return { value: root, wire: canonical, serialization }
278
292
  }
279
293
 
@@ -300,31 +314,32 @@ export function isPlainJsonTree(value) {
300
314
  const pending = [value]
301
315
  const seen = new WeakSet()
302
316
  while (pending.length > 0) {
303
- const current = pending.pop()
317
+ const current = popArray(pending)
304
318
  if (current === null || typeof current === 'string' || typeof current === 'boolean') continue
305
- if (typeof current === 'number' && Number.isFinite(current) && !Object.is(current, -0)) continue
319
+ if (typeof current === 'number' && numberIsFinite(current) && !Object.is(current, -0)) continue
306
320
  if (typeof current !== 'object') return false
307
- if (seen.has(current)) return false
308
- seen.add(current)
309
- if (Array.isArray(current)) {
321
+ if (weakSetHas(seen, current)) return false
322
+ weakSetAdd(seen, current)
323
+ if (isArray(current)) {
310
324
  for (let index = 0; index < current.length; index += 1) {
311
325
  if (!Object.hasOwn(current, index)) return false
312
- pending.push(current[index])
326
+ appendArray(pending, current[index])
313
327
  }
314
328
  } else {
315
- for (const key of Object.keys(current)) pending.push(current[key])
329
+ const keys = Object.keys(current)
330
+ for (let index = 0; index < keys.length; index += 1) appendArray(pending, current[keys[index]])
316
331
  }
317
332
  }
318
333
  return true
319
334
  }
320
335
 
321
336
  function renderKey(key) {
322
- return IDENTIFIER.test(key) ? key : JSON.stringify(key)
337
+ return regexpTest(IDENTIFIER, key) ? key : jsonStringify(key)
323
338
  }
324
339
 
325
340
  function atomText(atom) {
326
- if (atom === null || typeof atom === 'boolean' || typeof atom === 'number') return String(atom)
327
- if (typeof atom === 'string') return JSON.stringify(atom)
341
+ if (atom === null || typeof atom === 'boolean' || typeof atom === 'number') return toString(atom)
342
+ if (typeof atom === 'string') return jsonStringify(atom)
328
343
  if (atom.tag === 'undefined') return 'undefined'
329
344
  if (atom.tag === 'number') {
330
345
  return atom.value === 'nan' ? 'NaN'
@@ -338,17 +353,24 @@ function atomText(atom) {
338
353
  /** Deterministic bounded TS-like presentation. It is never a decode format. */
339
354
  export function renderValueWire(wire, options = {}) {
340
355
  const limits = limitsOf(options)
341
- const normalized = normalizeValueWire(wire, limits)
342
- const references = new Array(normalized.nodes.length).fill(0)
356
+ return renderCanonicalValue(decodeCanonicalValue(wire, limits).wire, limits)
357
+ }
358
+
359
+ function renderCanonicalValue(normalized, limits) {
360
+ const references = new Array(normalized.nodes.length)
361
+ for (let index = 0; index < references.length; index += 1) references[index] = 0
343
362
  const count = (atom) => {
344
363
  if (isRecord(atom) && atom.tag === 'reference') references[atom.index] += 1
345
364
  }
346
365
  count(normalized.root)
347
- for (const node of normalized.nodes) for (const entry of node.entries) count(entry[1])
366
+ for (let nodeIndex = 0; nodeIndex < normalized.nodes.length; nodeIndex += 1) {
367
+ const entries = normalized.nodes[nodeIndex].entries
368
+ for (let entryIndex = 0; entryIndex < entries.length; entryIndex += 1) count(entries[entryIndex][1])
369
+ }
348
370
  const labels = new Map()
349
371
  let nextLabel = 0
350
372
  for (let index = 0; index < references.length; index += 1) {
351
- if (references[index] > 1) labels.set(index, ++nextLabel)
373
+ if (references[index] > 1) mapSet(labels, index, ++nextLabel)
352
374
  }
353
375
 
354
376
  const chunks = []
@@ -356,19 +378,19 @@ export function renderValueWire(wire, options = {}) {
356
378
  const append = (text) => {
357
379
  bytes += stringBytes(text)
358
380
  if (bytes > limits.maxStringBytes) throw new TypeError(`rendered PTC value exceeds ${limits.maxStringBytes} bytes`)
359
- chunks.push(text)
381
+ appendArray(chunks, text)
360
382
  }
361
383
  const emitted = new Set()
362
384
  const active = new Set()
363
385
  const tasks = [{ kind: 'atom', atom: normalized.root }]
364
386
  while (tasks.length > 0) {
365
- const task = tasks.pop()
387
+ const task = popArray(tasks)
366
388
  if (task.kind === 'text') {
367
389
  append(task.text)
368
390
  continue
369
391
  }
370
392
  if (task.kind === 'leave') {
371
- active.delete(task.index)
393
+ setDelete(active, task.index)
372
394
  continue
373
395
  }
374
396
  const primitive = atomText(task.atom)
@@ -377,38 +399,44 @@ export function renderValueWire(wire, options = {}) {
377
399
  continue
378
400
  }
379
401
  const index = task.atom.index
380
- const label = labels.get(index)
381
- if (emitted.has(index)) {
382
- append(active.has(index) ? `[Circular *${label}]` : `[Reference *${label}]`)
402
+ const label = mapGet(labels, index)
403
+ if (setHas(emitted, index)) {
404
+ append(setHas(active, index) ? `[Circular *${label}]` : `[Reference *${label}]`)
383
405
  continue
384
406
  }
385
- emitted.add(index)
386
- active.add(index)
407
+ setAdd(emitted, index)
408
+ setAdd(active, index)
387
409
  if (label !== undefined) append(`<ref *${label}> `)
388
410
  const node = normalized.nodes[index]
389
- tasks.push({ kind: 'leave', index })
411
+ appendArray(tasks, { kind: 'leave', index })
390
412
  if (node.type === 'array') {
391
413
  append('[')
392
- tasks.push({ kind: 'text', text: ']' })
393
- const entries = new Map(node.entries)
394
- if (node.length > 0 && !entries.has(node.length - 1)) tasks.push({ kind: 'text', text: ',' })
414
+ appendArray(tasks, { kind: 'text', text: ']' })
415
+ const entries = new Map()
416
+ for (let entryIndex = 0; entryIndex < node.entries.length; entryIndex += 1) {
417
+ const entry = node.entries[entryIndex]
418
+ mapSet(entries, entry[0], entry[1])
419
+ }
420
+ if (node.length > 0 && !mapHas(entries, node.length - 1)) appendArray(tasks, { kind: 'text', text: ',' })
395
421
  for (let item = node.length - 1; item >= 0; item -= 1) {
396
- if (item < node.length - 1) tasks.push({ kind: 'text', text: ', ' })
397
- const value = entries.get(item)
398
- if (value !== undefined) tasks.push({ kind: 'atom', atom: value })
422
+ if (item < node.length - 1) appendArray(tasks, { kind: 'text', text: ', ' })
423
+ const value = mapGet(entries, item)
424
+ if (value !== undefined) appendArray(tasks, { kind: 'atom', atom: value })
399
425
  }
400
426
  continue
401
427
  }
402
428
  append('{')
403
- tasks.push({ kind: 'text', text: '}' })
429
+ appendArray(tasks, { kind: 'text', text: '}' })
404
430
  for (let entryIndex = node.entries.length - 1; entryIndex >= 0; entryIndex -= 1) {
405
- const [key, value] = node.entries[entryIndex]
406
- if (entryIndex < node.entries.length - 1) tasks.push({ kind: 'text', text: ', ' })
407
- tasks.push({ kind: 'atom', atom: value })
408
- tasks.push({ kind: 'text', text: `${renderKey(key)}: ` })
431
+ const entry = node.entries[entryIndex]
432
+ const key = entry[0]
433
+ const value = entry[1]
434
+ if (entryIndex < node.entries.length - 1) appendArray(tasks, { kind: 'text', text: ', ' })
435
+ appendArray(tasks, { kind: 'atom', atom: value })
436
+ appendArray(tasks, { kind: 'text', text: `${renderKey(key)}: ` })
409
437
  }
410
438
  }
411
- return chunks.join('')
439
+ return join(chunks, '')
412
440
  }
413
441
 
414
442
  /**
@@ -416,6 +444,17 @@ export function renderValueWire(wire, options = {}) {
416
444
  * text. The untagged ranges overlap because a plain JSON root may itself be a string.
417
445
  */
418
446
  export function projectValueWire(wire, options = {}) {
419
- const value = decodeValue(wire, options)
420
- return isPlainJsonTree(value) ? value : renderValueWire(wire, options)
447
+ return prepareValueWire(wire, options).projectedValue
448
+ }
449
+
450
+ /** Validate once when canonical storage and outer presentation are both required. */
451
+ export function prepareValueWire(wire, options = {}) {
452
+ const limits = limitsOf(options)
453
+ const canonical = decodeCanonicalValue(wire, limits)
454
+ return {
455
+ wire: canonical.wire,
456
+ projectedValue: isPlainJsonTree(canonical.value)
457
+ ? canonical.value
458
+ : renderCanonicalValue(canonical.wire, limits),
459
+ }
421
460
  }
@@ -1,65 +1,32 @@
1
1
  import { mkdtemp, rm } from 'node:fs/promises'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { isAbsolute, join } from 'node:path'
4
- import { Worker } from 'node:worker_threads'
5
- import { messageOf } from './failure-reporting.js'
4
+ import { messageOf, processExitDescription } from './failure-reporting.js'
5
+ import { IsolatedOwner } from './isolated-worker.js'
6
+ import { normalizeWorkerEnvironment } from './worker-environment.js'
6
7
 
7
- const WINDOWS_ENVIRONMENT_NAMES = new Map([
8
- ['appdata', 'APPDATA'],
9
- ['comspec', 'ComSpec'],
10
- ['home', 'HOME'],
11
- ['homedrive', 'HOMEDRIVE'],
12
- ['homepath', 'HOMEPATH'],
13
- ['localappdata', 'LOCALAPPDATA'],
14
- ['path', 'PATH'],
15
- ['pathext', 'PATHEXT'],
16
- ['programdata', 'ProgramData'],
17
- ['programfiles', 'ProgramFiles'],
18
- ['programfiles(x86)', 'ProgramFiles(x86)'],
19
- ['systemdrive', 'SystemDrive'],
20
- ['systemroot', 'SystemRoot'],
21
- ['temp', 'TEMP'],
22
- ['tmp', 'TMP'],
23
- ['userprofile', 'USERPROFILE'],
24
- ['windir', 'windir'],
25
- ])
26
- const HOST_ONLY_ENVIRONMENT_NAMES = new Set(['node_test_context', 'node_v8_coverage'])
8
+ export { normalizeWorkerEnvironment }
27
9
 
28
- /** Project the host environment once without losing Windows case variants. */
29
- export function normalizeWorkerEnvironment(source, platform = process.platform) {
30
- if (platform !== 'win32') {
31
- return Object.fromEntries(Object.entries(source).filter(([name, value]) => (
32
- value !== undefined && !HOST_ONLY_ENVIRONMENT_NAMES.has(name.toLowerCase())
33
- )))
34
- }
35
- const normalized = new Map()
36
- for (const [name, value] of Object.entries(source)) {
37
- if (value === undefined) continue
38
- const key = name.toLowerCase()
39
- if (HOST_ONLY_ENVIRONMENT_NAMES.has(key)) continue
40
- const canonicalName = WINDOWS_ENVIRONMENT_NAMES.get(key) ?? name
41
- const current = normalized.get(key)
42
- if (current === undefined || name === canonicalName) {
43
- normalized.set(key, [canonicalName, value])
44
- }
45
- }
46
- return Object.fromEntries(normalized.values())
47
- }
10
+ const KERNEL_HELPER = new URL('./kernel-child.js', import.meta.url)
48
11
 
49
12
  /** Owns one session kernel's worker process, private port, and scratch directory. */
50
13
  export class WorkerClient {
51
- constructor({ workerUrl, cwd, onMessage, onFailure }) {
14
+ constructor({ workerUrl, cwd, onMessage, onFailure, compilerCache }) {
52
15
  this.workerUrl = workerUrl
53
16
  this.cwd = cwd
54
17
  this.onMessage = onMessage
55
18
  this.onFailure = onFailure
19
+ this.compilerCache = compilerCache
56
20
  this.worker = undefined
57
21
  this.workerLimit = undefined
58
22
  this.workerReady = undefined
59
23
  this.port = undefined
60
24
  this.scratchReady = undefined
61
25
  this.stderrTails = new WeakMap()
62
- this.terminations = new Set()
26
+ // One owner keeps every started kernel transport, including startup failures
27
+ // and closes that were refused, until its real exit is observed.
28
+ this.owner = new IsolatedOwner()
29
+ this.hostIds = new WeakMap()
63
30
  this.disposed = false
64
31
  }
65
32
 
@@ -101,19 +68,20 @@ export class WorkerClient {
101
68
  const environment = normalizeWorkerEnvironment(process.env)
102
69
  let worker
103
70
  try {
104
- worker = new Worker(this.workerUrl, {
71
+ const started = this.owner.start({
72
+ helper: KERNEL_HELPER,
73
+ entry: this.workerUrl,
74
+ workerData: { cwd: this.cwd, compilerCache: this.compilerCache?.() },
75
+ resourceLimits: { maxOldGenerationSizeMb: this.workerLimit },
105
76
  env: {
106
77
  ...environment,
107
78
  TEMP: scratchDirectory,
108
79
  TMP: scratchDirectory,
109
80
  TMPDIR: scratchDirectory,
110
81
  },
111
- execArgv: [],
112
- workerData: this.cwd === undefined ? {} : { cwd: this.cwd },
113
- resourceLimits: { maxOldGenerationSizeMb: this.workerLimit },
114
- stdout: true,
115
- stderr: true,
116
82
  })
83
+ worker = started.transport
84
+ this.hostIds.set(worker, started.id)
117
85
  } catch (error) {
118
86
  this.workerLimit = undefined
119
87
  throw error
@@ -129,15 +97,23 @@ export class WorkerClient {
129
97
  })
130
98
  this.stderrTails.set(worker, stderrTail)
131
99
  worker.on('error', error => this.fail(worker, `worker error: ${messageOf(error)}`))
132
- worker.on('exit', code => this.fail(worker, `worker exited with code ${code}`))
100
+ worker.on('exit', (code, signal) => {
101
+ this.fail(worker, `worker exited with ${processExitDescription(code, signal)}`)
102
+ })
133
103
  this.worker = worker
134
104
  this.workerReady = new Promise((resolve, reject) => {
135
- const onError = error => reject(error)
136
- const onExit = (code) => {
105
+ const onError = error => {
137
106
  const detail = this.stderrDetail(worker)
107
+ reject(detail === undefined
108
+ ? error
109
+ : new Error(`${messageOf(error)}; last stderr: ${detail}`, { cause: error }))
110
+ }
111
+ const onExit = (code, signal) => {
112
+ const detail = this.stderrDetail(worker)
113
+ const exit = processExitDescription(code, signal)
138
114
  reject(new Error(detail === undefined
139
- ? `worker exited with code ${code} before opening its private channel`
140
- : `worker exited with code ${code} before opening its private channel; last stderr: ${detail}`))
115
+ ? `worker exited with ${exit} before opening its private channel`
116
+ : `worker exited with ${exit} before opening its private channel; last stderr: ${detail}`))
141
117
  }
142
118
  worker.once('error', onError)
143
119
  worker.once('exit', onExit)
@@ -146,12 +122,16 @@ export class WorkerClient {
146
122
  worker.removeListener('exit', onExit)
147
123
  if (message?.type === 'startup-error' && typeof message.error === 'string') {
148
124
  reject(new Error(message.error))
149
- void this.reset(worker)
125
+ // The owner keeps a refused reclamation, so this must not surface as an
126
+ // unhandled rejection after the caller already handled the startup error.
127
+ void this.reset(worker).catch(() => {})
150
128
  return
151
129
  }
152
130
  if (message?.type !== 'ready' || typeof message.port?.postMessage !== 'function') {
153
131
  reject(new Error('kernel worker returned an invalid private channel'))
154
- void this.reset(worker)
132
+ // The owner keeps a refused reclamation, so this must not surface as an
133
+ // unhandled rejection after the caller already handled the startup error.
134
+ void this.reset(worker).catch(() => {})
155
135
  return
156
136
  }
157
137
  this.port = message.port
@@ -185,27 +165,37 @@ export class WorkerClient {
185
165
  }
186
166
 
187
167
  async reset(worker) {
168
+ const port = this.worker === worker ? this.port : undefined
188
169
  if (this.worker === worker) {
189
170
  this.worker = undefined
190
171
  this.workerLimit = undefined
191
172
  this.workerReady = undefined
192
- this.port?.close()
193
173
  this.port = undefined
194
174
  }
195
- const termination = worker.terminate()
196
- this.terminations.add(termination)
197
- try {
198
- await termination
199
- } finally {
200
- this.terminations.delete(termination)
201
- }
175
+ await this.owner.stop(this.hostIds.get(worker))
176
+ this.hostIds.delete(worker)
177
+ port?.close()
202
178
  }
203
179
 
204
180
  async dispose() {
205
181
  this.disposed = true
206
182
  const worker = this.worker
207
- if (worker !== undefined) await this.reset(worker)
208
- await Promise.all([...this.terminations])
183
+ const port = this.port
184
+ this.worker = undefined
185
+ this.workerLimit = undefined
186
+ this.workerReady = undefined
187
+ this.port = undefined
188
+ if (worker !== undefined) this.hostIds.delete(worker)
189
+ const failures = []
190
+ try {
191
+ port?.close()
192
+ } catch (error) {
193
+ failures.push(error)
194
+ }
195
+ failures.push(...await this.owner.dispose())
196
+ if (failures.length > 0) {
197
+ throw new AggregateError(failures, 'ptc-plus kernel worker disposal failed')
198
+ }
209
199
  if (this.scratchReady !== undefined) {
210
200
  try {
211
201
  const scratchDirectory = await this.scratchReady