dsh-ptc-plus 0.4.1 → 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 (53) hide show
  1. package/README.en.md +6 -10
  2. package/README.md +6 -10
  3. package/client.js +262 -31
  4. package/compiler-core.cjs +132 -130
  5. package/compiler-core.cjs.map +3 -3
  6. package/docs/README.md +1 -0
  7. package/docs/adr/0023-global-user-bindings.md +6 -6
  8. package/docs/adr/0025-use-versioned-logical-binding-identities.md +3 -3
  9. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +5 -5
  10. package/docs/architecture.md +9 -5
  11. package/docs/client-ui.md +1 -1
  12. package/docs/durability-design.md +6 -3
  13. package/docs/installation.md +4 -4
  14. package/docs/runtime-reference.md +5 -1
  15. package/docs/stateful-binding-updates-proposal.md +8 -4
  16. package/docs/user-bindings.en.md +5 -3
  17. package/docs/user-bindings.md +4 -2
  18. package/docs/verification-optimization-work.md +41 -0
  19. package/docs/verification.md +15 -1
  20. package/internal/async-local-storage-intrinsics.js +38 -0
  21. package/internal/binding-declaration-contract.js +8 -0
  22. package/internal/binding-source-analysis.js +381 -64
  23. package/internal/binding-update-policy.js +7 -1
  24. package/internal/cell-analysis.js +21 -8
  25. package/internal/compiler-intrinsics.js +9 -43
  26. package/internal/compiler-module-links.js +7 -4
  27. package/internal/dynamic-environment-integration.js +8 -4
  28. package/internal/dynamic-environment-runtime.js +24 -3
  29. package/internal/dynamic-native-calls.js +4 -1
  30. package/internal/dynamic-native-runtime.js +15 -1
  31. package/internal/failure-reporting.js +38 -30
  32. package/internal/isolated-worker.js +17 -10
  33. package/internal/kernel-worker.js +493 -331
  34. package/internal/module-compilation.js +5 -4
  35. package/internal/module-transform-contract.js +32 -2
  36. package/internal/record-utils.js +24 -15
  37. package/internal/repl-value-observer.js +6 -3
  38. package/internal/runtime-intrinsics.js +101 -0
  39. package/internal/session-cell-executor.js +2 -1
  40. package/internal/session-journal-schema.js +30 -13
  41. package/internal/session-journal.js +19 -4
  42. package/internal/session-runtime.js +1 -0
  43. package/internal/source-position-map.js +19 -0
  44. package/internal/stateful-root-compiler.js +3 -1
  45. package/internal/typescript-transform.js +4 -2
  46. package/internal/user-binding-console-worker.js +101 -41
  47. package/internal/user-bindings.js +10 -2
  48. package/internal/value-wire.js +101 -71
  49. package/internal/worker-cwd-virtualization.js +80 -53
  50. package/internal/worker-environment.js +20 -0
  51. package/internal/worker-repl-realm.js +134 -0
  52. package/package.json +2 -1
  53. package/src/client-activity.js +8 -1
@@ -1,4 +1,5 @@
1
1
  import { assertFields, isRecord } from './record-utils.js'
