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,5 +1,5 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks'
2
1
  import { randomUUID } from 'node:crypto'
2
+ import { createPrivateAsyncLocalStorage } from './async-local-storage-intrinsics.js'
3
3
  import { create as createDomain } from 'node:domain'
4
4
  import { createRequire, registerHooks } from 'node:module'
5
5
  import { resolve } from 'node:path'
@@ -8,7 +8,6 @@ import { PassThrough } from 'node:stream'
8
8
  import { inspect } from 'node:util'
9
9
  import { pathToFileURL } from 'node:url'
10
10
  import { parentPort, workerData } from 'node:worker_threads'
11
- import { runInContext } from 'node:vm'
12
11
  import { prepareConsoleProgram } from './compiler-service.js'
13
12
  import { createStatefulRootRuntime } from './stateful-root-runtime.js'
14
13
  import { managedModuleImport, managedRequire, readModuleImport } from './stateful-module-runtime.js'
@@ -18,21 +17,24 @@ import { moduleRuntimeIntrinsics } from './compiler-intrinsics.js'
18
17
  import { stringifyCompilerData } from './compiler-data.js'
19
18
  import { createCellCompletionObserver } from './cell-completion.js'
20
19
  import { WORKER_SHUTDOWN_ACKNOWLEDGEMENT, WORKER_SHUTDOWN_REQUEST } from './worker-shutdown.js'
20
+ import { WORKER_REPL_OPTIONS, createWorkerControlPromise, createWorkerReplErrorHandler,
21
+ disableWorkerReplDomain,
22
+ captureWorkerReplGlobals, protectWorkerReplAsyncContext, restoreWorkerReplGlobals, runInWorkerReplRealm,
23
+ workerReplContext } from './worker-repl-realm.js'
21
24
 
22
25
  const { Object, Set, setHas, setAdd, setDelete, appendArray } = moduleRuntimeIntrinsics
26
+ const Promise = globalThis.Promise
27
+ const process = globalThis.process
28
+ const hasProperty = globalThis.Reflect.has
29
+ const ControlPromise = createWorkerControlPromise()
23
30
 
24
- const input = new PassThrough()
25
- const output = new PassThrough()
26
- output.resume()
27
- const evaluations = new AsyncLocalStorage()
28
- const server = repl.start({ input, output, terminal: false, prompt: '', useGlobal: false })
29
- const completionObserver = createCellCompletionObserver(runInContext('Function', server.context))
30
- server.context.console = console
31
- const errorDomain = server.eval.domain ?? createDomain()
32
- errorDomain.removeAllListeners('error')
33
- errorDomain.on('error', error => evaluations.getStore()?.finish(true, error))
31
+ const evaluations = createPrivateAsyncLocalStorage()
32
+ const context = globalThis
33
+ const completionObserver = createCellCompletionObserver(runInWorkerReplRealm('Function'))
34
+ context.console = console
35
+ let retainedEvaluator
34
36
  const completionKey = `__ptc_console_${randomUUID().replaceAll('-', '')}`
