@pulse-compute/wasm-host-runtime 0.0.0 → 1.0.0-beta.1
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 +46 -1
- package/package.json +44 -6
- package/src/compiler/backend-capabilities.js +372 -0
- package/src/compiler/channel-broadcaster.js +194 -0
- package/src/compiler/compiled-wasm-runtime.js +612 -0
- package/src/compiler/host-runtime-kernel.js +690 -0
- package/src/compiler/index.js +33 -0
- package/src/compiler/json-body.js +454 -0
- package/src/compiler/request-result-headers.js +153 -0
- package/src/compiler/streaming-passthrough.js +212 -0
- package/src/compiler/wasm-host-abi.js +314 -0
- package/src/compiler/wasm-host-bridge.js +984 -0
- package/src/diagnostics.js +3 -0
- package/src/index.js +11 -0
- package/src/runtime/canonical-api-runtime.js +1559 -0
- package/src/runtime/canonical-native-host.js +1173 -0
- package/src/runtime/compiled-wasm-host-runtime-kv.js +32 -0
- package/src/runtime/continuation-registry.js +181 -0
- package/src/runtime/index.js +22 -0
- package/src/runtime/kv-provider.js +56 -0
- package/src/runtime/native-crypto-verifier.js +330 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { getDefaultArtifactsDir } = require('@pulse-compute/wasm-build-support/artifacts-dir');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
|
+
const { PACKAGE_VERSION, normalizeArtifact, normalizeDiagnostic } = require('@pulse-compute/wasm-contracts/diagnostics');
|
|
9
|
+
const {
|
|
10
|
+
STREAMING_PASSTHROUGH_VERSION,
|
|
11
|
+
STREAM_RESULT_ABI_VERSION,
|
|
12
|
+
RESPONSE_PASSTHROUGH_POLICY_VERSION,
|
|
13
|
+
STREAMING_PHASE,
|
|
14
|
+
STREAMING_SMOKE_VERSION,
|
|
15
|
+
streamResultAbiFields,
|
|
16
|
+
responsePassthroughPolicyFields
|
|
17
|
+
} = loadContractsStreamingPassthrough();
|
|
18
|
+
|
|
19
|
+
function loadContractsStreamingPassthrough() {
|
|
20
|
+
try {
|
|
21
|
+
return require('@pulse-compute/wasm-contracts/host/streaming-passthrough');
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
24
|
+
return require('../../../contracts/src/host/streaming-passthrough.js');
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function diagnosticFromParts(parts) {
|
|
31
|
+
return normalizeDiagnostic({ severity: 'error', loc: { file: '<generated>' }, ...parts });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sha256Text(text) { return crypto.createHash('sha256').update(text, 'utf8').digest('hex'); }
|
|
35
|
+
function sha256Buffer(buffer) { return crypto.createHash('sha256').update(buffer).digest('hex'); }
|
|
36
|
+
|
|
37
|
+
function headerValues(headers, name) {
|
|
38
|
+
const lower = String(name).toLowerCase();
|
|
39
|
+
return (headers || []).filter(([key]) => String(key).toLowerCase() === lower).map(([, value]) => value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildMarkdown(artifact) {
|
|
43
|
+
const lines = [];
|
|
44
|
+
lines.push('# PulseWasm Streaming / Response Pass-through ABI v1');
|
|
45
|
+
lines.push('');
|
|
46
|
+
lines.push(`Generated by: \`${artifact.generatedBy}\``);
|
|
47
|
+
lines.push(`Phase: \`${artifact.policy.phase}\``);
|
|
48
|
+
lines.push('');
|
|
49
|
+
lines.push('## Locked positions');
|
|
50
|
+
lines.push('');
|
|
51
|
+
for (const item of artifact.lockedPositions) lines.push(`- ${item}`);
|
|
52
|
+
lines.push('');
|
|
53
|
+
lines.push('## Result kinds');
|
|
54
|
+
lines.push('');
|
|
55
|
+
for (const [name, value] of Object.entries(artifact.resultKinds)) lines.push(`- \`${name} = ${value}\``);
|
|
56
|
+
lines.push('');
|
|
57
|
+
lines.push('## ABI');
|
|
58
|
+
lines.push('');
|
|
59
|
+
for (const item of artifact.streamResultAbi.exports) lines.push(`- \`${item}\``);
|
|
60
|
+
lines.push('');
|
|
61
|
+
lines.push('## Header merge policy');
|
|
62
|
+
lines.push('');
|
|
63
|
+
for (const item of artifact.responsePassthroughPolicy.headerMergeRules) lines.push(`- ${item}`);
|
|
64
|
+
lines.push('');
|
|
65
|
+
lines.push('## Non-goals');
|
|
66
|
+
lines.push('');
|
|
67
|
+
for (const item of artifact.nonGoals) lines.push(`- ${item}`);
|
|
68
|
+
lines.push('');
|
|
69
|
+
return `${lines.join('\n')}\n`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function makeStreamResultAbi({ generatedBy, wasmHostAbi }) {
|
|
73
|
+
return normalizeArtifact({
|
|
74
|
+
version: STREAM_RESULT_ABI_VERSION,
|
|
75
|
+
generatedBy,
|
|
76
|
+
...streamResultAbiFields({ sourceWasmHostAbiVersion: wasmHostAbi?.artifact?.version })
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function makeResponsePassthroughPolicy({ generatedBy }) {
|
|
81
|
+
return normalizeArtifact({
|
|
82
|
+
version: RESPONSE_PASSTHROUGH_POLICY_VERSION,
|
|
83
|
+
generatedBy,
|
|
84
|
+
...responsePassthroughPolicyFields()
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function runStreamingSmoke({ hostRuntimeKernel, nodeAdapter, wasmPath, wasmHostAbi, deploymentPosture }) {
|
|
89
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulsewasm-streaming-'));
|
|
90
|
+
const hostDir = path.join(tempDir, 'host');
|
|
91
|
+
const nodeDir = path.join(tempDir, 'node');
|
|
92
|
+
fs.mkdirSync(hostDir, { recursive: true });
|
|
93
|
+
fs.mkdirSync(nodeDir, { recursive: true });
|
|
94
|
+
fs.writeFileSync(path.join(hostDir, 'host-runtime-kernel.cjs'), hostRuntimeKernel.files.find((file) => file.file === 'generated/host/host-runtime-kernel.cjs').text, 'utf8');
|
|
95
|
+
fs.writeFileSync(path.join(nodeDir, 'node-adapter.cjs'), nodeAdapter.files.find((file) => file.file === 'generated/node/node-adapter.cjs').text, 'utf8');
|
|
96
|
+
const hostModule = require(path.join(hostDir, 'host-runtime-kernel.cjs'));
|
|
97
|
+
const nodeModule = require(path.join(nodeDir, 'node-adapter.cjs'));
|
|
98
|
+
|
|
99
|
+
const observed = { streamHandlerCalled: 0 };
|
|
100
|
+
const handlers = {
|
|
101
|
+
auth(ctx) { ctx.next(); },
|
|
102
|
+
gateway(ctx) { ctx.next(); },
|
|
103
|
+
realtimeHandler(ctx) { ctx.text(200, `realtime:${ctx.param('id')}`); },
|
|
104
|
+
computeHandler(ctx) {
|
|
105
|
+
observed.streamHandlerCalled += 1;
|
|
106
|
+
ctx.responseHeader.delete('Server');
|
|
107
|
+
ctx.responseHeader.set('Cache-Control', 'public, max-age=120');
|
|
108
|
+
ctx.responseHeader.append('Grip-Channel', 'asset:override');
|
|
109
|
+
return {
|
|
110
|
+
kind: 'response',
|
|
111
|
+
response: {
|
|
112
|
+
status: 206,
|
|
113
|
+
headers: [
|
|
114
|
+
['Content-Type', 'application/octet-stream'],
|
|
115
|
+
['Cache-Control', 'public, max-age=60'],
|
|
116
|
+
['Grip-Channel', 'asset'],
|
|
117
|
+
['Grip-Channel', 'asset:42'],
|
|
118
|
+
['Server', 'origin']
|
|
119
|
+
],
|
|
120
|
+
bodyStream: { kind: 'mock-stream', chunks: ['asset:', '42'] }
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
bundleChannel() { return 'bundleChannel'; },
|
|
125
|
+
onConnect() {},
|
|
126
|
+
onDisconnect() {}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const runtime = hostModule.createPulseWasmHostRuntime({ wasmPath, wasmHostAbi, deploymentPosture, handlers, broadcaster: { broadcast() {} } });
|
|
130
|
+
const streamResult = runtime.executeRequest({ method: 'POST', path: '/compute/42' });
|
|
131
|
+
const res = { statusCode: 0, headers: [], body: '', setHeader(name, value) { if (Array.isArray(value)) for (const item of value) this.headers.push([name, item]); else this.headers.push([name, value]); }, write(chunk) { this.body += Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk); }, end(body) { if (body !== undefined) this.write(body); } };
|
|
132
|
+
nodeModule.writeNodeResponse(res, streamResult);
|
|
133
|
+
|
|
134
|
+
const checks = [
|
|
135
|
+
{ name: 'stream_result_kind', expected: 'stream', actual: streamResult.kind },
|
|
136
|
+
{ name: 'stream_result_status', expected: 206, actual: streamResult.status },
|
|
137
|
+
{ name: 'stream_handler_called', expected: 1, actual: observed.streamHandlerCalled },
|
|
138
|
+
{ name: 'stream_body_not_copied_to_wasm', expected: 1, actual: streamResult.body === undefined ? 1 : 0 },
|
|
139
|
+
{ name: 'node_stream_body_forwarded', expected: 'asset:42', actual: res.body },
|
|
140
|
+
{ name: 'backend_content_type_preserved', expected: 'application/octet-stream', actual: headerValues(streamResult.headers, 'Content-Type')[0] },
|
|
141
|
+
{ name: 'cache_control_overridden', expected: 'public, max-age=120', actual: headerValues(streamResult.headers, 'Cache-Control')[0] },
|
|
142
|
+
{ name: 'server_deleted', expected: 0, actual: headerValues(streamResult.headers, 'Server').length },
|
|
143
|
+
{ name: 'grip_channel_repeated_preserved', expected: 3, actual: headerValues(streamResult.headers, 'Grip-Channel').length },
|
|
144
|
+
{ name: 'result_stream_ref_visible', expected: 1, actual: streamResult.responseRef > 0 || streamResult.streamRef > 0 ? 1 : 0 }
|
|
145
|
+
].map((check) => ({ ...check, status: check.actual === check.expected ? 'ok' : 'error', message: check.actual === check.expected ? undefined : `Expected ${check.expected}, got ${check.actual}.` }));
|
|
146
|
+
return { version: STREAMING_SMOKE_VERSION, generatedBy: PACKAGE_VERSION, checks, failedChecks: checks.filter((check) => check.status !== 'ok'), summary: { executed: true, checks: checks.length, failedChecks: checks.filter((check) => check.status !== 'ok').length }, streamResult: { status: streamResult.status, kind: streamResult.kind, headers: streamResult.headers, responseRef: streamResult.responseRef, streamRef: streamResult.streamRef }, nodeResponse: { statusCode: res.statusCode, headers: res.headers, body: res.body } };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildStreamingPassthrough({ wasmHostAbi, wasmHostBridge, hostRuntimeKernel, nodeAdapter, requestResultHeaders }, options = {}) {
|
|
150
|
+
const cwd = options.cwd || process.cwd();
|
|
151
|
+
const outDir = options.outDir || getDefaultArtifactsDir(cwd);
|
|
152
|
+
const generatedBy = options.generatedBy || PACKAGE_VERSION;
|
|
153
|
+
const diagnostics = [];
|
|
154
|
+
if (!wasmHostAbi || !wasmHostAbi.artifact) diagnostics.push(diagnosticFromParts({ phase: 'streaming-passthrough', code: 'PULSEWASM_STREAMING_ABI_REQUIRED', message: 'Phase 12D requires wasm-host-abi.json.', hint: 'Enable --emit-wasm-host-abi.' }));
|
|
155
|
+
if (!wasmHostBridge || !wasmHostBridge.artifact) diagnostics.push(diagnosticFromParts({ phase: 'streaming-passthrough', code: 'PULSEWASM_STREAMING_BRIDGE_REQUIRED', message: 'Phase 12D requires wasm-host-bridge.json.', hint: 'Enable --emit-wasm-host-bridge.' }));
|
|
156
|
+
if (!hostRuntimeKernel || !hostRuntimeKernel.artifact) diagnostics.push(diagnosticFromParts({ phase: 'streaming-passthrough', code: 'PULSEWASM_STREAMING_HOST_RUNTIME_REQUIRED', message: 'Phase 12D requires host-runtime-kernel.json.', hint: 'Enable --emit-host-runtime-kernel.' }));
|
|
157
|
+
if (!nodeAdapter || !nodeAdapter.artifact) diagnostics.push(diagnosticFromParts({ phase: 'streaming-passthrough', code: 'PULSEWASM_STREAMING_NODE_ADAPTER_REQUIRED', message: 'Phase 12D requires node-adapter.json.', hint: 'Enable --emit-node-adapter.' }));
|
|
158
|
+
const wasmFile = path.join(outDir, 'generated/wasm-bridge/pulsewasm-host-bridge.wasm');
|
|
159
|
+
if (!fs.existsSync(wasmFile)) diagnostics.push(diagnosticFromParts({ phase: 'streaming-passthrough', code: 'PULSEWASM_STREAMING_WASM_NOT_FOUND', message: 'Phase 12D expected the host bridge Wasm file to exist.', details: { wasmFile } }));
|
|
160
|
+
|
|
161
|
+
const streamResultAbi = makeStreamResultAbi({ generatedBy, wasmHostAbi });
|
|
162
|
+
const responsePassthroughPolicy = makeResponsePassthroughPolicy({ generatedBy });
|
|
163
|
+
let smoke;
|
|
164
|
+
if (diagnostics.length === 0) {
|
|
165
|
+
try {
|
|
166
|
+
smoke = runStreamingSmoke({ hostRuntimeKernel, nodeAdapter, wasmPath: wasmFile, wasmHostAbi: wasmHostAbi.artifact, deploymentPosture: options.deploymentPosture?.artifact || options.deploymentPosture || { engine: 'wasm' } });
|
|
167
|
+
if (smoke.failedChecks.length > 0) diagnostics.push(diagnosticFromParts({ phase: 'streaming-passthrough', code: 'PULSEWASM_STREAMING_SMOKE_FAILED', message: 'Phase 12D streaming/pass-through smoke failed.', details: { failedChecks: smoke.failedChecks } }));
|
|
168
|
+
} catch (error) {
|
|
169
|
+
diagnostics.push(diagnosticFromParts({ phase: 'streaming-passthrough', code: 'PULSEWASM_STREAMING_SMOKE_EXECUTION_FAILED', message: 'Phase 12D streaming/pass-through smoke execution failed.', details: { message: error && error.message ? error.message : String(error), stack: error && error.stack ? error.stack : undefined } }));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const artifact = normalizeArtifact({
|
|
174
|
+
version: STREAMING_PASSTHROUGH_VERSION,
|
|
175
|
+
generatedBy,
|
|
176
|
+
status: diagnostics.length ? 'error' : 'ok',
|
|
177
|
+
sourceWasmHostAbiVersion: wasmHostAbi?.artifact?.version,
|
|
178
|
+
sourceWasmHostBridgeVersion: wasmHostBridge?.artifact?.version,
|
|
179
|
+
sourceHostRuntimeKernelVersion: hostRuntimeKernel?.artifact?.version,
|
|
180
|
+
sourceNodeAdapterVersion: nodeAdapter?.artifact?.version,
|
|
181
|
+
policy: { phase: STREAMING_PHASE, sourceOfTruth: 'stream-result-abi.json + response-passthrough-policy.json + host runtime stream smoke', platformBindings: false, assetSemantics: false, binaryBufferAbi: false, requestBodyParsing: false },
|
|
182
|
+
lockedPositions: [
|
|
183
|
+
'RESULT_STREAM is implemented as the primary payload pass-through primitive.',
|
|
184
|
+
'RESULT_BINARY remains reserved as an edge-case finite buffer path.',
|
|
185
|
+
'Host owns stream bytes; Wasm owns opaque response/stream refs only.',
|
|
186
|
+
'pulse_result_from_response(ctxRef, responseRef) is the primary pass-through primitive.',
|
|
187
|
+
'pulse_result_stream(ctxRef, statusCode, streamRef) is the secondary explicit stream primitive.',
|
|
188
|
+
'Response pass-through preserves backend headers, applies set/append/delete mutations, and preserves repeated headers.',
|
|
189
|
+
'Request body parsing and binary request parsing remain untouched.'
|
|
190
|
+
],
|
|
191
|
+
resultKinds: streamResultAbi.resultKinds,
|
|
192
|
+
streamResultAbi,
|
|
193
|
+
responsePassthroughPolicy,
|
|
194
|
+
smoke: smoke || { checks: [], failedChecks: [] },
|
|
195
|
+
nonGoals: [
|
|
196
|
+
'No pulse.assets implementation.',
|
|
197
|
+
'No binary request parsing.',
|
|
198
|
+
'No user-authored Uint8Array DSL.',
|
|
199
|
+
'No range requests.',
|
|
200
|
+
'No compression.',
|
|
201
|
+
'No cache metadata engine.',
|
|
202
|
+
'No streaming JSON parser.',
|
|
203
|
+
'No GRIP implementation.',
|
|
204
|
+
'No platform adapters.'
|
|
205
|
+
],
|
|
206
|
+
summary: { streamImplemented: true, binaryReserved: true, smokeExecuted: Boolean(smoke), checks: smoke?.checks?.length || 0, failedChecks: smoke?.failedChecks?.length || 0, failedSmokeChecks: smoke?.failedChecks?.length || 0, hostOwnsStream: true, wasmOwnsBytes: false, assetsImplemented: false, platformBindings: false, nodeAdapterStreamForwarding: Boolean(smoke && smoke.nodeResponse && smoke.nodeResponse.body === 'asset:42'), streamBody: smoke?.nodeResponse?.body, diagnostics: diagnostics.length }
|
|
207
|
+
}, cwd);
|
|
208
|
+
|
|
209
|
+
return { artifact, streamResultAbi, responsePassthroughPolicy, smoke: smoke || { checks: [], failedChecks: [] }, files: [{ file: 'generated/host/streaming-passthrough.md', text: buildMarkdown(artifact) }], diagnostics: diagnostics.map((diag, index) => normalizeDiagnostic(diag, index)) };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = { STREAMING_PASSTHROUGH_VERSION, STREAM_RESULT_ABI_VERSION, RESPONSE_PASSTHROUGH_POLICY_VERSION, STREAMING_SMOKE_VERSION, buildStreamingPassthrough };
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { PACKAGE_VERSION, normalizeArtifact } = require('@pulse-compute/wasm-contracts/diagnostics');
|
|
4
|
+
const {
|
|
5
|
+
ABI_VERSION,
|
|
6
|
+
ABI_PHASE,
|
|
7
|
+
METHOD_CODES,
|
|
8
|
+
RESULT_KINDS,
|
|
9
|
+
NEXT_OUTCOMES,
|
|
10
|
+
buildWasmHostAbiExportGroups,
|
|
11
|
+
flattenWasmHostAbiExports
|
|
12
|
+
} = loadContractsWasmHostAbi();
|
|
13
|
+
|
|
14
|
+
function loadContractsWasmHostAbi() {
|
|
15
|
+
try {
|
|
16
|
+
return require('@pulse-compute/wasm-contracts/host/wasm-host-abi');
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
19
|
+
return require('../../../contracts/src/host/wasm-host-abi.js');
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function roleSignature(role) {
|
|
26
|
+
if (role === 'error') return '(errRef: i32, ctxRef: i32, nextRef: i32) -> void';
|
|
27
|
+
if (role === 'channel') return '(ctxRef: i32) -> i32';
|
|
28
|
+
return '(ctxRef: i32, nextRef: i32) -> void';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function primaryRole(slot) {
|
|
32
|
+
const roles = slot.roles || [];
|
|
33
|
+
if (roles.includes('error')) return 'error';
|
|
34
|
+
if (roles.includes('channel')) return 'channel';
|
|
35
|
+
if (roles.includes('route')) return 'route';
|
|
36
|
+
if (roles.includes('middleware')) return 'middleware';
|
|
37
|
+
if (roles.includes('lifecycle')) return 'lifecycle';
|
|
38
|
+
return roles[0] || 'unknown';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildHandlerImports(dispatchTable) {
|
|
42
|
+
return (dispatchTable.handlerSlots || []).map((slot) => {
|
|
43
|
+
const role = primaryRole(slot);
|
|
44
|
+
return {
|
|
45
|
+
module: 'pulsewasm_handlers',
|
|
46
|
+
name: slot.name,
|
|
47
|
+
slot: slot.slot,
|
|
48
|
+
handlerId: slot.id,
|
|
49
|
+
roles: slot.roles || [],
|
|
50
|
+
primaryRole: role,
|
|
51
|
+
signature: roleSignature(role),
|
|
52
|
+
missingPolicy: 'build-time error; no runtime fallback'
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
function formatParam(param) {
|
|
59
|
+
return `${param.name}: ${param.type}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function functionSignature(item) {
|
|
63
|
+
if (item.returns === 'memory') return `${item.name}: memory`;
|
|
64
|
+
const params = (item.params || []).map(formatParam).join(', ');
|
|
65
|
+
return `${item.name}(${params}) -> ${item.returns || 'void'}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildMarkdown(artifact) {
|
|
69
|
+
const exports = artifact.exports.flatMap((group) => group.functions.map((item) => ({ group: group.name, ...item })));
|
|
70
|
+
const lines = [];
|
|
71
|
+
lines.push('# PulseWasm Wasm Host ABI Contract v4');
|
|
72
|
+
lines.push('');
|
|
73
|
+
lines.push(`Generated by: \`${artifact.generatedBy}\``);
|
|
74
|
+
lines.push(`Phase: \`${artifact.policy.phase}\``);
|
|
75
|
+
lines.push('');
|
|
76
|
+
lines.push('## Status');
|
|
77
|
+
lines.push('');
|
|
78
|
+
lines.push('Locked as a contract artifact. Phase 12D includes streaming / response pass-through ABI contract, but still does not implement WIT or platform bindings.');
|
|
79
|
+
lines.push('');
|
|
80
|
+
lines.push('## Memory and string policy');
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push(`- Encoding: \`${artifact.memory.stringEncoding}\``);
|
|
83
|
+
lines.push(`- Host strings: \`${artifact.memory.hostStringRepresentation}\``);
|
|
84
|
+
lines.push(`- Internal AS strings: \`${artifact.memory.internalStringRepresentation}\``);
|
|
85
|
+
lines.push(`- Ownership: \`${artifact.memory.ownership}\``);
|
|
86
|
+
lines.push('');
|
|
87
|
+
lines.push('## Wasm exports');
|
|
88
|
+
lines.push('');
|
|
89
|
+
for (const item of exports) {
|
|
90
|
+
lines.push(`- \`${functionSignature(item)}\` _(${item.group})_`);
|
|
91
|
+
}
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push('## Handler imports');
|
|
94
|
+
lines.push('');
|
|
95
|
+
for (const item of artifact.imports.handlers) {
|
|
96
|
+
lines.push(`- \`pulsewasm_handlers.${item.name}: ${item.signature}\` — slot ${item.slot}, role ${item.primaryRole}`);
|
|
97
|
+
}
|
|
98
|
+
lines.push('');
|
|
99
|
+
lines.push('## Broadcaster import');
|
|
100
|
+
lines.push('');
|
|
101
|
+
lines.push(`- \`${artifact.imports.broadcaster.module}.${artifact.imports.broadcaster.name}: ${artifact.imports.broadcaster.signature}\``);
|
|
102
|
+
lines.push(`- Required when channels resolve: \`${artifact.imports.broadcaster.requiredWhenChannelsResolve}\``);
|
|
103
|
+
lines.push('');
|
|
104
|
+
lines.push('## Defaults');
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push(`- Normal fallthrough: \`${artifact.defaults.normalFallthrough.statusCode}\``);
|
|
107
|
+
lines.push(`- Error fallthrough: \`${artifact.defaults.errorFallthrough.statusCode}\``);
|
|
108
|
+
lines.push(`- Contract violation: \`${artifact.defaults.contractViolation.statusCode} ${artifact.defaults.contractViolation.code}\``);
|
|
109
|
+
lines.push('');
|
|
110
|
+
lines.push('## Non-goals');
|
|
111
|
+
lines.push('');
|
|
112
|
+
for (const item of artifact.nonGoals) lines.push(`- ${item}`);
|
|
113
|
+
lines.push('');
|
|
114
|
+
return `${lines.join('\n')}\n`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildWasmHostAbi(dispatchTable, executionPlan, options = {}) {
|
|
118
|
+
const cwd = options.cwd || process.cwd();
|
|
119
|
+
const generatedBy = options.generatedBy || PACKAGE_VERSION;
|
|
120
|
+
const exportGroups = buildWasmHostAbiExportGroups();
|
|
121
|
+
const flatExports = flattenWasmHostAbiExports(exportGroups);
|
|
122
|
+
const handlerImports = buildHandlerImports(dispatchTable || {});
|
|
123
|
+
const channelHandlers = Number(dispatchTable?.summary?.channelHandlers || 0);
|
|
124
|
+
const lifecycleLinks = Number(dispatchTable?.summary?.lifecycleLinks || 0);
|
|
125
|
+
|
|
126
|
+
const artifact = normalizeArtifact({
|
|
127
|
+
version: ABI_VERSION,
|
|
128
|
+
generatedBy,
|
|
129
|
+
source: dispatchTable?.source,
|
|
130
|
+
policy: {
|
|
131
|
+
phase: ABI_PHASE,
|
|
132
|
+
status: 'locked-contract',
|
|
133
|
+
sourceOfTruth: [
|
|
134
|
+
'pulsewasm-runtime-abi-boundary-contract-v1.md',
|
|
135
|
+
'dispatch-table.json',
|
|
136
|
+
'execution-plan.json'
|
|
137
|
+
],
|
|
138
|
+
contractOnly: true,
|
|
139
|
+
jsonBodyAbi: true,
|
|
140
|
+
streamResultAbi: true,
|
|
141
|
+
hostBridgeImplemented: false,
|
|
142
|
+
wasmExecutionChanged: false,
|
|
143
|
+
platformBindings: false,
|
|
144
|
+
wit: false,
|
|
145
|
+
wasiHttp: false,
|
|
146
|
+
dynamicHandlerLookup: false
|
|
147
|
+
},
|
|
148
|
+
primitives: {
|
|
149
|
+
Pointer: 'i32',
|
|
150
|
+
ByteLength: 'i32',
|
|
151
|
+
MethodCode: 'i32',
|
|
152
|
+
StatusCode: 'i32',
|
|
153
|
+
StringRef: 'i32',
|
|
154
|
+
HostResponseRef: 'i32',
|
|
155
|
+
HostStreamRef: 'i32',
|
|
156
|
+
ErrorRef: 'i32',
|
|
157
|
+
ResultRef: 'i32',
|
|
158
|
+
ChannelsRef: 'i32',
|
|
159
|
+
CtxRef: 'i32',
|
|
160
|
+
NextRef: 'i32'
|
|
161
|
+
},
|
|
162
|
+
memory: {
|
|
163
|
+
exportedMemory: 'memory',
|
|
164
|
+
stringEncoding: 'utf8',
|
|
165
|
+
hostStringRepresentation: 'ptr + byteLen',
|
|
166
|
+
internalStringRepresentation: 'AssemblyScript string',
|
|
167
|
+
ownership: 'host writes through pulse_alloc; wasm owns refs returned by ctx/result/error/channel operations until reset',
|
|
168
|
+
resetInvalidatesRefs: true,
|
|
169
|
+
componentModelDeferred: true
|
|
170
|
+
},
|
|
171
|
+
headers: {
|
|
172
|
+
firstClass: true,
|
|
173
|
+
storage: 'ordered repeated header entries with ASCII case-insensitive lookup',
|
|
174
|
+
requestHeaders: 'read-only to handlers; host/adaptor seeded',
|
|
175
|
+
responseHeaders: 'mutable through set/append/delete; exported by ordered iteration',
|
|
176
|
+
gripFraming: 'expressed through normal response headers such as Grip-Hold and repeated Grip-Channel',
|
|
177
|
+
bodyPolicy: 'request body deferred; RESULT_TEXT, RESULT_EMPTY, RESULT_JSON text helper, and RESULT_STREAM implemented; RESULT_BINARY reserved'
|
|
178
|
+
},
|
|
179
|
+
constants: {
|
|
180
|
+
methodCodes: METHOD_CODES,
|
|
181
|
+
resultKinds: RESULT_KINDS,
|
|
182
|
+
nextOutcomes: NEXT_OUTCOMES
|
|
183
|
+
},
|
|
184
|
+
exports: Object.entries(exportGroups).map(([name, functions]) => ({ name, functions })),
|
|
185
|
+
exportCount: flatExports.length,
|
|
186
|
+
imports: {
|
|
187
|
+
handlers: handlerImports,
|
|
188
|
+
broadcaster: {
|
|
189
|
+
module: 'pulsewasm_broadcaster',
|
|
190
|
+
name: 'broadcast',
|
|
191
|
+
signature: '(channelsRef: i32, ctxRef: i32) -> i32',
|
|
192
|
+
returnPolicy: '0 = ok; non-zero = ErrorRef',
|
|
193
|
+
requiredWhenChannelsResolve: true,
|
|
194
|
+
missingBroadcasterFailure: {
|
|
195
|
+
code: 'PULSEWASM_BROADCASTER_MISSING',
|
|
196
|
+
message: 'Channel routing resolved channels, but no broadcaster adaptor was provided.',
|
|
197
|
+
statusCode: 500
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
importCount: handlerImports.length + 1,
|
|
202
|
+
ctxOperations: {
|
|
203
|
+
param: 'pulse_ctx_param(ctxRef, namePtr, nameLen) -> StringRef',
|
|
204
|
+
requestHeaderFirst: 'pulse_request_header_first(ctxRef, namePtr, nameLen) -> StringRef',
|
|
205
|
+
requestHeaderCount: 'pulse_request_header_count(ctxRef, namePtr, nameLen) -> i32',
|
|
206
|
+
requestHeaderAt: 'pulse_request_header_at(ctxRef, namePtr, nameLen, index) -> StringRef',
|
|
207
|
+
responseHeaderSet: 'pulse_response_header_set(ctxRef, namePtr, nameLen, valuePtr, valueLen)',
|
|
208
|
+
responseHeaderAppend: 'pulse_response_header_append(ctxRef, namePtr, nameLen, valuePtr, valueLen)',
|
|
209
|
+
responseHeaderDelete: 'pulse_response_header_delete(ctxRef, namePtr, nameLen)',
|
|
210
|
+
stateGet: 'pulse_ctx_state_get(ctxRef, keyPtr, keyLen) -> StringRef',
|
|
211
|
+
stateSet: 'pulse_ctx_state_set(ctxRef, keyPtr, keyLen, valuePtr, valueLen)',
|
|
212
|
+
resultText: 'pulse_result_text(ctxRef, statusCode, bodyPtr, bodyLen)',
|
|
213
|
+
resultJsonText: 'pulse_result_json_text(ctxRef, statusCode, jsonPtr, jsonLen)',
|
|
214
|
+
resultEmpty: 'pulse_result_empty(ctxRef, statusCode)',
|
|
215
|
+
next: 'pulse_next(ctxRef)',
|
|
216
|
+
nextError: 'pulse_next_error(ctxRef, errorRef)'
|
|
217
|
+
},
|
|
218
|
+
errorAbi: {
|
|
219
|
+
shape: {
|
|
220
|
+
codeRef: 'StringRef',
|
|
221
|
+
messageRef: 'StringRef',
|
|
222
|
+
statusCode: 'i32',
|
|
223
|
+
causeRef: 'ErrorRef | 0'
|
|
224
|
+
},
|
|
225
|
+
constructors: ['pulse_error', 'pulse_error_with_cause'],
|
|
226
|
+
throwRejected: true,
|
|
227
|
+
tryCatchRejected: true,
|
|
228
|
+
cleanTransition: 'pulse_next_error(ctxRef, errorRef)'
|
|
229
|
+
},
|
|
230
|
+
resultAbi: {
|
|
231
|
+
shape: {
|
|
232
|
+
statusCode: 'i32',
|
|
233
|
+
bodyRef: 'StringRef | BytesRef | 0',
|
|
234
|
+
headersRef: 'HeadersRef | 0',
|
|
235
|
+
kind: 'i32'
|
|
236
|
+
},
|
|
237
|
+
implemented: ['RESULT_TEXT', 'RESULT_JSON', 'RESULT_EMPTY', 'RESULT_STREAM'],
|
|
238
|
+
declaredReserved: ['RESULT_BINARY']
|
|
239
|
+
},
|
|
240
|
+
lifecycle: {
|
|
241
|
+
explicitEntrypoints: ['pulse_execute_connect', 'pulse_execute_disconnect'],
|
|
242
|
+
matchingInput: 'methodCode + full request path',
|
|
243
|
+
hiddenInsideRequestDispatch: false,
|
|
244
|
+
lifecycleLinks
|
|
245
|
+
},
|
|
246
|
+
mountPolicy: {
|
|
247
|
+
canonicalPathInput: 'full request path',
|
|
248
|
+
semanticRelativeTrimming: false,
|
|
249
|
+
continuation: 'execution-plan childStartIndex / parentContinueIndex / nextIndex'
|
|
250
|
+
},
|
|
251
|
+
streaming: {
|
|
252
|
+
resultStreamImplemented: true,
|
|
253
|
+
resultBinaryReserved: true,
|
|
254
|
+
wasmOwnsBytes: false,
|
|
255
|
+
hostOwnsStream: true,
|
|
256
|
+
primaryPrimitive: 'pulse_result_from_response(ctxRef, responseRef)',
|
|
257
|
+
secondaryPrimitive: 'pulse_result_stream(ctxRef, statusCode, streamRef)',
|
|
258
|
+
headerMergePolicy: 'backend response headers preserved, response header set overrides, append adds, delete removes by case-insensitive name'
|
|
259
|
+
},
|
|
260
|
+
defaults: {
|
|
261
|
+
normalFallthrough: { statusCode: 404, kind: 'RESULT_EMPTY' },
|
|
262
|
+
errorFallthrough: { statusCode: 500, kind: 'RESULT_TEXT' },
|
|
263
|
+
contractViolation: {
|
|
264
|
+
statusCode: 500,
|
|
265
|
+
code: 'PULSEWASM_NO_RESULT',
|
|
266
|
+
message: 'Handler halted without result or continuation.'
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
debugProductionPolicy: {
|
|
270
|
+
debugMayInclude: ['route stable IDs', 'handler IDs', 'handler names', 'diagnostic strings', 'artifact version metadata'],
|
|
271
|
+
productionShouldStripTo: ['numeric route IDs', 'method codes', 'path segment tables', 'param tables', 'handler slots', 'scope tables', 'entry transition tables', 'minimal matching strings'],
|
|
272
|
+
productionStrippingImplemented: false
|
|
273
|
+
},
|
|
274
|
+
nonGoals: [
|
|
275
|
+
'No WIT shape yet',
|
|
276
|
+
'No wasi:http binding shape yet',
|
|
277
|
+
'No platform adapter yet',
|
|
278
|
+
'No high-level Pulse wrapper port',
|
|
279
|
+
'No schema-generated JSON parser implementation yet',
|
|
280
|
+
'No binary body ABI implementation yet',
|
|
281
|
+
'No async strategy',
|
|
282
|
+
'No production memory optimizer'
|
|
283
|
+
],
|
|
284
|
+
summary: {
|
|
285
|
+
exports: flatExports.length,
|
|
286
|
+
handlerImports: handlerImports.length,
|
|
287
|
+
imports: handlerImports.length + 1,
|
|
288
|
+
headerExports: exportGroups.headers.length,
|
|
289
|
+
jsonBodyExports: exportGroups.jsonBody.length,
|
|
290
|
+
routes: dispatchTable?.summary?.routes || 0,
|
|
291
|
+
executionEntries: executionPlan?.summary?.entries || 0,
|
|
292
|
+
channelHandlers,
|
|
293
|
+
broadcasterRequiredWhenChannelsResolve: channelHandlers > 0,
|
|
294
|
+
lifecycleLinks,
|
|
295
|
+
contractOnly: true,
|
|
296
|
+
jsonBodyAbi: true
|
|
297
|
+
}
|
|
298
|
+
}, cwd);
|
|
299
|
+
|
|
300
|
+
const markdown = buildMarkdown(artifact);
|
|
301
|
+
return {
|
|
302
|
+
artifact,
|
|
303
|
+
markdown,
|
|
304
|
+
files: [
|
|
305
|
+
{ file: 'generated/host/wasm-host-abi.md', text: markdown }
|
|
306
|
+
],
|
|
307
|
+
diagnostics: []
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = {
|
|
312
|
+
ABI_VERSION,
|
|
313
|
+
buildWasmHostAbi
|
|
314
|
+
};
|