2
+ import { runtimeIntrinsics as internal } from './runtime-intrinsics.js'
2
3
  import {
3
4
  VALUE_CODEC,
4
5
  DEFAULT_VALUE_LIMITS,
@@ -12,12 +13,24 @@ import {
12
13
  } from './value-wire-schema.js'
13
14
 
14
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
15
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
16
26
 
17
27
  function limitsOf(options = {}) {
18
28
  const limits = { ...DEFAULT_VALUE_LIMITS, ...options }
19
- for (const [name, value] of Object.entries(limits)) {
20
- 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}`)
21
34
  }
22
35
  return limits
23
36
  }
@@ -28,7 +41,7 @@ function hasIntrinsicConstructor(prototype, name) {
28
41
  try {
29
42
  return constructor.name === name
30
43
  && constructor.prototype === prototype
31
- && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`
44
+ && reflectApply(intrinsicFunctionToString, constructor, []) === `function ${name}() { [native code] }`
32
45
  } catch {
33
46
  return false
34
47
  }
@@ -47,7 +60,7 @@ function plainObjectPrototype(value) {
47
60
 
48
61
  function hasPlainArrayPrototype(value) {
49
62
  const prototype = Object.getPrototypeOf(value)
50
- if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array')) return false
63
+ if (!isArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array')) return false
51
64
  const objectPrototype = Object.getPrototypeOf(prototype)
52
65
  return objectPrototype !== null && typeof objectPrototype === 'object'
53
66
  && isIntrinsicObjectPrototype(objectPrototype)
@@ -58,7 +71,7 @@ function invalid(path, detail) {
58
71
  }
59
72
 
60
73
  function stringBytes(value) {
61
- return Buffer.byteLength(value, 'utf8')
74
+ return bufferByteLength(value, 'utf8')
62
75
  }
63
76
 
64
77
  /** Encode one supported JavaScript graph into the project-owned JSON-safe wire. */
@@ -82,7 +95,7 @@ export function encodeValue(value, options = {}) {
82
95
  return current
83
96
  }
84
97
  if (typeof current === 'number') {
85
- if (Number.isNaN(current)) return { tag: 'number', value: 'nan' }
98
+ if (numberIsNaN(current)) return { tag: 'number', value: 'nan' }
86
99
  if (current === Infinity) return { tag: 'number', value: 'infinity' }
87
100
  if (current === -Infinity) return { tag: 'number', value: '-infinity' }
88
101
  if (Object.is(current, -0)) return { tag: 'number', value: '-0' }
@@ -90,20 +103,20 @@ export function encodeValue(value, options = {}) {
90
103
  }
91
104
  if (current === undefined) return { tag: 'undefined' }
92
105
  if (typeof current === 'bigint') {
93
- const text = String(current)
106
+ const text = toString(current)
94
107
  const digits = text[0] === '-' ? text.length - 1 : text.length
95
108
  if (digits > limits.maxBigIntDigits) invalid(path, `BigInt exceeds ${limits.maxBigIntDigits} digits`)
96
109
  accountText(text, path)
97
110
  return { tag: 'bigint', value: text }
98
111
  }
99
112
  if (typeof current !== 'object') invalid(path, typeof current)
100
- const prior = seen.get(current)
113
+ const prior = weakMapGet(seen, current)
101
114
  if (prior !== undefined) return { tag: 'reference', index: prior }
102
115
  if (nodes.length >= limits.maxNodes) invalid(path, `node budget exceeds ${limits.maxNodes}`)
103
116
  const index = nodes.length
104
- seen.set(current, index)
105
- nodes.push(undefined)
106
- pending.push({ value: current, index, path })
117
+ weakMapSet(seen, current, index)
118
+ appendArray(nodes, undefined)
119
+ appendArray(pending, { value: current, index, path })
107
120
  return { tag: 'reference', index }
108
121
  }
109
122
 
@@ -111,24 +124,26 @@ export function encodeValue(value, options = {}) {
111
124
  for (let cursor = 0; cursor < pending.length; cursor += 1) {
112
125
  const item = pending[cursor]
113
126
  const current = item.value
114
- if (Array.isArray(current)) {
127
+ if (isArray(current)) {
115
128
  if (!hasPlainArrayPrototype(current)) invalid(item.path, 'non-plain array')
116
129
  if (current.length > limits.maxArrayLength) invalid(item.path, `array length exceeds ${limits.maxArrayLength}`)
117
130
  const entries = []
118
- 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]
119
134
  if (key === 'length') continue
120
- if (typeof key !== 'string' || !/^(0|[1-9][0-9]*)$/.test(key)) {
135
+ if (typeof key !== 'string' || !regexpTest(/^(0|[1-9][0-9]*)$/, key)) {
121
136
  invalid(item.path, 'array has a symbol or non-index property')
122
137
  }
123
- const index = Number(key)
124
- 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')
125
140
  const descriptor = Object.getOwnPropertyDescriptor(current, key)
126
141
  if (descriptor === undefined || !Object.hasOwn(descriptor, 'value') || descriptor.enumerable !== true) {
127
142
  invalid(`${item.path}[${key}]`, 'array index must be an enumerable data property')
128
143
  }
129
144
  edgeCount += 1
130
145
  if (edgeCount > limits.maxEdges) invalid(item.path, `edge budget exceeds ${limits.maxEdges}`)
131
- entries.push([index, atom(descriptor.value, `${item.path}[${key}]`)])
146
+ appendArray(entries, [index, atom(descriptor.value, `${item.path}[${key}]`)])
132
147
  }
133
148
  nodes[item.index] = { type: 'array', length: current.length, entries }
134
149
  continue
@@ -137,7 +152,9 @@ export function encodeValue(value, options = {}) {
137
152
  const prototype = plainObjectPrototype(current)
138
153
  if (prototype === undefined) invalid(item.path, 'non-plain object')
139
154
  const entries = []
140
- 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]
141
158
  if (typeof key !== 'string') invalid(item.path, 'object has a symbol key')
142
159
  const descriptor = Object.getOwnPropertyDescriptor(current, key)
143
160
  if (descriptor === undefined || !Object.hasOwn(descriptor, 'value') || descriptor.enumerable !== true) {
@@ -146,7 +163,7 @@ export function encodeValue(value, options = {}) {
146
163
  accountText(key, `${item.path}.${key}`)
147
164
  edgeCount += 1
148
165
  if (edgeCount > limits.maxEdges) invalid(item.path, `edge budget exceeds ${limits.maxEdges}`)
149
- entries.push([key, atom(descriptor.value, `${item.path}.${key}`)])
166
+ appendArray(entries, [key, atom(descriptor.value, `${item.path}.${key}`)])
150
167
  }
151
168
  nodes[item.index] = { type: 'object', prototype, entries }
152
169
  }
@@ -161,7 +178,7 @@ function decodeAtom(atom, nodes, limits, budget, path) {
161
178
  return atom
162
179
  }
163
180
  if (typeof atom === 'number') {
164
- 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}`)
165
182
  return atom
166
183
  }
167
184
  if (!isRecord(atom) || typeof atom.tag !== 'string') throw new TypeError(`invalid PTC value atom at ${path}`)
@@ -179,31 +196,31 @@ function decodeAtom(atom, nodes, limits, budget, path) {
179
196
  }
180
197
  if (atom.tag === 'bigint') {
181
198
  assertFields(atom, BIGINT_FIELDS, 'PTC BigInt atom')
182
- 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)) {
183
200
  throw new TypeError('invalid PTC BigInt')
184
201
  }
185
202
  const digits = atom.value[0] === '-' ? atom.value.length - 1 : atom.value.length
186
203
  if (digits > limits.maxBigIntDigits) throw new TypeError('PTC BigInt digit budget exceeded')
187
204
  budget.textBytes += stringBytes(atom.value)
188
205
  if (budget.textBytes > limits.maxStringBytes) throw new TypeError('PTC value string budget exceeded')
189
- return BigInt(atom.value)
206
+ return toBigInt(atom.value)
190
207
  }
191
208
  if (atom.tag === 'reference') {
192
209
  assertFields(atom, REFERENCE_FIELDS, 'PTC reference atom')
193
- if (!Number.isSafeInteger(atom.index) || atom.index < 0 || atom.index >= nodes.length) {
210
+ if (!numberIsSafeInteger(atom.index) || atom.index < 0 || atom.index >= nodes.length) {
194
211
  throw new TypeError('dangling PTC value reference')
195
212
  }
196
- budget.reachable.add(atom.index)
213
+ setAdd(budget.reachable, atom.index)
197
214
  return nodes[atom.index]
198
215
  }
199
- throw new TypeError(`unknown PTC value atom tag ${JSON.stringify(atom.tag)}`)
216
+ throw new TypeError(`unknown PTC value atom tag ${jsonStringify(atom.tag)}`)
200
217
  }
201
218
 
202
219
  /** Validate, hydrate, and own one canonical PTC value envelope without recursive stack growth. */
203
220
  function decodeCanonicalValue(wire, options = {}) {
204
221
  const limits = limitsOf(options)
205
222
  assertFields(wire, ENVELOPE_FIELDS, 'PTC value envelope')
206
- 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')
207
224
  if (wire.nodes.length > limits.maxNodes) throw new TypeError('PTC value node budget exceeded')
208
225
  const targets = new Array(wire.nodes.length)
209
226
  for (let index = 0; index < wire.nodes.length; index += 1) {
@@ -211,14 +228,14 @@ function decodeCanonicalValue(wire, options = {}) {
211
228
  if (!isRecord(node)) throw new TypeError(`invalid PTC value node ${index}`)
212
229
  if (node.type === 'array') {
213
230
  assertFields(node, ARRAY_NODE_FIELDS, `PTC array node ${index}`)
214
- if (!Number.isSafeInteger(node.length) || node.length < 0 || node.length > limits.maxArrayLength) {
231
+ if (!numberIsSafeInteger(node.length) || node.length < 0 || node.length > limits.maxArrayLength) {
215
232
  throw new TypeError(`invalid PTC array length at node ${index}`)
216
233
  }
217
- 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}`)
218
235
  targets[index] = new Array(node.length)
219
236
  } else if (node.type === 'object') {
220
237
  assertFields(node, OBJECT_NODE_FIELDS, `PTC object node ${index}`)
221
- if (!['object', 'null'].includes(node.prototype) || !Array.isArray(node.entries)) {
238
+ if ((node.prototype !== 'object' && node.prototype !== 'null') || !isArray(node.entries)) {
222
239
  throw new TypeError(`invalid PTC object node ${index}`)
223
240
  }
224
241
  targets[index] = node.prototype === 'null' ? Object.create(null) : {}
@@ -234,8 +251,9 @@ function decodeCanonicalValue(wire, options = {}) {
234
251
  const target = targets[nodeIndex]
235
252
  if (node.type === 'array') {
236
253
  let previous = -1
237
- for (const entry of node.entries) {
238
- 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])
239
257
  || entry[0] <= previous || entry[0] < 0 || entry[0] >= node.length) {
240
258
  throw new TypeError(`invalid PTC array entry at node ${nodeIndex}`)
241
259
  }
@@ -250,11 +268,12 @@ function decodeCanonicalValue(wire, options = {}) {
250
268
  continue
251
269
  }
252
270
  const keys = new Set()
253
- for (const entry of node.entries) {
254
- 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])) {
255
274
  throw new TypeError(`invalid PTC object entry at node ${nodeIndex}`)
256
275
  }
257
- keys.add(entry[0])
276
+ setAdd(keys, entry[0])
258
277
  budget.textBytes += stringBytes(entry[0])
259
278
  if (budget.textBytes > limits.maxStringBytes) throw new TypeError('PTC value string budget exceeded')
260
279
  budget.edges += 1
@@ -265,10 +284,10 @@ function decodeCanonicalValue(wire, options = {}) {
265
284
  })
266
285
  }
267
286
  }
268
- 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')
269
288
  const canonical = encodeValue(root, limits)
270
- const serialization = JSON.stringify(canonical)
271
- 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')
272
291
  return { value: root, wire: canonical, serialization }
273
292
  }
274
293
 
@@ -295,31 +314,32 @@ export function isPlainJsonTree(value) {
295
314
  const pending = [value]
296
315
  const seen = new WeakSet()
297
316
  while (pending.length > 0) {
298
- const current = pending.pop()
317
+ const current = popArray(pending)
299
318
  if (current === null || typeof current === 'string' || typeof current === 'boolean') continue
300
- if (typeof current === 'number' && Number.isFinite(current) && !Object.is(current, -0)) continue
319
+ if (typeof current === 'number' && numberIsFinite(current) && !Object.is(current, -0)) continue
301
320
  if (typeof current !== 'object') return false
302
- if (seen.has(current)) return false
303
- seen.add(current)
304
- if (Array.isArray(current)) {
321
+ if (weakSetHas(seen, current)) return false
322
+ weakSetAdd(seen, current)
323
+ if (isArray(current)) {
305
324
  for (let index = 0; index < current.length; index += 1) {
306
325
  if (!Object.hasOwn(current, index)) return false
307
- pending.push(current[index])
326
+ appendArray(pending, current[index])
308
327
  }
309
328
  } else {
310
- 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]])
311
331
  }
312
332
  }