35
- Object.defineProperty(server.context, completionKey, { value: value => {
37
+ Object.defineProperty(context, completionKey, { value: value => {
36
38
  evaluations.getStore().completed = true
37
39
  return value
38
40
  } })
@@ -44,16 +46,16 @@ const adapterParents = new Set()
44
46
  const knownBindings = new Set()
45
47
  const roots = createStatefulRootRuntime({
46
48
  importModule: (source, options) => managedModuleImport(replParent, source, options, replModuleRealm),
47
- errors: runInContext('({ ReferenceError, TypeError })', server.context),
48
- dynamicIntrinsics: runInContext('({intrinsicEval:eval,realmFunction:Function,globalObject:globalThis,errors:{ReferenceError,TypeError,SyntaxError},reflect:Reflect,object:Object})', server.context),
49
- readAmbient: name => runInContext(name, server.context),
50
- typeofAmbient: name => runInContext(`typeof ${name}`, server.context),
49
+ errors: runInWorkerReplRealm('({ ReferenceError, TypeError })'),
50
+ dynamicIntrinsics: runInWorkerReplRealm('({intrinsicEval:eval,realmFunction:Function,globalObject:globalThis,errors:{ReferenceError,TypeError,SyntaxError},reflect:Reflect,object:Object})'),
51
+ readAmbient: name => runInWorkerReplRealm(name),
52
+ typeofAmbient: name => runInWorkerReplRealm(`typeof ${name}`),
51
53
  writeAmbient(name, value, strict) {
52
54
  const argument = name === '__ptc_value' ? '__ptc_other_value' : '__ptc_value'
53
- runInContext(`${strict ? '"use strict";' : ''}(${argument}) => (${name} = ${argument})`, server.context)(value)
55
+ runInWorkerReplRealm(`${strict ? '"use strict";' : ''}(${argument}) => (${name} = ${argument})`)(value)
54
56
  },
55
- deleteAmbient: name => runInContext(`delete ${name}`, server.context),
56
- hasAmbient: name => Reflect.has(server.context, name),
57
+ deleteAmbient: name => runInWorkerReplRealm(`delete ${name}`),
58
+ hasAmbient: name => hasProperty(context, name),
57
59
  publish: name => setAdd(knownBindings, name),
58
60
  })
59
61
  class ConsoleReturn {
@@ -62,7 +64,8 @@ class ConsoleReturn {
62
64
  const moduleCompilation = createUserModuleCompilationHooks({
63
65
  transformForParent: parent => parent === replParent || parent === parentUrl ? USER_BINDING_TRANSFORM : undefined,
64
66
  })
65
- server.context.require = managedRequire(parentUrl, createRequire(parentUrl))
67
+ const providedRequire = managedRequire(parentUrl, createRequire(parentUrl))
68
+ Object.defineProperty(context, 'require', { configurable: true, value: providedRequire })
66
69
  const importCanary = 'data:text/javascript,export default 1'
67
70
  registerHooks({ resolve(specifier, context, nextResolve) {
68
71
  if (specifier === importCanary) replParent = context.parentURL
@@ -74,14 +77,34 @@ const describe = value => inspect(value, {
74
77
  colors: false, customInspect: false, getters: false, depth: 4,
75
78
  maxArrayLength: 40, maxStringLength: Math.min(workerData.maxOutputBytes, 8192), breakLength: 100,
76
79
  })
80
+ process.on('uncaughtException', error => {
81
+ const state = evaluations.getStore()
82
+ if (state?.finish(true, error) === true || state?.settled === true) return
83
+ parentPort.postMessage({ fatal: true, error: describe(error) })
84
+ })
77
85
 
78
- async function evaluate(source) {
86
+ function evaluate(source) {
79
87
  const prepared = prepareConsoleProgram(source, { languageSemantics: 'stateful-v1', knownBindings })
80
88
  const helpers = [prepared.rootRuntimeName, prepared.returnSignal]
81
- Object.defineProperty(server.context, prepared.rootRuntimeName, {
89
+ Object.defineProperty(context, prepared.rootRuntimeName, {
82
90
  configurable: true, value: roots.begin({ ...prepared.rootBindings, committed: () => {} }),
83
91
  })
84
- Object.defineProperty(server.context, prepared.returnSignal, { configurable: true, value: ConsoleReturn })
92
+ Object.defineProperty(context, prepared.returnSignal, { configurable: true, value: ConsoleReturn })
93
+ if (prepared.moduleLoads.length === 0) {
94
+ const operation = evaluatePrepared(prepared.code, prepared.asyncCompletion)
95
+ return new ControlPromise((resolve, reject) => operation.then(
96
+ value => { removeHelpers(helpers); resolve(value) },
97
+ error => { removeHelpers(helpers); reject(error) },
98
+ ))
99
+ }
100
+ return evaluateWithModules(prepared, helpers)
101
+ }
102
+
103
+ function removeHelpers(helpers) {
104
+ for (let index = 0; index < helpers.length; index++) delete context[helpers[index]]
105
+ }
106
+
107
+ async function evaluateWithModules(prepared, helpers) {
85
108
  try {
86
109
  for (let index = 0; index < prepared.moduleLoads.length; index++) {
87
110
  const load = prepared.moduleLoads[index]
@@ -109,30 +132,48 @@ async function evaluate(source) {
109
132
  } finally { setDelete(adapterParents, adapter) }
110
133
  if (load.global !== undefined) {
111
134
  appendArray(helpers, load.global)
112
- Object.defineProperty(server.context, load.global, { configurable: true, value: namespace })
135
+ Object.defineProperty(context, load.global, { configurable: true, value: namespace })
113
136
  }
114
137
  }
115
138
  return await evaluatePrepared(prepared.code, prepared.asyncCompletion)
116
139
  } finally {
117
- for (let index = 0; index < helpers.length; index++) delete server.context[helpers[index]]
140
+ removeHelpers(helpers)
118
141
  }
119
142
  }
120
143
 
121
144
  function evaluatePrepared(javascript, asyncCompletion = false) {
122
- return new Promise((resolve, reject) => {
145
+ releaseRetainedEvaluator()
146
+ const input = new PassThrough()
147
+ const output = new PassThrough()
148
+ output.resume()
149
+ const workerGlobalBaseline = captureWorkerReplGlobals(context)
150
+ const server = repl.start({ input, output, terminal: false, prompt: '', ...WORKER_REPL_OPTIONS,
151
+ handleError: createWorkerReplErrorHandler(evaluations, (state, error) => state.finish(true, error)) })
152
+ protectWorkerReplAsyncContext(server)
153
+ workerReplContext(server)
154
+ restoreWorkerReplGlobals(workerGlobalBaseline, context)
155
+ Object.defineProperty(context, 'require', { configurable: true, value: providedRequire })
156
+ const errorDomain = disableWorkerReplDomain(server.eval.domain ?? createDomain())
157
+ const evaluator = { server, input, output, errorDomain }
158
+ retainedEvaluator = evaluator
159
+ return new ControlPromise((resolve, reject) => {
123
160
  const complete = `this[${stringifyCompilerData(completionKey)}]`
124
161
  // Preserve expression results while distinguishing rejection with null/undefined.
125
162
  const code = `${javascript}\n;{let completed = ${complete}();}\n;`
126
163
  let settled = false
127
164
  const state = { completed: false, finish(failed, value) {
128
- if (settled) return
165
+ if (settled) return false
129
166
  settled = true
167
+ state.settled = true
130
168
  if (failed && value instanceof ConsoleReturn) completionObserver.settle(value.value, resolve, reject)
131
169
  else if (failed) reject(value)
132
170
  else completionObserver.settle(value, resolve, reject)
171
+ return true
133
172
  } }
173
+ evaluator.state = state
174
+ errorDomain.on('error', error => state.finish(true, error))
134
175
  evaluations.run(state, () => errorDomain.run(() => {
135
- server.eval(`${code}\n`, server.context, 'ptc-plus-console', (error, value) => {
176
+ server.eval(`${code}\n`, context, 'ptc-plus-console', (error, value) => {
136
177
  if (asyncCompletion && (error === null || error === undefined)) {
137
178
  completionObserver.observe(value, value => state.finish(false, value), error => state.finish(true, error))
138
179
  return
@@ -143,6 +184,16 @@ function evaluatePrepared(javascript, asyncCompletion = false) {
143
184
  }))
144
185
  })
145
186
  }
187
+
188
+ function releaseRetainedEvaluator() {
189
+ const evaluator = retainedEvaluator
190
+ if (evaluator === undefined) return undefined
191
+ retainedEvaluator = undefined
192
+ evaluator.errorDomain.removeAllListeners('error')
193
+ evaluator.server.close()
194
+ evaluator.input.destroy()
195
+ evaluator.output.destroy()
196
+ }
146
197
  try {
147
198
  replModuleRealm = await evaluatePrepared('({Promise,stringify:value=>`${value}`,importModule:(source,options)=>import(source,options)})')
148
199
  const prepared = compileStatefulModule(workerData.source)
@@ -154,30 +205,39 @@ try {
154
205
  for (let index = 0; index < names.length; index++) {
155
206
  const name = names[index]
156
207
  setAdd(knownBindings, name)
157
- Object.defineProperty(server.context, name, { configurable: true, enumerable: true, get: () => namespace[name] })
208
+ Object.defineProperty(context, name, { configurable: true, enumerable: true, get: () => namespace[name] })
158
209
  }
159
- parentPort.on('message', async (message) => {
210
+ parentPort.on('message', (message) => {
160
211
  if (message?.type === WORKER_SHUTDOWN_REQUEST) {
161
212
  // Release the realm, REPL and domain this console owns, then report the
162
213
  // release before the host gives up on the thread; see worker-shutdown.js.
163
- server.close()
164
- input.destroy()
165
- output.destroy()
166
- errorDomain.removeAllListeners('error')
214
+ releaseRetainedEvaluator()
167
215
  parentPort.postMessage({ type: WORKER_SHUTDOWN_ACKNOWLEDGEMENT })
168
216
  parentPort.close()
169
217
  return
170
218
  }
171
219
  const { id, code } = message
172
- let result
220
+ const send = result => {
221
+ const flushed = new ControlPromise(resolve => process.stdout.write('', () => process.stderr.write('', resolve)))
222
+ moduleRuntimeIntrinsics.observeOwnedPromise(flushed, () => {
223
+ parentPort.postMessage(result)
224
+ releaseRetainedEvaluator()
225
+ /* c8 ignore next 3 -- flushed has no reject capability */
226
+ }, error => {
227
+ releaseRetainedEvaluator()
228
+ parentPort.postMessage({ id, error: describe(error) })
229
+ })
230
+ }
231
+ let operation
173
232
  try {
174
- const value = await evaluate(code)
175
- result = { id, output: describe(value) }
233
+ operation = evaluate(code)
176
234
  } catch (error) {
177
- result = { id, error: describe(error) }
235
+ send({ id, error: describe(error) })
236
+ return
178
237
  }
179
- await new Promise(resolve => process.stdout.write('', () => process.stderr.write('', resolve)))
180
- parentPort.postMessage(result)
238
+ moduleRuntimeIntrinsics.observeOwnedPromise(operation,
239
+ value => send({ id, output: describe(value) }),
240
+ error => send({ id, error: describe(error) }))
181
241
  })
182
242
  } catch (error) {
183
243
  parentPort.postMessage({ fatal: true, error: describe(error) })
@@ -62,11 +62,19 @@ function selectedDescriptors(source, symbols, transform) {
62
62
 
63
63
 
64
64
  function declarationFor(entry, descriptors) {
65
+ const typeNamespace = `__ptcBinding_${createHash('sha256').update(entry.id).digest('hex').slice(0, 12)}`
66
+ const typeNamespaceTokens = new Set(descriptors.map(item => item.typeNamespaceToken))
67
+ if (typeNamespaceTokens.size !== 1 || typeNamespaceTokens.has(undefined)) throw new TypeError('binding symbols do not share one compiler type namespace')
68
+ const [typeNamespaceToken] = typeNamespaceTokens
69
+ const render = text => text.replaceAll(typeNamespaceToken, typeNamespace)
70
+ const typeDeclarations = [...new Set(descriptors.flatMap(item => item.typeDeclarations ?? []))]
71
+ const typePrefix = typeDeclarations.length === 0 ? '' : `declare namespace ${typeNamespace} {\n${typeDeclarations
72
+ .map(item => render(item).split('\n').map(line => ` ${line}`).join('\n')).join('\n')}\n}\n`
65
73
  const comment = entry.purpose === '' ? '' : `/** ${entry.purpose.replaceAll('*/', '* /')} */\n`
66
74
  if (entry.scope === 'namespace') {
67
- return `${comment}declare const ${entry.name}: {\n${descriptors.map(item => ` ${item.member};`).join('\n')}\n}`
75
+ return `${typePrefix}${comment}declare const ${entry.name}: {\n${descriptors.map(item => ` ${render(item.member)};`).join('\n')}\n}`
68
76
  }
69
- return descriptors.map((item, index) => `${index === 0 ? comment : ''}declare ${item.declaration}`).join('\n')
77
+ return `${typePrefix}${descriptors.map((item, index) => `${index === 0 ? comment : ''}declare ${render(item.declaration)}`).join('\n')}`
70
78
  }
71
79
 
72
80
  export function normalizeUserBindingEntry(value, { transform = USER_BINDING_TRANSFORM } = {}) {