@pulse-compute/wasm-compiler 0.0.0 → 1.0.0-beta.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.
- package/README.md +53 -1
- package/bin/provider-proof-composition.js +34 -0
- package/bin/pulsewasm-extract.js +20 -0
- package/package.json +60 -5
- package/src/artifacts-dir.js +13 -0
- package/src/ast-json.js +52 -0
- package/src/build-manifest.js +354 -0
- package/src/canonical-api-compiler.js +269 -0
- package/src/canonical-native-compiler.js +411 -0
- package/src/canonical-native-plan.js +1478 -0
- package/src/canonical-project-compiler.js +1224 -0
- package/src/canonical-router-compiler.js +25 -0
- package/src/cli-intents.js +1235 -0
- package/src/cli.js +1927 -0
- package/src/codegen/assemblyscript-compile.js +3 -0
- package/src/codegen/assemblyscript-core.js +3 -0
- package/src/codegen/assemblyscript-shape.js +3 -0
- package/src/codegen/assemblyscript-wasm-smoke.js +3 -0
- package/src/codegen/backend-capabilities.js +3 -0
- package/src/codegen/channel-broadcaster.js +3 -0
- package/src/codegen/compiled-handlers.js +3 -0
- package/src/codegen/compiled-wasm-runtime.js +3 -0
- package/src/codegen/config-references.js +248 -0
- package/src/codegen/dispatch-ts.js +145 -0
- package/src/codegen/effect-composition.js +331 -0
- package/src/codegen/effect-runtime.js +418 -0
- package/src/codegen/execution-harness-ts.js +467 -0
- package/src/codegen/handler-bindings-ts.js +298 -0
- package/src/codegen/handler-library-contracts.js +3 -0
- package/src/codegen/host-capabilities.js +3 -0
- package/src/codegen/host-runtime-kernel.js +3 -0
- package/src/codegen/integrated-compiled-app.js +3 -0
- package/src/codegen/json-body.js +3 -0
- package/src/codegen/library-sidecars.js +3 -0
- package/src/codegen/local-harness-ts.js +453 -0
- package/src/codegen/pulse-wrapper.js +217 -0
- package/src/codegen/request-result-headers.js +3 -0
- package/src/codegen/schema-json-compile.js +3 -0
- package/src/codegen/schema-json-sidecar-v2.js +3 -0
- package/src/codegen/schema-json-sidecar.js +3 -0
- package/src/codegen/streaming-passthrough.js +3 -0
- package/src/codegen/wasm-host-abi.js +3 -0
- package/src/codegen/wasm-host-bridge.js +3 -0
- package/src/compiled-wasm-host-runtime-kv.js +3 -0
- package/src/config-resolver.js +813 -0
- package/src/crypto-requirement-planner.js +89 -0
- package/src/definitions/config-schema.js +14 -0
- package/src/definitions/handler-roles.js +14 -0
- package/src/definitions/path-grammar.js +14 -0
- package/src/definitions/router-api.js +14 -0
- package/src/diagnostics/codes.js +14 -0
- package/src/diagnostics/reporter.js +14 -0
- package/src/diagnostics.js +14 -0
- package/src/dispatch-table.js +400 -0
- package/src/events/event-emit.js +265 -0
- package/src/events/event-topology.js +127 -0
- package/src/execution-plan.js +463 -0
- package/src/extractor.js +2816 -0
- package/src/handler-eval.js +1154 -0
- package/src/handler-table.js +326 -0
- package/src/index.js +19 -0
- package/src/javascript-application-plan.js +181 -0
- package/src/kv-provider.js +3 -0
- package/src/path-table.js +60 -0
- package/src/path.js +14 -0
- package/src/patterns/config-define.js +30 -0
- package/src/patterns/dependency-call.js +18 -0
- package/src/patterns/env-lookup.js +13 -0
- package/src/patterns/handler-reference.js +29 -0
- package/src/patterns/path-literal.js +35 -0
- package/src/patterns/result.js +15 -0
- package/src/patterns/router-chain-call.js +50 -0
- package/src/patterns/router-construction.js +19 -0
- package/src/project/package-reachability.js +782 -0
- package/src/project/reachable-graph-builder.js +992 -0
- package/src/project/reachable-graph-contract.js +54 -0
- package/src/project/reachable-graph-implementation.js +36 -0
- package/src/project/router-module-linker.js +710 -0
- package/src/project-config-compiler.js +222 -0
- package/src/project-target-support.js +500 -0
- package/src/provider-toolchain.js +299 -0
- package/src/spine/async-surface-normalizer.js +328 -0
- package/src/spine/canonical-handler-ir.js +336 -0
- package/src/spine/canonical-native-module.js +87 -0
- package/src/spine/canonical-native-plan.js +89 -0
- package/src/spine/canonical-project.js +76 -0
- package/src/spine/canonical-router.js +155 -0
- package/src/spine/canonical-source.js +165 -0
- package/src/spine/diagnostic-authority.js +290 -0
- package/src/spine/equivalence.js +262 -0
- package/src/spine/guest-unit-stage.js +87 -0
- package/src/spine/handler-ir-emitter.js +455 -0
- package/src/spine/handler-ir-managed.js +1598 -0
- package/src/spine/handler-ir.js +797 -0
- package/src/spine/handler-surface-authority.js +588 -0
- package/src/spine/package-operation-seam.js +1015 -0
- package/src/spine/pipeline.js +202 -0
- package/src/spine/plain-handler-frontend.js +545 -0
- package/src/spine/provider-requirement-authority.js +208 -0
- package/src/spine/router-control-contract.js +17 -0
- package/src/spine/router-handler-frontend.js +514 -0
- package/src/spine/router-handler-ir.js +372 -0
- package/src/spine/router-topology-frontend.js +638 -0
- package/src/stable-id.js +14 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const { PACKAGE_VERSION, normalizeArtifact } = require('../diagnostics.js');
|
|
5
|
+
const { EXECUTION_PLAN_VERSION } = loadContractsExecutionPlan();
|
|
6
|
+
|
|
7
|
+
const EXECUTION_HARNESS_VERSION = 'pulsewasm.execution-harness.v2';
|
|
8
|
+
|
|
9
|
+
function loadContractsExecutionPlan() {
|
|
10
|
+
try {
|
|
11
|
+
return require('@pulse-compute/wasm-contracts/execution-plan');
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
14
|
+
return require('../../../contracts/src/execution-plan.js');
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sha256(text) {
|
|
21
|
+
return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function tsJson(value) {
|
|
25
|
+
return JSON.stringify(value, null, 2)
|
|
26
|
+
.replace(/</g, '\\u003c')
|
|
27
|
+
.replace(/>/g, '\\u003e')
|
|
28
|
+
.replace(/&/g, '\\u0026');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function header(file = 'execution harness') {
|
|
32
|
+
return [
|
|
33
|
+
'/*',
|
|
34
|
+
' * Generated by PulseWasm Phase 8E.',
|
|
35
|
+
' * Do not edit by hand.',
|
|
36
|
+
' *',
|
|
37
|
+
` * This ${file} is generated from execution-plan.json + handler-bindings.json.`,
|
|
38
|
+
' * It executes ordered entry metadata and must not use the original Router.',
|
|
39
|
+
' */',
|
|
40
|
+
''
|
|
41
|
+
].join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeFile(file, text) {
|
|
45
|
+
return {
|
|
46
|
+
file,
|
|
47
|
+
bytes: Buffer.byteLength(text, 'utf8'),
|
|
48
|
+
sha256: sha256(text),
|
|
49
|
+
text
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function uniqueNumbers(values) {
|
|
54
|
+
return Array.from(new Set(values.filter((value) => Number.isInteger(value)))).sort((a, b) => a - b);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function collectExpectedSlots(dispatchTable, executionPlan) {
|
|
58
|
+
const slots = [];
|
|
59
|
+
if (executionPlan && Array.isArray(executionPlan.entries)) {
|
|
60
|
+
for (const entry of executionPlan.entries) {
|
|
61
|
+
if (Number.isInteger(entry.handler?.slot)) slots.push(entry.handler.slot);
|
|
62
|
+
if (Number.isInteger(entry.channel?.slot)) slots.push(entry.channel.slot);
|
|
63
|
+
}
|
|
64
|
+
for (const scope of executionPlan.scopes || []) {
|
|
65
|
+
for (const step of scope.connect || []) if (Number.isInteger(step.slot)) slots.push(step.slot);
|
|
66
|
+
for (const step of scope.disconnect || []) if (Number.isInteger(step.slot)) slots.push(step.slot);
|
|
67
|
+
if (Number.isInteger(scope.channel?.slot)) slots.push(scope.channel.slot);
|
|
68
|
+
}
|
|
69
|
+
return uniqueNumbers(slots);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const route of dispatchTable?.routes || []) {
|
|
73
|
+
for (const step of route.execution?.normal || []) if (Number.isInteger(step.slot)) slots.push(step.slot);
|
|
74
|
+
for (const step of route.execution?.error || []) if (Number.isInteger(step.slot)) slots.push(step.slot);
|
|
75
|
+
for (const step of route.execution?.lifecycle?.connect || []) if (Number.isInteger(step.slot)) slots.push(step.slot);
|
|
76
|
+
for (const step of route.execution?.lifecycle?.disconnect || []) if (Number.isInteger(step.slot)) slots.push(step.slot);
|
|
77
|
+
const channel = route.execution?.channel;
|
|
78
|
+
if (channel && channel.kind === 'handler' && Number.isInteger(channel.slot)) slots.push(channel.slot);
|
|
79
|
+
}
|
|
80
|
+
return uniqueNumbers(slots);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildExecutionPlanSource(executionPlan) {
|
|
84
|
+
return `${header('execution plan module')}export const EXECUTION_PLAN_VERSION = ${tsJson(executionPlan?.version || EXECUTION_PLAN_VERSION)} as const\n\nexport const EXECUTION_PLAN = ${tsJson(executionPlan || { entries: [], scopes: [] })} as const\n\nexport const EXECUTION_ENTRIES = EXECUTION_PLAN.entries\n\nexport const EXECUTION_SCOPES = EXECUTION_PLAN.scopes\n`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildExecutionHarnessSource(expectedSlots) {
|
|
88
|
+
return `${header()}import { match, matchPathPattern } from './dispatch'
|
|
89
|
+
import type { DispatchMatch, HandlerUse } from './types'
|
|
90
|
+
import { EXECUTION_ENTRIES, EXECUTION_SCOPES } from './execution-plan'
|
|
91
|
+
import { getHandlerBySlot, assertHandlerBindingCoverage, type PulseHandler } from './handler-slots'
|
|
92
|
+
|
|
93
|
+
export type ExecutionMode = 'entry-scan'
|
|
94
|
+
export type ExecutionPhase = 'normal' | 'error' | 'lifecycle:connect' | 'lifecycle:disconnect' | 'channel'
|
|
95
|
+
export type ExecutionStatus = 'response' | 'halt' | 'continue' | 'error' | 'not-found'
|
|
96
|
+
|
|
97
|
+
export type ExecutionTraceEvent = {
|
|
98
|
+
readonly kind: 'enter' | 'next' | 'exit' | 'halt' | 'skip' | 'channel' | 'scope' | 'mount'
|
|
99
|
+
readonly phase: ExecutionPhase
|
|
100
|
+
readonly index: number
|
|
101
|
+
readonly slot?: number
|
|
102
|
+
readonly handlerId?: string
|
|
103
|
+
readonly name?: string
|
|
104
|
+
readonly error?: boolean
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type ExecutionContext = {
|
|
108
|
+
readonly method: string
|
|
109
|
+
readonly path: string
|
|
110
|
+
params: Record<string, string>
|
|
111
|
+
locals: Record<string, unknown>
|
|
112
|
+
trace: ExecutionTraceEvent[]
|
|
113
|
+
scope?: unknown
|
|
114
|
+
activeError?: unknown
|
|
115
|
+
[key: string]: unknown
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type HandlerResolver = (slot: number) => PulseHandler | undefined
|
|
119
|
+
|
|
120
|
+
export type ExecuteOptions = {
|
|
121
|
+
readonly ctx?: ExecutionContext
|
|
122
|
+
readonly getHandlerBySlot?: HandlerResolver
|
|
123
|
+
readonly trace?: ExecutionTraceEvent[]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type ExecutionResult = {
|
|
127
|
+
readonly status: ExecutionStatus
|
|
128
|
+
readonly value?: unknown
|
|
129
|
+
readonly error?: unknown
|
|
130
|
+
readonly ctx: ExecutionContext
|
|
131
|
+
readonly trace: readonly ExecutionTraceEvent[]
|
|
132
|
+
readonly completed: boolean
|
|
133
|
+
readonly executedSlots: readonly number[]
|
|
134
|
+
readonly finalIndex: number
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const EXPECTED_HANDLER_SLOTS = ${tsJson(expectedSlots)} as const
|
|
138
|
+
|
|
139
|
+
type AnyEntry = typeof EXECUTION_ENTRIES[number]
|
|
140
|
+
type AnyScope = typeof EXECUTION_SCOPES[number]
|
|
141
|
+
|
|
142
|
+
function slotFor(step: HandlerUse | { readonly slot?: number | null; readonly name?: string; readonly id?: string; readonly handlerId?: string }, phase: ExecutionPhase): number {
|
|
143
|
+
if (!Number.isInteger(step.slot)) {
|
|
144
|
+
throw new Error('Missing handler slot for ' + phase + ' step ' + String(step.name || step.handlerId || step.id || '<unknown>'))
|
|
145
|
+
}
|
|
146
|
+
return step.slot as number
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function handlerIdFor(step: HandlerUse | { readonly id?: string; readonly handlerId?: string }): string | undefined {
|
|
150
|
+
return step.handlerId || step.id
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ensureSync(result: unknown, name: string | undefined): void {
|
|
154
|
+
if (result && typeof (result as { then?: unknown }).then === 'function') {
|
|
155
|
+
throw new Error('PulseWasm v1 execution harness received a Promise from handler ' + String(name || '<unknown>'))
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function traceEvent(kind: ExecutionTraceEvent['kind'], phase: ExecutionPhase, index: number, step?: { readonly slot?: number | null; readonly id?: string; readonly handlerId?: string; readonly name?: string }, error?: boolean): ExecutionTraceEvent {
|
|
160
|
+
return {
|
|
161
|
+
kind,
|
|
162
|
+
phase,
|
|
163
|
+
index,
|
|
164
|
+
slot: step && Number.isInteger(step.slot) ? step.slot as number : undefined,
|
|
165
|
+
handlerId: step ? handlerIdFor(step) : undefined,
|
|
166
|
+
name: step?.name,
|
|
167
|
+
error
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function scopeById(scopeId: number | null | undefined): AnyScope | undefined {
|
|
172
|
+
if (!Number.isInteger(scopeId)) return undefined
|
|
173
|
+
return (EXECUTION_SCOPES as readonly AnyScope[]).find((scope) => scope.id === scopeId)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function mergeParams(target: Record<string, string>, next: Record<string, string>): void {
|
|
177
|
+
for (const [key, value] of Object.entries(next || {})) target[key] = value
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function matchEntryPattern(entry: AnyEntry, requestPath: string): { params: Record<string, string>; rest: string } | null {
|
|
181
|
+
const pattern = (entry as { pattern?: unknown }).pattern
|
|
182
|
+
if (!pattern) return { params: {}, rest: requestPath }
|
|
183
|
+
return matchPathPattern(pattern as never, requestPath)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function createTraceBackedContext(method: string, requestPath: string, extra: Record<string, unknown> = {}): ExecutionContext {
|
|
187
|
+
return {
|
|
188
|
+
...extra,
|
|
189
|
+
method: String(method || '').toUpperCase(),
|
|
190
|
+
path: requestPath,
|
|
191
|
+
params: { ...(extra.params as Record<string, string> || {}) },
|
|
192
|
+
locals: Object.assign({}, extra.locals || {}),
|
|
193
|
+
trace: []
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function createExecutionContext(method: string, requestPath: string, extra: Record<string, unknown> = {}): ExecutionContext {
|
|
198
|
+
return createTraceBackedContext(method, requestPath, extra)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function assertExecutionBindingCoverage(expectedSlots: readonly number[] = EXPECTED_HANDLER_SLOTS): void {
|
|
202
|
+
assertHandlerBindingCoverage(expectedSlots)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function execute(method: string, requestPath: string, options: ExecuteOptions = {}): ExecutionResult {
|
|
206
|
+
const normalizedMethod = String(method || '').toUpperCase()
|
|
207
|
+
const ctx = options.ctx || createExecutionContext(normalizedMethod, requestPath)
|
|
208
|
+
ctx.method = normalizedMethod
|
|
209
|
+
ctx.path = requestPath
|
|
210
|
+
const trace = options.trace || ctx.trace || []
|
|
211
|
+
ctx.trace = trace
|
|
212
|
+
const resolver = options.getHandlerBySlot || getHandlerBySlot
|
|
213
|
+
const executedSlots: number[] = []
|
|
214
|
+
|
|
215
|
+
function done(status: ExecutionStatus, finalIndex: number, value?: unknown, error?: unknown): ExecutionResult {
|
|
216
|
+
return { status, value, error, ctx, trace, completed: status === 'response' || status === 'halt' || status === 'error' || status === 'not-found', executedSlots, finalIndex }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function scan(startIndex: number, activeError?: unknown): ExecutionResult {
|
|
220
|
+
let index = startIndex
|
|
221
|
+
while (index < EXECUTION_ENTRIES.length) {
|
|
222
|
+
const entry = EXECUTION_ENTRIES[index] as AnyEntry
|
|
223
|
+
const kind = (entry as { kind: string }).kind
|
|
224
|
+
const nextIndex = Number((entry as { nextIndex?: number }).nextIndex ?? index + 1)
|
|
225
|
+
|
|
226
|
+
if (activeError !== undefined) {
|
|
227
|
+
if (kind !== 'error') {
|
|
228
|
+
trace.push(traceEvent('skip', 'error', index, undefined, true))
|
|
229
|
+
index = kind === 'mount' ? Number((entry as { parentContinueIndex?: number }).parentContinueIndex ?? nextIndex) : nextIndex
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const handlerUse = (entry as { handler?: HandlerUse }).handler
|
|
234
|
+
if (!handlerUse) {
|
|
235
|
+
index = nextIndex
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
const slot = slotFor(handlerUse, 'error')
|
|
239
|
+
const handler = resolver(slot)
|
|
240
|
+
if (!handler) throw new Error('Missing generated error handler binding for slot ' + slot)
|
|
241
|
+
executedSlots.push(slot)
|
|
242
|
+
ctx.scope = scopeById((entry as { scopeId?: number }).scopeId)
|
|
243
|
+
ctx.activeError = activeError
|
|
244
|
+
trace.push(traceEvent('enter', 'error', index, handlerUse, true))
|
|
245
|
+
let didNext = false
|
|
246
|
+
let nextResult: ExecutionResult | undefined
|
|
247
|
+
const next = (nextError?: unknown): unknown => {
|
|
248
|
+
didNext = true
|
|
249
|
+
trace.push(traceEvent('next', nextError === undefined ? 'normal' : 'error', index, handlerUse, nextError !== undefined))
|
|
250
|
+
nextResult = scan(nextIndex, nextError)
|
|
251
|
+
return nextResult.value
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
const out = handler(activeError, ctx, next)
|
|
255
|
+
ensureSync(out, handlerUse.name || handlerIdFor(handlerUse))
|
|
256
|
+
trace.push(traceEvent('exit', 'error', index, handlerUse, activeError !== undefined))
|
|
257
|
+
if (out !== undefined) return done('response', index, out)
|
|
258
|
+
if (didNext) return nextResult || done('continue', nextIndex)
|
|
259
|
+
index = nextIndex
|
|
260
|
+
continue
|
|
261
|
+
} catch (err) {
|
|
262
|
+
activeError = err
|
|
263
|
+
index = nextIndex
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (kind === 'error') {
|
|
269
|
+
index = nextIndex
|
|
270
|
+
continue
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (kind === 'channel' || kind === 'lifecycle') {
|
|
274
|
+
trace.push(traceEvent('scope', 'normal', index))
|
|
275
|
+
index = nextIndex
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (kind === 'mount') {
|
|
280
|
+
const matched = matchEntryPattern(entry, requestPath)
|
|
281
|
+
if (!matched) {
|
|
282
|
+
trace.push(traceEvent('mount', 'normal', index))
|
|
283
|
+
index = Number((entry as { parentContinueIndex?: number }).parentContinueIndex ?? nextIndex)
|
|
284
|
+
} else {
|
|
285
|
+
mergeParams(ctx.params, matched.params)
|
|
286
|
+
index = Number((entry as { childStartIndex?: number }).childStartIndex ?? nextIndex)
|
|
287
|
+
}
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (kind !== 'use' && kind !== 'route') {
|
|
292
|
+
index = nextIndex
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (kind === 'route' && String((entry as { method?: string }).method || '').toUpperCase() !== normalizedMethod) {
|
|
297
|
+
index = nextIndex
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const matched = matchEntryPattern(entry, requestPath)
|
|
302
|
+
if (!matched) {
|
|
303
|
+
index = nextIndex
|
|
304
|
+
continue
|
|
305
|
+
}
|
|
306
|
+
mergeParams(ctx.params, matched.params)
|
|
307
|
+
const handlerUse = (entry as { handler?: HandlerUse }).handler
|
|
308
|
+
if (!handlerUse) {
|
|
309
|
+
index = nextIndex
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
const slot = slotFor(handlerUse, kind === 'route' ? 'normal' : 'normal')
|
|
313
|
+
const handler = resolver(slot)
|
|
314
|
+
if (!handler) throw new Error('Missing generated handler binding for slot ' + slot)
|
|
315
|
+
executedSlots.push(slot)
|
|
316
|
+
ctx.scope = scopeById((entry as { scopeId?: number }).scopeId)
|
|
317
|
+
trace.push(traceEvent('enter', 'normal', index, handlerUse))
|
|
318
|
+
let didNext = false
|
|
319
|
+
let nextResult: ExecutionResult | undefined
|
|
320
|
+
const next = (err?: unknown): unknown => {
|
|
321
|
+
didNext = true
|
|
322
|
+
trace.push(traceEvent('next', err === undefined ? 'normal' : 'error', index, handlerUse, err !== undefined))
|
|
323
|
+
nextResult = scan(nextIndex, err)
|
|
324
|
+
return nextResult.value
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const out = handler(ctx, next)
|
|
328
|
+
ensureSync(out, handlerUse.name || handlerIdFor(handlerUse))
|
|
329
|
+
trace.push(traceEvent('exit', 'normal', index, handlerUse))
|
|
330
|
+
if (out !== undefined) return done('response', index, out)
|
|
331
|
+
if (didNext) return nextResult || done('continue', nextIndex)
|
|
332
|
+
trace.push(traceEvent('halt', 'normal', index, handlerUse))
|
|
333
|
+
return done('halt', index)
|
|
334
|
+
} catch (err) {
|
|
335
|
+
return scan(nextIndex, err)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (activeError !== undefined) return done('error', index, undefined, activeError)
|
|
339
|
+
return done('not-found', index)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return scan(0, undefined)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function executeMatch(matchResult: DispatchMatch, options: ExecuteOptions = {}): ExecutionResult {
|
|
346
|
+
const requestPath = (matchResult as unknown as { requestPath?: string }).requestPath || matchResult.path
|
|
347
|
+
return execute(matchResult.method, requestPath, options)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function executeLifecycle(matchResult: DispatchMatch, event: 'connect' | 'disconnect', options: ExecuteOptions = {}): ExecutionResult {
|
|
351
|
+
const ctx = options.ctx || createExecutionContext(matchResult.method, (matchResult as unknown as { requestPath?: string }).requestPath || matchResult.path)
|
|
352
|
+
const steps = event === 'connect' ? matchResult.execution.lifecycle.connect : matchResult.execution.lifecycle.disconnect
|
|
353
|
+
const trace = options.trace || ctx.trace || []
|
|
354
|
+
ctx.trace = trace
|
|
355
|
+
const resolver = options.getHandlerBySlot || getHandlerBySlot
|
|
356
|
+
const executedSlots: number[] = []
|
|
357
|
+
for (let i = 0; i < steps.length; i += 1) {
|
|
358
|
+
const step = steps[i]
|
|
359
|
+
const slot = slotFor(step, event === 'connect' ? 'lifecycle:connect' : 'lifecycle:disconnect')
|
|
360
|
+
const handler = resolver(slot)
|
|
361
|
+
if (!handler) throw new Error('Missing generated lifecycle handler binding for slot ' + slot)
|
|
362
|
+
executedSlots.push(slot)
|
|
363
|
+
trace.push(traceEvent('enter', event === 'connect' ? 'lifecycle:connect' : 'lifecycle:disconnect', i, step))
|
|
364
|
+
const out = handler(ctx, () => undefined)
|
|
365
|
+
ensureSync(out, step.name || handlerIdFor(step))
|
|
366
|
+
trace.push(traceEvent('exit', event === 'connect' ? 'lifecycle:connect' : 'lifecycle:disconnect', i, step))
|
|
367
|
+
}
|
|
368
|
+
return { status: 'continue', ctx, trace, completed: true, executedSlots, finalIndex: steps.length }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function resolveChannel(matchResult: DispatchMatch, options: ExecuteOptions = {}): unknown {
|
|
372
|
+
const channel = matchResult.execution.channel
|
|
373
|
+
if (!channel) return undefined
|
|
374
|
+
if (channel.kind === 'static') return channel.value
|
|
375
|
+
|
|
376
|
+
const resolver = options.getHandlerBySlot || getHandlerBySlot
|
|
377
|
+
const slot = slotFor(channel as HandlerUse, 'channel')
|
|
378
|
+
const handler = resolver(slot)
|
|
379
|
+
if (!handler) throw new Error('Missing generated channel handler binding for slot ' + slot)
|
|
380
|
+
const ctx = options.ctx || createExecutionContext(matchResult.method, (matchResult as unknown as { requestPath?: string }).requestPath || matchResult.path)
|
|
381
|
+
const trace = options.trace || ctx.trace || []
|
|
382
|
+
trace.push(traceEvent('channel', 'channel', 0, channel as HandlerUse))
|
|
383
|
+
const result = handler(ctx)
|
|
384
|
+
ensureSync(result, (channel as HandlerUse).name || handlerIdFor(channel as HandlerUse))
|
|
385
|
+
return result
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export { match }
|
|
389
|
+
`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function buildTypeScriptExecutionHarness(dispatchTable, handlerBindings, options = {}) {
|
|
393
|
+
const cwd = options.cwd || process.cwd();
|
|
394
|
+
const diagnostics = [];
|
|
395
|
+
const executionPlan = options.executionPlan;
|
|
396
|
+
|
|
397
|
+
if (!handlerBindings) {
|
|
398
|
+
diagnostics.push({
|
|
399
|
+
pass: 'execution-harness',
|
|
400
|
+
code: 'PULSEWASM_EXECUTION_HARNESS_REQUIRES_HANDLER_BINDINGS',
|
|
401
|
+
severity: 'error',
|
|
402
|
+
message: 'The generated execution harness requires generated handler slot bindings.',
|
|
403
|
+
hint: 'Enable --emit-handler-bindings or use --emit-execution-harness, which implies handler bindings.',
|
|
404
|
+
loc: { file: '<execution-harness>' }
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (!executionPlan) {
|
|
409
|
+
diagnostics.push({
|
|
410
|
+
pass: 'execution-harness',
|
|
411
|
+
code: 'PULSEWASM_EXECUTION_HARNESS_REQUIRES_EXECUTION_PLAN',
|
|
412
|
+
severity: 'error',
|
|
413
|
+
message: 'The Phase 8E execution harness requires execution-plan.json.',
|
|
414
|
+
hint: 'Run the execution-plan pass before generated harness emission.',
|
|
415
|
+
loc: { file: '<execution-harness>' }
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const expectedSlots = collectExpectedSlots(dispatchTable, executionPlan);
|
|
420
|
+
const files = [
|
|
421
|
+
makeFile('generated/execution-plan.ts', buildExecutionPlanSource(executionPlan)),
|
|
422
|
+
makeFile('generated/execution-harness.ts', buildExecutionHarnessSource(expectedSlots))
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
const artifact = normalizeArtifact({
|
|
426
|
+
version: EXECUTION_HARNESS_VERSION,
|
|
427
|
+
generatedBy: options.generatedBy || PACKAGE_VERSION,
|
|
428
|
+
source: dispatchTable?.source,
|
|
429
|
+
dispatchTableVersion: dispatchTable?.version,
|
|
430
|
+
executionPlanVersion: executionPlan?.version,
|
|
431
|
+
handlerBindingsVersion: handlerBindings?.artifact?.version,
|
|
432
|
+
policy: {
|
|
433
|
+
phase: '8E',
|
|
434
|
+
sourceOfTruth: 'execution-plan.json + handler-bindings.json',
|
|
435
|
+
usesOriginalRouter: false,
|
|
436
|
+
requiresHandlerBindings: true,
|
|
437
|
+
defaultExecutionMode: 'entry-scan',
|
|
438
|
+
preservesRouteNext: true,
|
|
439
|
+
preservesNextErrorTransition: true,
|
|
440
|
+
generatedDispatchExecutes: true,
|
|
441
|
+
wasmCodegen: false
|
|
442
|
+
},
|
|
443
|
+
expectedHandlerSlots: expectedSlots,
|
|
444
|
+
output: {
|
|
445
|
+
language: 'typescript',
|
|
446
|
+
module: 'esm',
|
|
447
|
+
files: files.map(({ file, bytes, sha256: hash }) => ({ file, bytes, sha256: hash }))
|
|
448
|
+
},
|
|
449
|
+
summary: {
|
|
450
|
+
entries: executionPlan?.summary?.entries || 0,
|
|
451
|
+
routes: executionPlan?.summary?.routes || 0,
|
|
452
|
+
errors: executionPlan?.summary?.errors || 0,
|
|
453
|
+
scopes: executionPlan?.summary?.scopes || 0,
|
|
454
|
+
expectedHandlerSlots: expectedSlots.length,
|
|
455
|
+
handlerBindings: handlerBindings?.artifact?.summary?.bindings || 0,
|
|
456
|
+
diagnostics: diagnostics.length
|
|
457
|
+
}
|
|
458
|
+
}, cwd);
|
|
459
|
+
|
|
460
|
+
return { version: EXECUTION_HARNESS_VERSION, artifact, files, diagnostics };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
module.exports = {
|
|
464
|
+
EXECUTION_HARNESS_VERSION,
|
|
465
|
+
buildTypeScriptExecutionHarness,
|
|
466
|
+
collectExpectedSlots
|
|
467
|
+
};
|