313
333
  return true
314
334
  }
315
335
 
316
336
  function renderKey(key) {
317
- return IDENTIFIER.test(key) ? key : JSON.stringify(key)
337
+ return regexpTest(IDENTIFIER, key) ? key : jsonStringify(key)
318
338
  }
319
339
 
320
340
  function atomText(atom) {
321
- if (atom === null || typeof atom === 'boolean' || typeof atom === 'number') return String(atom)
322
- 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)
323
343
  if (atom.tag === 'undefined') return 'undefined'
324
344
  if (atom.tag === 'number') {
325
345
  return atom.value === 'nan' ? 'NaN'
@@ -337,16 +357,20 @@ export function renderValueWire(wire, options = {}) {
337
357
  }
338
358
 
339
359
  function renderCanonicalValue(normalized, limits) {
340
- const references = new Array(normalized.nodes.length).fill(0)
360
+ const references = new Array(normalized.nodes.length)
361
+ for (let index = 0; index < references.length; index += 1) references[index] = 0
341
362
  const count = (atom) => {
342
363
  if (isRecord(atom) && atom.tag === 'reference') references[atom.index] += 1
343
364
  }
344
365
  count(normalized.root)
345
- 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
+ }
346
370
  const labels = new Map()
347
371
  let nextLabel = 0
348
372
  for (let index = 0; index < references.length; index += 1) {
349
- if (references[index] > 1) labels.set(index, ++nextLabel)
373
+ if (references[index] > 1) mapSet(labels, index, ++nextLabel)
350
374
  }
351
375
 
352
376
  const chunks = []
@@ -354,19 +378,19 @@ function renderCanonicalValue(normalized, limits) {
354
378
  const append = (text) => {
355
379
  bytes += stringBytes(text)
356
380
  if (bytes > limits.maxStringBytes) throw new TypeError(`rendered PTC value exceeds ${limits.maxStringBytes} bytes`)
357
- chunks.push(text)
381
+ appendArray(chunks, text)
358
382
  }
359
383
  const emitted = new Set()
360
384
  const active = new Set()
361
385
  const tasks = [{ kind: 'atom', atom: normalized.root }]
362
386
  while (tasks.length > 0) {
363
- const task = tasks.pop()
387
+ const task = popArray(tasks)
364
388
  if (task.kind === 'text') {
365
389
  append(task.text)
366
390
  continue
367
391
  }
368
392
  if (task.kind === 'leave') {
369
- active.delete(task.index)
393
+ setDelete(active, task.index)
370
394
  continue
371
395
  }
372
396
  const primitive = atomText(task.atom)
@@ -375,38 +399,44 @@ function renderCanonicalValue(normalized, limits) {
375
399
  continue
376
400
  }
377
401
  const index = task.atom.index
378
- const label = labels.get(index)
379
- if (emitted.has(index)) {
380
- 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}]`)
381
405
  continue
382
406
  }
383
- emitted.add(index)
384
- active.add(index)
407
+ setAdd(emitted, index)
408
+ setAdd(active, index)
385
409
  if (label !== undefined) append(`<ref *${label}> `)
386
410
  const node = normalized.nodes[index]
387
- tasks.push({ kind: 'leave', index })
411
+ appendArray(tasks, { kind: 'leave', index })
388
412
  if (node.type === 'array') {
389
413
  append('[')
390
- tasks.push({ kind: 'text', text: ']' })
391
- const entries = new Map(node.entries)
392
- 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: ',' })
393
421
  for (let item = node.length - 1; item >= 0; item -= 1) {
394
- if (item < node.length - 1) tasks.push({ kind: 'text', text: ', ' })
395
- const value = entries.get(item)
396
- 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 })
397
425
  }
398
426
  continue
399
427
  }
400
428
  append('{')
401
- tasks.push({ kind: 'text', text: '}' })
429
+ appendArray(tasks, { kind: 'text', text: '}' })
402
430
  for (let entryIndex = node.entries.length - 1; entryIndex >= 0; entryIndex -= 1) {
403
- const [key, value] = node.entries[entryIndex]
404
- if (entryIndex < node.entries.length - 1) tasks.push({ kind: 'text', text: ', ' })
405
- tasks.push({ kind: 'atom', atom: value })
406
- 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)}: ` })
407
437
  }
