@pulse-compute/wasm-host-runtime 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 +47 -1
- package/package.json +46 -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 +1569 -0
- package/src/runtime/canonical-native-host.js +1187 -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 +340 -0
package/README.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
1
|
# @pulse-compute/wasm-host-runtime
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<!-- pulse-package-status:start -->
|
|
4
|
+
> **Support tier:** Implementation/transitive surface<br>
|
|
5
|
+
> **Audience:** Pulse host-runtime and provider maintainers.<br>
|
|
6
|
+
> **Install directly:** No for application projects; it is installed transitively.<br>
|
|
7
|
+
> **Supported entry points:** `None for application authors.`<br>
|
|
8
|
+
> **Stability:** Internal compiler/runtime interface synchronized with this release set.<br>
|
|
9
|
+
> **npm:** [`@pulse-compute/wasm-host-runtime`](https://www.npmjs.com/package/@pulse-compute/wasm-host-runtime)<br>
|
|
10
|
+
> **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.2/packages/implementation-packages/)
|
|
11
|
+
>
|
|
12
|
+
> This release-status block is generated from the synchronized `Pulse 1.0.0-beta.2` package policy.
|
|
13
|
+
<!-- pulse-package-status:end -->
|
|
14
|
+
|
|
15
|
+
Package-owned PulseWasm host-runtime build-time builders and local runtime helpers.
|
|
16
|
+
|
|
17
|
+
## Runtime surface
|
|
18
|
+
|
|
19
|
+
The package root re-exports the package-owned compiler and runtime surfaces:
|
|
20
|
+
|
|
21
|
+
- `compiler` / `@pulse-compute/wasm-host-runtime/compiler`
|
|
22
|
+
- `runtime` / `@pulse-compute/wasm-host-runtime/runtime`
|
|
23
|
+
|
|
24
|
+
## Compiler/build-time surface
|
|
25
|
+
|
|
26
|
+
Host-runtime builders now live in this package instead of `@pulse-compute/wasm-compiler` internals:
|
|
27
|
+
|
|
28
|
+
- `wasmHostAbi`
|
|
29
|
+
- `wasmHostBridge`
|
|
30
|
+
- `requestResultHeaders`
|
|
31
|
+
- `channelBroadcaster`
|
|
32
|
+
- `jsonBody`
|
|
33
|
+
- `backendCapabilities`
|
|
34
|
+
- `hostRuntimeKernel`
|
|
35
|
+
- `streamingPassthrough`
|
|
36
|
+
- `compiledWasmRuntime`
|
|
37
|
+
|
|
38
|
+
The compiler orchestrator imports these builders from `@pulse-compute/wasm-host-runtime/compiler`.
|
|
39
|
+
|
|
40
|
+
## Runtime helpers
|
|
41
|
+
|
|
42
|
+
Local runtime helpers are exposed under `@pulse-compute/wasm-host-runtime/runtime`:
|
|
43
|
+
|
|
44
|
+
- `compiledWasmHostRuntimeKv`
|
|
45
|
+
- `kvProvider`
|
|
46
|
+
|
|
47
|
+
## Dependencies
|
|
48
|
+
|
|
49
|
+
This package depends on contracts and build-support. It must not depend on `@pulse-compute/wasm-compiler`.
|
package/package.json
CHANGED
|
@@ -1,15 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pulse-compute/wasm-host-runtime",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Inert namespace bootstrap for Pulse.",
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
5
4
|
"license": "Apache-2.0",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": "^22.14.0 || ^24.0.0"
|
|
7
|
+
},
|
|
8
|
+
"description": "Platform-neutral PulseWasm host runtime package-owned builders and runtime helpers.",
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"main": "./src/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./src/index.js",
|
|
13
|
+
"./compiler": "./src/compiler/index.js",
|
|
14
|
+
"./compiler/host-runtime-kernel": "./src/compiler/host-runtime-kernel.js",
|
|
15
|
+
"./compiler/wasm-host-abi": "./src/compiler/wasm-host-abi.js",
|
|
16
|
+
"./compiler/wasm-host-bridge": "./src/compiler/wasm-host-bridge.js",
|
|
17
|
+
"./compiler/request-result-headers": "./src/compiler/request-result-headers.js",
|
|
18
|
+
"./compiler/channel-broadcaster": "./src/compiler/channel-broadcaster.js",
|
|
19
|
+
"./compiler/streaming-passthrough": "./src/compiler/streaming-passthrough.js",
|
|
20
|
+
"./compiler/json-body": "./src/compiler/json-body.js",
|
|
21
|
+
"./compiler/backend-capabilities": "./src/compiler/backend-capabilities.js",
|
|
22
|
+
"./compiler/compiled-wasm-runtime": "./src/compiler/compiled-wasm-runtime.js",
|
|
23
|
+
"./runtime": "./src/runtime/index.js",
|
|
24
|
+
"./runtime/compiled-wasm-host-runtime-kv": "./src/runtime/compiled-wasm-host-runtime-kv.js",
|
|
25
|
+
"./runtime/kv-provider": "./src/runtime/kv-provider.js",
|
|
26
|
+
"./runtime/continuation-registry": "./src/runtime/continuation-registry.js",
|
|
27
|
+
"./runtime/canonical-api-runtime": "./src/runtime/canonical-api-runtime.js",
|
|
28
|
+
"./runtime/canonical-native-host": "./src/runtime/canonical-native-host.js"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@pulse-compute/crypto": "1.0.0-beta.2",
|
|
32
|
+
"@pulse-compute/runtime": "1.0.0-beta.2",
|
|
33
|
+
"@pulse-compute/wasm-build-support": "1.0.0-beta.2",
|
|
34
|
+
"@pulse-compute/wasm-contracts": "1.0.0-beta.2",
|
|
35
|
+
"@pulse-compute/wasm-schema-json": "1.0.0-beta.2"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
6
40
|
"files": [
|
|
41
|
+
"src",
|
|
7
42
|
"README.md",
|
|
8
43
|
"LICENSE",
|
|
9
44
|
"NOTICE"
|
|
10
45
|
],
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/pulse-compute/pulse.git",
|
|
49
|
+
"directory": "wasm/packages/host-runtime"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://pulsecompute.io/v1.0.0-beta.2/packages/implementation-packages/",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/pulse-compute/pulse/issues"
|
|
14
54
|
}
|
|
15
|
-
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { PACKAGE_VERSION, normalizeArtifact, normalizeDiagnostic } = require('@pulse-compute/wasm-contracts/diagnostics');
|
|
4
|
+
const { selectRuntimeEngine } = loadContractsJsonBody();
|
|
5
|
+
const {
|
|
6
|
+
BACKEND_CAPABILITIES_VERSION,
|
|
7
|
+
BACKEND_CAPABILITIES_PHASE,
|
|
8
|
+
DEFAULT_BACKEND_TIMEOUT_MS,
|
|
9
|
+
METHOD_NAMES,
|
|
10
|
+
normalizeBackendMethods
|
|
11
|
+
} = loadContractsBackendCapabilities();
|
|
12
|
+
|
|
13
|
+
function loadContractsJsonBody() {
|
|
14
|
+
try {
|
|
15
|
+
return require('@pulse-compute/wasm-contracts/host/json-body');
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
18
|
+
return require('../../../contracts/src/host/json-body.js');
|
|
19
|
+
}
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function loadContractsBackendCapabilities() {
|
|
25
|
+
try {
|
|
26
|
+
return require('@pulse-compute/wasm-contracts/host/backend-capabilities');
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
29
|
+
return require('../../../contracts/src/host/backend-capabilities.js');
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function plainObject(value) {
|
|
36
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function configArtifact(options = {}) {
|
|
40
|
+
return options.resolvedConfig?.artifact || options.resolvedConfig || {};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isEnvArtifact(value) {
|
|
44
|
+
return value && typeof value === 'object' && typeof value.$env === 'string';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeDiagnostic(code, message, hint, details) {
|
|
48
|
+
return normalizeDiagnostic({
|
|
49
|
+
severity: 'error',
|
|
50
|
+
phase: 'backend-capabilities',
|
|
51
|
+
code,
|
|
52
|
+
message,
|
|
53
|
+
hint,
|
|
54
|
+
details,
|
|
55
|
+
loc: { file: '<config-or-handler>' }
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sourceDescriptor(value, options = {}) {
|
|
60
|
+
if (options.envRef) {
|
|
61
|
+
return {
|
|
62
|
+
source: 'env',
|
|
63
|
+
name: options.envRef.name,
|
|
64
|
+
secret: Boolean(options.envRef.secret),
|
|
65
|
+
redacted: Boolean(options.envRef.redacted),
|
|
66
|
+
resolved: Boolean(options.envRef.resolved),
|
|
67
|
+
compiledIntoBinary: false
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (isEnvArtifact(value)) {
|
|
71
|
+
return {
|
|
72
|
+
source: 'env',
|
|
73
|
+
name: value.$env,
|
|
74
|
+
secret: Boolean(value.secret),
|
|
75
|
+
redacted: Boolean(value.redacted),
|
|
76
|
+
resolved: Boolean(value.resolved),
|
|
77
|
+
compiledIntoBinary: false
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (value && typeof value === 'object' && typeof value.fromEnv === 'string') {
|
|
81
|
+
return {
|
|
82
|
+
source: 'env',
|
|
83
|
+
name: value.fromEnv,
|
|
84
|
+
secret: options.secret !== false,
|
|
85
|
+
redacted: options.secret !== false,
|
|
86
|
+
resolved: false,
|
|
87
|
+
compiledIntoBinary: false
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (typeof value === 'string') {
|
|
91
|
+
return {
|
|
92
|
+
source: 'literal',
|
|
93
|
+
value,
|
|
94
|
+
secret: Boolean(options.secret),
|
|
95
|
+
redacted: Boolean(options.secret),
|
|
96
|
+
compiledIntoBinary: !options.secret
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return { source: 'unsupported', value: value === undefined ? null : value, compiledIntoBinary: false };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function findEnvRefForTail(envRefByPath, tailSegments) {
|
|
103
|
+
const tail = tailSegments.join('.');
|
|
104
|
+
for (const [pathKey, ref] of envRefByPath.entries()) {
|
|
105
|
+
if (pathKey === tail || pathKey.endsWith('.' + tail)) return ref;
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeHeaderConfig(headers, backendKey, envRefByPath) {
|
|
111
|
+
const out = [];
|
|
112
|
+
const obj = plainObject(headers);
|
|
113
|
+
for (const [name, raw] of Object.entries(obj)) {
|
|
114
|
+
const envRef = findEnvRefForTail(envRefByPath, ['backends', backendKey, 'headers', name]) || findEnvRefForTail(envRefByPath, ['capabilities', 'backends', backendKey, 'headers', name]);
|
|
115
|
+
const descriptor = sourceDescriptor(raw, { secret: /authorization|token|secret|key|password/i.test(name), envRef });
|
|
116
|
+
const item = { ...descriptor, name };
|
|
117
|
+
if (descriptor.source === 'env' && descriptor.name) item.env = descriptor.name;
|
|
118
|
+
out.push(item);
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeBackends(config) {
|
|
124
|
+
const backends = [];
|
|
125
|
+
const diagnostics = [];
|
|
126
|
+
const envRefByPath = new Map();
|
|
127
|
+
for (const ref of config.env?.references || []) {
|
|
128
|
+
if (Array.isArray(ref.path)) envRefByPath.set(ref.path.join('.'), ref);
|
|
129
|
+
}
|
|
130
|
+
const runtime = plainObject(config.runtime);
|
|
131
|
+
const capabilities = plainObject(runtime.capabilities);
|
|
132
|
+
const rawBackends = plainObject(capabilities.backends || runtime.backends || config.backends);
|
|
133
|
+
for (const [key, raw] of Object.entries(rawBackends)) {
|
|
134
|
+
const value = plainObject(raw);
|
|
135
|
+
const allowedMethods = normalizeBackendMethods(value.allowedMethods);
|
|
136
|
+
const timeoutMs = value.timeoutMs === undefined || value.timeoutMs === null ? DEFAULT_BACKEND_TIMEOUT_MS : Number(value.timeoutMs);
|
|
137
|
+
const backend = {
|
|
138
|
+
key,
|
|
139
|
+
baseUrl: sourceDescriptor(value.baseUrl, { secret: false, envRef: findEnvRefForTail(envRefByPath, ['backends', key, 'baseUrl']) || findEnvRefForTail(envRefByPath, ['capabilities', 'backends', key, 'baseUrl']) }),
|
|
140
|
+
allowedMethods,
|
|
141
|
+
timeoutMs,
|
|
142
|
+
headers: normalizeHeaderConfig(value.headers, key, envRefByPath),
|
|
143
|
+
policy: {
|
|
144
|
+
declared: true,
|
|
145
|
+
staticKey: true,
|
|
146
|
+
secretsCompiledIntoBinary: false
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
if (!key || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
150
|
+
diagnostics.push(makeDiagnostic(
|
|
151
|
+
'PULSEWASM_BACKEND_KEY_INVALID',
|
|
152
|
+
`Backend key ${JSON.stringify(key)} is not a valid static identifier-like key.`,
|
|
153
|
+
'Use keys like usersApi or payments.',
|
|
154
|
+
{ key }
|
|
155
|
+
));
|
|
156
|
+
}
|
|
157
|
+
if (backend.baseUrl.source === 'unsupported') {
|
|
158
|
+
diagnostics.push(makeDiagnostic(
|
|
159
|
+
'PULSEWASM_BACKEND_BASE_URL_INVALID',
|
|
160
|
+
`Backend ${key} must declare a literal or env-backed baseUrl.`,
|
|
161
|
+
'Use baseUrl: env("USERS_API_BASE_URL") or a literal URL.',
|
|
162
|
+
{ key }
|
|
163
|
+
));
|
|
164
|
+
}
|
|
165
|
+
if (allowedMethods.length === 0) {
|
|
166
|
+
diagnostics.push(makeDiagnostic(
|
|
167
|
+
'PULSEWASM_BACKEND_METHODS_REQUIRED',
|
|
168
|
+
`Backend ${key} must declare allowedMethods.`,
|
|
169
|
+
'Use allowedMethods: ["GET", "POST"].',
|
|
170
|
+
{ key }
|
|
171
|
+
));
|
|
172
|
+
}
|
|
173
|
+
for (const method of allowedMethods) {
|
|
174
|
+
if (!METHOD_NAMES.has(method)) {
|
|
175
|
+
diagnostics.push(makeDiagnostic(
|
|
176
|
+
'PULSEWASM_BACKEND_METHOD_INVALID',
|
|
177
|
+
`Backend ${key} declares unsupported method ${JSON.stringify(method)}.`,
|
|
178
|
+
'Use standard uppercase HTTP methods.',
|
|
179
|
+
{ key, method }
|
|
180
|
+
));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || Math.floor(timeoutMs) !== timeoutMs) {
|
|
184
|
+
diagnostics.push(makeDiagnostic(
|
|
185
|
+
'PULSEWASM_BACKEND_TIMEOUT_INVALID',
|
|
186
|
+
`Backend ${key} timeoutMs must be a positive integer.`,
|
|
187
|
+
`Use a positive timeout such as ${DEFAULT_BACKEND_TIMEOUT_MS}.`,
|
|
188
|
+
{ key, timeoutMs }
|
|
189
|
+
));
|
|
190
|
+
}
|
|
191
|
+
backends.push(backend);
|
|
192
|
+
}
|
|
193
|
+
return { backends, diagnostics };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function collectFetchUsages(handlerEval) {
|
|
197
|
+
const usages = [];
|
|
198
|
+
const items = [];
|
|
199
|
+
for (const handler of handlerEval?.handlers || []) items.push(handler);
|
|
200
|
+
for (const dep of handlerEval?.dependencyEvaluations || []) items.push(dep);
|
|
201
|
+
for (const evaluation of items) {
|
|
202
|
+
for (const fetch of evaluation.fetches || []) {
|
|
203
|
+
usages.push({
|
|
204
|
+
handlerId: evaluation.handlerId,
|
|
205
|
+
handler: evaluation.handlerName,
|
|
206
|
+
roles: evaluation.roles || [],
|
|
207
|
+
backend: fetch.backend,
|
|
208
|
+
method: fetch.method,
|
|
209
|
+
path: fetch.path,
|
|
210
|
+
static: Boolean(fetch.static),
|
|
211
|
+
loc: fetch.loc
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return usages;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function validateUsages(usages, backends, engine) {
|
|
219
|
+
const diagnostics = [];
|
|
220
|
+
const byKey = new Map(backends.map((backend) => [backend.key, backend]));
|
|
221
|
+
for (const usage of usages) {
|
|
222
|
+
if (!usage.backend) continue;
|
|
223
|
+
const backend = byKey.get(usage.backend);
|
|
224
|
+
if (!backend) {
|
|
225
|
+
diagnostics.push(makeDiagnostic(
|
|
226
|
+
'PULSEWASM_UNDECLARED_BACKEND',
|
|
227
|
+
`Handler ${usage.handler} uses undeclared backend ${JSON.stringify(usage.backend)}.`,
|
|
228
|
+
'Declare the backend in defineConfig.backends before using ctx.fetch().',
|
|
229
|
+
usage
|
|
230
|
+
));
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (usage.method && !backend.allowedMethods.includes(usage.method)) {
|
|
234
|
+
diagnostics.push(makeDiagnostic(
|
|
235
|
+
'PULSEWASM_BACKEND_METHOD_NOT_ALLOWED',
|
|
236
|
+
`Handler ${usage.handler} uses ${usage.method} for backend ${usage.backend}, but it is not allowed.`,
|
|
237
|
+
`Add ${usage.method} to defineConfig.backends.${usage.backend}.allowedMethods or use an allowed method.`,
|
|
238
|
+
{ usage, allowedMethods: backend.allowedMethods }
|
|
239
|
+
));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (engine.engine === 'wasm' && usages.length > 0) {
|
|
243
|
+
diagnostics.push(makeDiagnostic(
|
|
244
|
+
'PULSEWASM_FETCH_EFFECT_UNSUPPORTED',
|
|
245
|
+
'ctx.fetch requires the Wasm host effect runtime, which is not enabled yet.',
|
|
246
|
+
'Use runtime.engine = "js" manually, remove ctx.fetch from Wasm-targeted handlers, or wait for the future Wasm fetch-effect runtime.',
|
|
247
|
+
{ usages: usages.map((usage) => ({ handler: usage.handler, backend: usage.backend, method: usage.method, path: usage.path })) }
|
|
248
|
+
));
|
|
249
|
+
}
|
|
250
|
+
return diagnostics;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function buildMarkdown(artifact) {
|
|
254
|
+
const lines = [];
|
|
255
|
+
lines.push('# PulseWasm Backend Capability / ctx.fetch Contract v1');
|
|
256
|
+
lines.push('');
|
|
257
|
+
lines.push(`Generated by: \`${artifact.generatedBy}\``);
|
|
258
|
+
lines.push(`Phase: \`${artifact.policy.phase}\``);
|
|
259
|
+
lines.push('');
|
|
260
|
+
lines.push('## Locked positions');
|
|
261
|
+
lines.push('');
|
|
262
|
+
for (const item of artifact.lockedPositions) lines.push(`- ${item}`);
|
|
263
|
+
lines.push('');
|
|
264
|
+
lines.push('## Backends');
|
|
265
|
+
lines.push('');
|
|
266
|
+
if (artifact.backends.length === 0) lines.push('- none declared');
|
|
267
|
+
for (const backend of artifact.backends) {
|
|
268
|
+
lines.push(`- \`${backend.key}\`: methods \`${backend.allowedMethods.join(', ') || '<none>'}\`, timeout \`${backend.timeoutMs}\`ms`);
|
|
269
|
+
}
|
|
270
|
+
lines.push('');
|
|
271
|
+
lines.push('## ctx.fetch usages');
|
|
272
|
+
lines.push('');
|
|
273
|
+
if (artifact.usages.length === 0) lines.push('- none detected');
|
|
274
|
+
for (const usage of artifact.usages) {
|
|
275
|
+
lines.push(`- \`${usage.handler}\` -> \`${usage.backend}\` \`${usage.method || '?'} ${usage.path || '?'}\``);
|
|
276
|
+
}
|
|
277
|
+
lines.push('');
|
|
278
|
+
lines.push('## Diagnostics');
|
|
279
|
+
lines.push('');
|
|
280
|
+
if (artifact.diagnostics.length === 0) lines.push('- none');
|
|
281
|
+
for (const diag of artifact.diagnostics) lines.push(`- \`${diag.code}\`: ${diag.message}`);
|
|
282
|
+
lines.push('');
|
|
283
|
+
return `${lines.join('\n')}\n`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function buildBackendCapabilities(dispatchTable, executionPlan, handlerEval, options = {}) {
|
|
287
|
+
const cwd = options.cwd || process.cwd();
|
|
288
|
+
const generatedBy = options.generatedBy || PACKAGE_VERSION;
|
|
289
|
+
const config = configArtifact(options);
|
|
290
|
+
const engine = selectRuntimeEngine(config);
|
|
291
|
+
const normalized = normalizeBackends(config);
|
|
292
|
+
const usages = collectFetchUsages(handlerEval);
|
|
293
|
+
const usageDiagnostics = validateUsages(usages, normalized.backends, engine);
|
|
294
|
+
const diagnostics = [...normalized.diagnostics, ...usageDiagnostics];
|
|
295
|
+
|
|
296
|
+
const artifact = normalizeArtifact({
|
|
297
|
+
version: BACKEND_CAPABILITIES_VERSION,
|
|
298
|
+
generatedBy,
|
|
299
|
+
source: dispatchTable?.source,
|
|
300
|
+
policy: {
|
|
301
|
+
phase: BACKEND_CAPABILITIES_PHASE,
|
|
302
|
+
status: 'locked-contract',
|
|
303
|
+
sanctionedPath: 'ctx.fetch("backendKey", { method, path })',
|
|
304
|
+
declarationSurface: 'defineConfig.backends',
|
|
305
|
+
appFetchOutbound: false,
|
|
306
|
+
globalFetchAllowed: false,
|
|
307
|
+
arbitraryNetworkImportsAllowed: false,
|
|
308
|
+
wasmFetchEffectImplemented: false,
|
|
309
|
+
jsEngineEscapeHatch: 'manual-only'
|
|
310
|
+
},
|
|
311
|
+
lockedPositions: [
|
|
312
|
+
'defineConfig.backends declares outbound backend capabilities.',
|
|
313
|
+
'ctx.fetch consumes declared outbound backend capabilities during active execution.',
|
|
314
|
+
'Global fetch and networking libraries are forbidden in PulseWasm handlers.',
|
|
315
|
+
'backend key, method, and request object are static in v1.',
|
|
316
|
+
'Wasm fetch execution is reserved for a future host effect/resume runtime.',
|
|
317
|
+
'JS engine usage is manual escape hatch debt; fallback is never automatic.'
|
|
318
|
+
],
|
|
319
|
+
runtime: {
|
|
320
|
+
engine: engine.engine,
|
|
321
|
+
engineSource: engine.source,
|
|
322
|
+
manualEscapeHatch: engine.engine === 'js',
|
|
323
|
+
automaticFallback: false
|
|
324
|
+
},
|
|
325
|
+
defaults: {
|
|
326
|
+
timeoutMs: DEFAULT_BACKEND_TIMEOUT_MS
|
|
327
|
+
},
|
|
328
|
+
backends: normalized.backends,
|
|
329
|
+
usages,
|
|
330
|
+
diagnostics,
|
|
331
|
+
diagnosticsPolicy: {
|
|
332
|
+
errors: [
|
|
333
|
+
'PULSEWASM_FETCH_EFFECT_UNSUPPORTED',
|
|
334
|
+
'PULSEWASM_UNDECLARED_BACKEND',
|
|
335
|
+
'PULSEWASM_DYNAMIC_BACKEND_KEY',
|
|
336
|
+
'PULSEWASM_DYNAMIC_FETCH_REQUEST',
|
|
337
|
+
'PULSEWASM_BACKEND_METHOD_NOT_ALLOWED',
|
|
338
|
+
'PULSEWASM_FORBIDDEN_GLOBAL_FETCH',
|
|
339
|
+
'PULSEWASM_FORBIDDEN_NETWORK_IMPORT'
|
|
340
|
+
],
|
|
341
|
+
debt: ['PULSEWASM_FETCH_REQUIRES_JS_ENGINE']
|
|
342
|
+
},
|
|
343
|
+
futureWasmEffectAbi: {
|
|
344
|
+
implementedIn10G: false,
|
|
345
|
+
reservedShape: 'pulse_fetch_start(ctxRef, backendKeyRef, requestRef) -> FetchEffectRef',
|
|
346
|
+
resumeRuntimeRequired: true
|
|
347
|
+
},
|
|
348
|
+
summary: {
|
|
349
|
+
backends: normalized.backends.length,
|
|
350
|
+
usages: usages.length,
|
|
351
|
+
engine: engine.engine,
|
|
352
|
+
wasmFetchEffectImplemented: false,
|
|
353
|
+
wasmModeBlockedByFetch: engine.engine === 'wasm' && usages.length > 0,
|
|
354
|
+
jsEscapeHatchDebt: engine.engine === 'js' && usages.length > 0,
|
|
355
|
+
diagnostics: diagnostics.length
|
|
356
|
+
}
|
|
357
|
+
}, cwd);
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
artifact,
|
|
361
|
+
files: [{ file: 'generated/host/backend-capabilities.md', text: buildMarkdown(artifact) }],
|
|
362
|
+
diagnostics
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
module.exports = {
|
|
367
|
+
BACKEND_CAPABILITIES_VERSION,
|
|
368
|
+
DEFAULT_BACKEND_TIMEOUT_MS,
|
|
369
|
+
buildBackendCapabilities,
|
|
370
|
+
collectFetchUsages,
|
|
371
|
+
normalizeBackends
|
|
372
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { PACKAGE_VERSION, normalizeArtifact } = require('@pulse-compute/wasm-contracts/diagnostics');
|
|
4
|
+
const {
|
|
5
|
+
CHANNEL_BROADCASTER_VERSION,
|
|
6
|
+
CHANNEL_BROADCASTER_PHASE,
|
|
7
|
+
BROADCASTER_MISSING_CODE,
|
|
8
|
+
channelAbiContract,
|
|
9
|
+
broadcasterImportContract
|
|
10
|
+
} = loadContractsChannelBroadcaster();
|
|
11
|
+
|
|
12
|
+
function loadContractsChannelBroadcaster() {
|
|
13
|
+
try {
|
|
14
|
+
return require('@pulse-compute/wasm-contracts/host/channel-broadcaster');
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
17
|
+
return require('../../../contracts/src/host/channel-broadcaster.js');
|
|
18
|
+
}
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function channelKind(channel) {
|
|
24
|
+
if (!channel) return 'none';
|
|
25
|
+
if (channel.kind === 'handler') return 'handler';
|
|
26
|
+
if (channel.kind === 'static') return 'static';
|
|
27
|
+
return channel.kind || 'unknown';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function collectChannelUses(dispatchTable, executionPlan) {
|
|
31
|
+
const routeUses = [];
|
|
32
|
+
for (const route of dispatchTable?.routes || []) {
|
|
33
|
+
const channel = route.execution?.channel || route.channel;
|
|
34
|
+
if (!channel) continue;
|
|
35
|
+
routeUses.push({
|
|
36
|
+
kind: 'route',
|
|
37
|
+
runtimeId: route.runtimeId,
|
|
38
|
+
stableId: route.stableId,
|
|
39
|
+
method: route.method,
|
|
40
|
+
path: route.path,
|
|
41
|
+
channelKind: channelKind(channel),
|
|
42
|
+
slot: Number.isInteger(channel.slot) ? channel.slot : undefined,
|
|
43
|
+
handlerId: channel.id || channel.handlerId,
|
|
44
|
+
name: channel.name,
|
|
45
|
+
staticValue: channel.kind === 'static' ? channel.value : undefined
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const scopeUses = [];
|
|
50
|
+
for (const scope of executionPlan?.scopes || []) {
|
|
51
|
+
if (!scope.channel) continue;
|
|
52
|
+
const channel = scope.channel;
|
|
53
|
+
scopeUses.push({
|
|
54
|
+
kind: 'scope',
|
|
55
|
+
scopeId: scope.id,
|
|
56
|
+
channelKind: channelKind(channel),
|
|
57
|
+
slot: Number.isInteger(channel.slot) ? channel.slot : undefined,
|
|
58
|
+
handlerId: channel.id || channel.handlerId,
|
|
59
|
+
name: channel.name,
|
|
60
|
+
staticValue: channel.kind === 'static' ? channel.value : undefined
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { routeUses, scopeUses };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildMarkdown(artifact) {
|
|
68
|
+
const lines = [];
|
|
69
|
+
lines.push('# PulseWasm Channel / Broadcaster Adaptor Contract v1');
|
|
70
|
+
lines.push('');
|
|
71
|
+
lines.push(`Generated by: \`${artifact.generatedBy}\``);
|
|
72
|
+
lines.push(`Phase: \`${artifact.policy.phase}\``);
|
|
73
|
+
lines.push('');
|
|
74
|
+
lines.push('## Locked positions');
|
|
75
|
+
lines.push('');
|
|
76
|
+
for (const item of artifact.lockedPositions) lines.push(`- ${item}`);
|
|
77
|
+
lines.push('');
|
|
78
|
+
lines.push('## Channel representation');
|
|
79
|
+
lines.push('');
|
|
80
|
+
lines.push(`- Channel handler signature: \`${artifact.channelAbi.handlerSignature}\``);
|
|
81
|
+
lines.push(`- ChannelsRef: \`${artifact.channelAbi.channelsRef}\``);
|
|
82
|
+
lines.push(`- Ordered: \`${artifact.channelAbi.ordered}\``);
|
|
83
|
+
lines.push(`- Empty channel list behavior: \`${artifact.channelAbi.emptyListBehavior}\``);
|
|
84
|
+
lines.push('');
|
|
85
|
+
lines.push('## Host-visible channel exports');
|
|
86
|
+
lines.push('');
|
|
87
|
+
for (const item of artifact.channelAbi.exports) lines.push(`- \`${item}\``);
|
|
88
|
+
lines.push('');
|
|
89
|
+
lines.push('## Broadcaster import');
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push(`- Module: \`${artifact.broadcaster.module}\``);
|
|
92
|
+
lines.push(`- Name: \`${artifact.broadcaster.name}\``);
|
|
93
|
+
lines.push(`- Signature: \`${artifact.broadcaster.signature}\``);
|
|
94
|
+
lines.push(`- Return policy: \`${artifact.broadcaster.returnPolicy}\``);
|
|
95
|
+
lines.push(`- Required when channels resolve: \`${artifact.broadcaster.requiredWhenChannelsResolve}\``);
|
|
96
|
+
lines.push('');
|
|
97
|
+
lines.push('## Failure');
|
|
98
|
+
lines.push('');
|
|
99
|
+
lines.push(`- Code: \`${artifact.broadcaster.missingFailure.code}\``);
|
|
100
|
+
lines.push(`- Status: \`${artifact.broadcaster.missingFailure.statusCode}\``);
|
|
101
|
+
lines.push(`- Message: \`${artifact.broadcaster.missingFailure.message}\``);
|
|
102
|
+
lines.push('');
|
|
103
|
+
lines.push('## GRIP / headers');
|
|
104
|
+
lines.push('');
|
|
105
|
+
lines.push('Channel resolution does not invent a separate GRIP side channel. GRIP/Fanout framing remains expressed through normal ordered response headers such as `Grip-Hold` and repeated `Grip-Channel`.');
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push('## Summary');
|
|
108
|
+
lines.push('');
|
|
109
|
+
for (const [key, value] of Object.entries(artifact.summary)) lines.push(`- ${key}: \`${value}\``);
|
|
110
|
+
lines.push('');
|
|
111
|
+
return `${lines.join('\n')}\n`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function buildChannelBroadcaster(dispatchTable, executionPlan, options = {}) {
|
|
115
|
+
const cwd = options.cwd || process.cwd();
|
|
116
|
+
const generatedBy = options.generatedBy || PACKAGE_VERSION;
|
|
117
|
+
const channelHandlers = Number(dispatchTable?.summary?.channelHandlers || 0);
|
|
118
|
+
const staticChannels = Number(dispatchTable?.summary?.staticChannels || 0);
|
|
119
|
+
const { routeUses, scopeUses } = collectChannelUses(dispatchTable, executionPlan);
|
|
120
|
+
const channelsMayResolve = routeUses.length > 0 || scopeUses.length > 0 || channelHandlers > 0 || staticChannels > 0;
|
|
121
|
+
|
|
122
|
+
const artifact = normalizeArtifact({
|
|
123
|
+
version: CHANNEL_BROADCASTER_VERSION,
|
|
124
|
+
generatedBy,
|
|
125
|
+
source: dispatchTable?.source,
|
|
126
|
+
policy: {
|
|
127
|
+
phase: CHANNEL_BROADCASTER_PHASE,
|
|
128
|
+
status: 'locked-contract-and-local-bridge-proof',
|
|
129
|
+
sourceOfTruth: [
|
|
130
|
+
'dispatch-table.json',
|
|
131
|
+
'execution-plan.json',
|
|
132
|
+
'wasm-host-abi.json',
|
|
133
|
+
'request-result-headers.json',
|
|
134
|
+
'pulsewasm channel/broadcaster adaptor contract v1'
|
|
135
|
+
],
|
|
136
|
+
contractOnly: true,
|
|
137
|
+
platformBindings: false,
|
|
138
|
+
gripImplementation: false,
|
|
139
|
+
broadcasterAdaptorRequired: channelsMayResolve,
|
|
140
|
+
silentIgnoreForbidden: true
|
|
141
|
+
},
|
|
142
|
+
lockedPositions: [
|
|
143
|
+
'Channel resolution is a real routing artifact.',
|
|
144
|
+
'Channel handlers return ChannelsRef through the locked ChannelHandler ABI.',
|
|
145
|
+
'ChannelsRef is an ordered list of one or more StringRef channel names.',
|
|
146
|
+
'Static channels and handler-produced channels are normalized to the same ChannelsRef surface.',
|
|
147
|
+
'If no channel resolves, no broadcaster call occurs.',
|
|
148
|
+
'If one or more channels resolve and a broadcaster adaptor exists, broadcaster.broadcast(channelsRef, ctxRef) is invoked.',
|
|
149
|
+
'If one or more channels resolve and the broadcaster adaptor is missing, the runtime/host bridge fails explicitly.',
|
|
150
|
+
'Broadcaster failure returns an ErrorRef through the broadcaster return value.',
|
|
151
|
+
'GRIP/Fanout framing remains represented through normal ordered response headers, not a hidden side channel.'
|
|
152
|
+
],
|
|
153
|
+
channelAbi: channelAbiContract(),
|
|
154
|
+
broadcaster: broadcasterImportContract({ requiredWhenChannelsResolve: channelsMayResolve }),
|
|
155
|
+
grip: {
|
|
156
|
+
implementedHere: false,
|
|
157
|
+
framedThroughHeaders: true,
|
|
158
|
+
headers: ['Grip-Hold', 'Grip-Channel'],
|
|
159
|
+
repeatedGripChannelPreserved: true,
|
|
160
|
+
adaptorFuture: 'GRIP/Fanout adaptor may translate ChannelsRef into ordered response headers and/or platform broadcaster calls.'
|
|
161
|
+
},
|
|
162
|
+
channelUses: {
|
|
163
|
+
routes: routeUses,
|
|
164
|
+
scopes: scopeUses
|
|
165
|
+
},
|
|
166
|
+
summary: {
|
|
167
|
+
routes: Array.isArray(dispatchTable?.routes) ? dispatchTable.routes.length : 0,
|
|
168
|
+
executionEntries: Array.isArray(executionPlan?.entries) ? executionPlan.entries.length : 0,
|
|
169
|
+
scopes: Array.isArray(executionPlan?.scopes) ? executionPlan.scopes.length : 0,
|
|
170
|
+
routeChannelUses: routeUses.length,
|
|
171
|
+
scopeChannelUses: scopeUses.length,
|
|
172
|
+
channelHandlers,
|
|
173
|
+
staticChannels,
|
|
174
|
+
channelsMayResolve,
|
|
175
|
+
broadcasterRequiredWhenChannelsResolve: channelsMayResolve,
|
|
176
|
+
missingBroadcasterFailureCode: BROADCASTER_MISSING_CODE,
|
|
177
|
+
gripHeadersModeledThroughHeaders: true,
|
|
178
|
+
platformBindings: false
|
|
179
|
+
}
|
|
180
|
+
}, cwd);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
artifact,
|
|
184
|
+
files: [
|
|
185
|
+
{ file: 'generated/host/channel-broadcaster.md', text: buildMarkdown(artifact) }
|
|
186
|
+
],
|
|
187
|
+
diagnostics: []
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
CHANNEL_BROADCASTER_VERSION,
|
|
193
|
+
buildChannelBroadcaster
|
|
194
|
+
};
|