408
438
  }
409
- return chunks.join('')
439
+ return join(chunks, '')
410
440
  }
411
441
 
412
442
  /**
@@ -1,5 +1,10 @@
1
1
  import { isAbsolute, parse, resolve, sep } from 'node:path'
2
2
  import { promisify } from 'node:util'
3
+ import { runtimeIntrinsics as internal } from './runtime-intrinsics.js'
4
+
5
+ const { Reflect: privateReflect, appendArray, bufferConcat, bufferIsBuffer, bufferToString,
6
+ copyArray, isArray, regexpTest } = internal
7
+ const TRAILING_SEPARATOR = /[\\/]$/
3
8
 
4
9
  const FILE_SYSTEM_PATH_ARGUMENTS = Object.freeze({
5
10
  access: [0], accessSync: [0], appendFile: [0], appendFileSync: [0],
@@ -34,7 +39,7 @@ const CHILD_PROCESS_PROJECTED_PROPERTIES = Object.freeze({
34
39
  function wrapBuiltinCallable(original, projectArguments, projectedProperties = []) {
35
40
  const wrapped = {
36
41
  invoke(...args) {
37
- return Reflect.apply(original, this, projectArguments(args))
42
+ return privateReflect.apply(original, this, projectArguments(args))
38
43
  },
39
44
  }.invoke
40
45
  const descriptors = Object.getOwnPropertyDescriptors(original)
@@ -59,80 +64,101 @@ function wrapBuiltin(owner, name, projectArguments, projectedProperties) {
59
64
  owner[name] = wrapBuiltinCallable(original, projectArguments, projectedProperties)
60
65
  }
61
66
 
67
+ export function projectChildProcessArguments(name, args, sessionCwd) {
68
+ const sessionChildOptions = options => (
69
+ options === null || typeof options !== 'object' || options.cwd !== undefined
70
+ ? options
71
+ : { ...options, cwd: sessionCwd }
72
+ )
73
+ const withDefaultOptions = (index) => {
74
+ const next = copyArray(args)
75
+ next[index] = sessionChildOptions(next[index]) ?? { cwd: sessionCwd }
76
+ return next
77
+ }
78
+ const insertDefaultOptions = (index) => {
79
+ const next = copyArray(args, 0, index)
80
+ appendArray(next, { cwd: sessionCwd })
81
+ return copyArray(args, index, args.length, next)
82
+ }
83
+ if (name === 'exec' || name === 'execSync') {
84
+ const next = copyArray(args)
85
+ if (typeof next[1] === 'function') return insertDefaultOptions(1)
86
+ else next[1] = sessionChildOptions(next[1]) ?? { cwd: sessionCwd }
87
+ return next
88
+ }
89
+ if (name === 'execFile' || name === 'execFileSync') {
90
+ if (args.length === 1 || typeof args[1] === 'function') {
91
+ return insertDefaultOptions(1)
92
+ }
93
+ if (typeof args[2] === 'function') {
94
+ if (args[1] !== undefined && !isArray(args[1])) return withDefaultOptions(1)
95
+ return insertDefaultOptions(2)
96
+ }
97
+ if (args[1] !== undefined && !isArray(args[1])) return withDefaultOptions(1)
98
+ return withDefaultOptions(2)
99
+ }
100
+ return args.length === 1 || (args[1] !== undefined && !isArray(args[1]))
101
+ ? withDefaultOptions(1)
102
+ : withDefaultOptions(2)
103
+ }
104
+
62
105
  /** Install one session's cwd projection into Node builtins loaded by the worker. */
63
- export function installWorkerCwdVirtualization(sessionCwd, originalRequire) {
64
- if (sessionCwd === undefined) return
106
+ export function installWorkerCwdVirtualization(sessionCwd, originalRequire, markVolatile) {
107
+ if (sessionCwd === undefined) {
108
+ const nativeCwd = process.cwd
109
+ process.cwd = () => {
110
+ markVolatile('process.cwd')
111
+ return privateReflect.apply(nativeCwd, process, [])
112
+ }
113
+ return
114
+ }
65
115
  const nativeResolve = resolve
66
- const bufferPrefix = Buffer.from(sessionCwd + (/[\\/]$/.test(sessionCwd) ? '' : sep))
116
+ const bufferPrefix = Buffer.from(sessionCwd + (regexpTest(TRAILING_SEPARATOR, sessionCwd) ? '' : sep))
67
117
  const sessionPath = (value) => {
68
118
  if (typeof value === 'string') return isAbsolute(value) ? value : nativeResolve(sessionCwd, value)
69
- if (Buffer.isBuffer(value)) {
70
- const absolute = isAbsolute(String.fromCharCode(...value.subarray(0, 3)))
71
- return absolute ? value : Buffer.concat([bufferPrefix, value])
119
+ if (bufferIsBuffer(value)) {
120
+ const absolute = isAbsolute(bufferToString(value, 'utf8', 0, 3))
121
+ return absolute ? value : bufferConcat([bufferPrefix, value])
72
122
  }
73
123
  return value
74
124
  }
75
125
  const sessionPathPrefix = (value) => {
76
126
  if (typeof value !== 'string' || isAbsolute(value) || parse(value).root !== '') return sessionPath(value)
77
- return sessionCwd + (/[\\/]$/.test(sessionCwd) ? '' : sep) + value
127
+ return sessionCwd + (regexpTest(TRAILING_SEPARATOR, sessionCwd) ? '' : sep) + value
78
128
  }
79
129
  const projectPaths = (args, indices) => {
80
- const next = [...args]
81
- for (const index of indices) next[index] = sessionPath(next[index])
130
+ const next = copyArray(args)
131
+ for (let offset = 0; offset < indices.length; offset += 1) {
132
+ const index = indices[offset]
133
+ next[index] = sessionPath(next[index])
134
+ }
82
135
  return next
83
136
  }
84
137
  const projectPrefix = (args) => {
85
- const next = [...args]
138
+ const next = copyArray(args)
86
139
  next[0] = sessionPathPrefix(next[0])
87
140
  return next
88
141
  }
89
- const sessionChildOptions = options => (
90
- options === null || typeof options !== 'object' || options.cwd !== undefined
91
- ? options
92
- : { ...options, cwd: sessionCwd }
93
- )
94
- const withDefaultOptions = (args, index) => {
95
- const next = [...args]
96
- next[index] = sessionChildOptions(next[index]) ?? { cwd: sessionCwd }
97
- return next
98
- }
99
- const childProcessOptions = (name, args) => {
100
- if (name === 'exec' || name === 'execSync') {
101
- const next = [...args]
102
- if (typeof next[1] === 'function') next.splice(1, 0, { cwd: sessionCwd })
103
- else next[1] = sessionChildOptions(next[1]) ?? { cwd: sessionCwd }
104
- return next
105
- }
106
- if (name === 'execFile' || name === 'execFileSync') {
107
- if (args.length === 1 || typeof args[1] === 'function') {
108
- const next = [...args]
109
- next.splice(1, 0, { cwd: sessionCwd })
110
- return next
111
- }
112
- if (typeof args[2] === 'function') {
113
- if (args[1] !== undefined && !Array.isArray(args[1])) return withDefaultOptions(args, 1)
114
- const next = [...args]
115
- next.splice(2, 0, { cwd: sessionCwd })
116
- return next
117
- }
118
- if (args[1] !== undefined && !Array.isArray(args[1])) return withDefaultOptions(args, 1)
119
- return withDefaultOptions(args, 2)
120
- }
121
- return args.length === 1 || (args[1] !== undefined && !Array.isArray(args[1]))
122
- ? withDefaultOptions(args, 1)
123
- : withDefaultOptions(args, 2)
124
- }
125
142
  const sessionGlobOptions = (args) => {
126
- const next = [...args]
127
- if (typeof next[1] === 'function') next.splice(1, 0, { cwd: sessionCwd })
128
- else if (next[1] === undefined) next[1] = { cwd: sessionCwd }
143
+ if (typeof args[1] === 'function') {
144
+ const next = copyArray(args, 0, 1)
145
+ appendArray(next, { cwd: sessionCwd })
146
+ return copyArray(args, 1, args.length, next)
147
+ }
148
+ const next = copyArray(args)
149
+ if (next[1] === undefined) next[1] = { cwd: sessionCwd }
129
150
  else if (next[1] !== null && typeof next[1] === 'object' && next[1].cwd === undefined) {
130
151
  next[1] = { ...next[1], cwd: sessionCwd }
131
152
  }
132
153
  return next
133
154
  }
134
155
 
135
- process.cwd = () => sessionCwd
156
+ Object.defineProperty(process, 'cwd', {
157
+ configurable: false,
158
+ enumerable: true,
159
+ writable: false,
160
+ value: () => sessionCwd,
161
+ })
136
162
  const fs = originalRequire('node:fs')
137
163
  const path = originalRequire('node:path')
138
164
  for (const [name, indices] of Object.entries(FILE_SYSTEM_PATH_ARGUMENTS)) {
@@ -147,9 +173,10 @@ export function installWorkerCwdVirtualization(sessionCwd, originalRequire) {
147
173
  wrapBuiltin(fs, name, sessionGlobOptions)
148
174
  wrapBuiltin(fs.promises, name, sessionGlobOptions)
149
175
  }
150
- wrapBuiltin(path, 'resolve', args => [sessionCwd, ...args])
176
+ wrapBuiltin(path, 'resolve', args => copyArray(args, 0, args.length, [sessionCwd]))
151
177
  const childProcess = originalRequire('node:child_process')
152
178
  for (const name of ['exec', 'execFile', 'execFileSync', 'execSync', 'fork', 'spawn', 'spawnSync']) {
153
- wrapBuiltin(childProcess, name, args => childProcessOptions(name, args), CHILD_PROCESS_PROJECTED_PROPERTIES[name])
179
+ wrapBuiltin(childProcess, name,
180
+ args => projectChildProcessArguments(name, args, sessionCwd), CHILD_PROCESS_PROJECTED_PROPERTIES[name])
154
181
  }
155
182
  }
@@ -23,6 +23,26 @@ export const HOST_ONLY_ENVIRONMENT_NAMES = new Set([
23
23
  'dsh_ptc_compiler_bytecode',
24
24
  ])
25
25
 
26
+ /** Detect whether the Host executable needs Electron's Node startup mode. */
27
+ export function isElectronHost(versions = process.versions) {
28
+ return typeof versions?.electron === 'string'
29
+ }
30
+
31
+ /** Overlay only the helper process environment needed to boot an Electron executable as Node. */
32
+ export function helperProcessEnvironment(source, platform = process.platform, electronHost = isElectronHost()) {
33
+ if (!electronHost) return source
34
+ const environment = { ...(source ?? process.env) }
35
+ if (platform === 'win32') {
36
+ for (const name of Object.keys(environment)) {
37
+ if (name !== 'ELECTRON_RUN_AS_NODE' && name.toLowerCase() === 'electron_run_as_node') {
38
+ delete environment[name]
39
+ }
40
+ }
41
+ }
42
+ environment.ELECTRON_RUN_AS_NODE = '1'
43
+ return environment
44
+ }
45
+
26
46
  /** Project the host environment once without losing Windows case variants. */
27
47
  export function normalizeWorkerEnvironment(source, platform = process.platform) {
28
48
  if (platform !== 'win32') {