@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,984 @@
|
|
|
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 { spawnSync } = require('node:child_process');
|
|
9
|
+
const { PACKAGE_VERSION, normalizeArtifact, normalizeDiagnostic } = require('@pulse-compute/wasm-contracts/diagnostics');
|
|
10
|
+
|
|
11
|
+
function diagnosticFromParts(parts) {
|
|
12
|
+
return normalizeDiagnostic({ severity: 'error', loc: { file: '<generated>' }, ...parts });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const WASM_BRIDGE_VERSION = 'pulsewasm.wasm-host-bridge.v3';
|
|
16
|
+
const WASM_BRIDGE_PHASE = '12D';
|
|
17
|
+
const BRIDGE_RUNNER_FILE = 'generated/host-bridge/host-bridge-runner.as.ts';
|
|
18
|
+
const BRIDGE_WASM_REL = 'generated/wasm-bridge/pulsewasm-host-bridge.wasm';
|
|
19
|
+
const BRIDGE_WAT_REL = 'generated/wasm-bridge/pulsewasm-host-bridge.wat';
|
|
20
|
+
|
|
21
|
+
function sha256Buffer(buffer) { return crypto.createHash('sha256').update(buffer).digest('hex'); }
|
|
22
|
+
function sha256Text(text) { return crypto.createHash('sha256').update(text, 'utf8').digest('hex'); }
|
|
23
|
+
function stripAnsi(value) { return String(value || '').replace(/\u001b\[[0-9;]*m/g, ''); }
|
|
24
|
+
|
|
25
|
+
function parseAscWarnings(stderr) {
|
|
26
|
+
const text = stripAnsi(stderr);
|
|
27
|
+
const warnings = [];
|
|
28
|
+
const regex = /WARNING\s+(AS\d+):\s+([^\n]+)(?:.|\n)*?in ([^\n]+)\((\d+),(\d+)\)/g;
|
|
29
|
+
let match;
|
|
30
|
+
while ((match = regex.exec(text))) {
|
|
31
|
+
warnings.push({ code: match[1], message: match[2].trim(), file: match[3], line: Number(match[4]), column: Number(match[5]) });
|
|
32
|
+
}
|
|
33
|
+
if (warnings.length === 0 && /WARNING\s+AS\d+:/m.test(text)) {
|
|
34
|
+
warnings.push({ code: 'AS_WARNING', message: 'AssemblyScript emitted one or more warnings. Inspect result.stderr for details.' });
|
|
35
|
+
}
|
|
36
|
+
return warnings;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function assemblyScriptVersion(cwd) {
|
|
40
|
+
try {
|
|
41
|
+
const packageJson = require.resolve('assemblyscript/package.json', { paths: [cwd] });
|
|
42
|
+
return JSON.parse(fs.readFileSync(packageJson, 'utf8')).version;
|
|
43
|
+
} catch (_) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fileRecord(filePath, cwd, kind) {
|
|
49
|
+
const buffer = fs.readFileSync(filePath);
|
|
50
|
+
return { file: path.relative(cwd, filePath).replace(/\\/g, '/'), kind, bytes: buffer.length, sha256: sha256Buffer(buffer) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeStagedFiles(stagingDir, shape, core, bridgeRunnerText) {
|
|
54
|
+
const allFiles = [];
|
|
55
|
+
if (shape && Array.isArray(shape.files)) allFiles.push(...shape.files);
|
|
56
|
+
if (core && Array.isArray(core.files)) allFiles.push(...core.files);
|
|
57
|
+
const byFile = new Map();
|
|
58
|
+
for (const file of allFiles) byFile.set(file.file, file.text);
|
|
59
|
+
byFile.set(BRIDGE_RUNNER_FILE, bridgeRunnerText);
|
|
60
|
+
for (const [file, text] of byFile.entries()) {
|
|
61
|
+
const abs = path.join(stagingDir, file);
|
|
62
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
63
|
+
fs.writeFileSync(abs, text, 'utf8');
|
|
64
|
+
}
|
|
65
|
+
return Array.from(byFile.entries()).map(([file, text]) => ({ file, bytes: Buffer.byteLength(text, 'utf8'), sha256: sha256Text(text) }));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildBridgeRunnerSource() {
|
|
69
|
+
return `/*
|
|
70
|
+
* Generated by PulseWasm Phase 12D.
|
|
71
|
+
* Do not edit by hand.
|
|
72
|
+
*
|
|
73
|
+
* This file adapts the generated AssemblyScript routing core to the locked
|
|
74
|
+
* Wasm Host ABI v4 so Node can drive the compiled Wasm through ptr+byteLen
|
|
75
|
+
* strings, ordered repeated headers, and exported continuation/result helpers.
|
|
76
|
+
*/
|
|
77
|
+
import {
|
|
78
|
+
METHOD_GET,
|
|
79
|
+
METHOD_POST,
|
|
80
|
+
CHANNEL_HANDLER
|
|
81
|
+
} from '../as/types.as'
|
|
82
|
+
|
|
83
|
+
import {
|
|
84
|
+
EXECUTION_ENTRY_COUNT,
|
|
85
|
+
ROUTE_COUNT
|
|
86
|
+
} from '../as/route-table.as'
|
|
87
|
+
|
|
88
|
+
import {
|
|
89
|
+
HANDLER_BINDING_COUNT
|
|
90
|
+
} from '../as/handler-slots.as'
|
|
91
|
+
|
|
92
|
+
import {
|
|
93
|
+
PulseWasmDispatchState,
|
|
94
|
+
PulseWasmErrorRef,
|
|
95
|
+
createDispatchState,
|
|
96
|
+
createPulseWasmError,
|
|
97
|
+
stepDispatchState,
|
|
98
|
+
continueNormal,
|
|
99
|
+
continueError,
|
|
100
|
+
pulse_set_result,
|
|
101
|
+
defaultStatusForStepResult,
|
|
102
|
+
STEP_INVOKE,
|
|
103
|
+
STEP_NOT_FOUND,
|
|
104
|
+
STEP_UNHANDLED_ERROR,
|
|
105
|
+
STEP_CONTRACT_VIOLATION,
|
|
106
|
+
NEXT_NORMAL,
|
|
107
|
+
NEXT_ERROR,
|
|
108
|
+
ENTRY_ROUTE,
|
|
109
|
+
invokeCurrentHandler,
|
|
110
|
+
currentParamCount,
|
|
111
|
+
currentParamNameAt,
|
|
112
|
+
currentParamValueAt,
|
|
113
|
+
scopeChannelKind,
|
|
114
|
+
scopeChannelSlot,
|
|
115
|
+
scopeConnectLength,
|
|
116
|
+
scopeConnectSlotAt,
|
|
117
|
+
scopeDisconnectLength,
|
|
118
|
+
scopeDisconnectSlotAt,
|
|
119
|
+
invokeChannelHandler,
|
|
120
|
+
invokeLifecycleHandler
|
|
121
|
+
} from '../as/runtime-core.as'
|
|
122
|
+
|
|
123
|
+
@external('pulsewasm_broadcaster', 'broadcast')
|
|
124
|
+
declare function __pulsewasm_broadcast(channelsRef: usize, ctxRef: usize): i32
|
|
125
|
+
|
|
126
|
+
export const RESULT_NONE: i32 = 0
|
|
127
|
+
export const RESULT_TEXT: i32 = 1
|
|
128
|
+
export const RESULT_JSON: i32 = 2
|
|
129
|
+
export const RESULT_BINARY: i32 = 3
|
|
130
|
+
export const RESULT_EMPTY: i32 = 4
|
|
131
|
+
export const RESULT_STREAM: i32 = 5
|
|
132
|
+
|
|
133
|
+
export const HOST_BRIDGE_OK: i32 = 0
|
|
134
|
+
export const HOST_BRIDGE_NOT_FOUND: i32 = 404
|
|
135
|
+
export const HOST_BRIDGE_INTERNAL_ERROR: i32 = 500
|
|
136
|
+
export const HOST_BRIDGE_BROADCASTER_ERROR: i32 = 590
|
|
137
|
+
|
|
138
|
+
let __currentState: PulseWasmDispatchState | null = null
|
|
139
|
+
let __resultStatus: i32 = 0
|
|
140
|
+
let __resultKind: i32 = RESULT_NONE
|
|
141
|
+
let __resultBodyPtr: usize = 0
|
|
142
|
+
let __resultBodyLen: i32 = 0
|
|
143
|
+
let __resultHeadersRef: usize = 0
|
|
144
|
+
let __resultErrorRef: usize = 0
|
|
145
|
+
let __resultStreamRef: usize = 0
|
|
146
|
+
let __resultResponseRef: usize = 0
|
|
147
|
+
let __lastStringBuffer: ArrayBuffer | null = null
|
|
148
|
+
let __lastHeaderBuffer: ArrayBuffer | null = null
|
|
149
|
+
let __lastChannelsRef: usize = 0
|
|
150
|
+
let __lastBroadcastResult: i32 = 0
|
|
151
|
+
let __lastInvokedHandlers: i32 = 0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
let __requestHeaderNames = new Array<string>()
|
|
155
|
+
let __requestHeaderLowerNames = new Array<string>()
|
|
156
|
+
let __requestHeaderValues = new Array<string>()
|
|
157
|
+
let __responseHeaderNames = new Array<string>()
|
|
158
|
+
let __responseHeaderLowerNames = new Array<string>()
|
|
159
|
+
let __responseHeaderValues = new Array<string>()
|
|
160
|
+
let __responseHeaderModes = new Array<i32>()
|
|
161
|
+
let __responseHeaderDeletedNames = new Array<string>()
|
|
162
|
+
let __responseHeaderDeletedLowerNames = new Array<string>()
|
|
163
|
+
|
|
164
|
+
function decodeUtf8(ptr: usize, len: i32): string {
|
|
165
|
+
if (ptr == 0 || len <= 0) return ''
|
|
166
|
+
return String.UTF8.decodeUnsafe(ptr, len, true)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function stringRef(value: string): usize { return changetype<usize>(value) }
|
|
170
|
+
|
|
171
|
+
function lowerAscii(value: string): string {
|
|
172
|
+
let out = ''
|
|
173
|
+
for (let i: i32 = 0; i < value.length; i += 1) {
|
|
174
|
+
let code = value.charCodeAt(i)
|
|
175
|
+
if (code >= 65 && code <= 90) code += 32
|
|
176
|
+
out += String.fromCharCode(code)
|
|
177
|
+
}
|
|
178
|
+
return out
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function clearRequestHeaders(): void {
|
|
182
|
+
__requestHeaderNames.length = 0
|
|
183
|
+
__requestHeaderLowerNames.length = 0
|
|
184
|
+
__requestHeaderValues.length = 0
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function clearResponseHeaders(): void {
|
|
188
|
+
__responseHeaderNames.length = 0
|
|
189
|
+
__responseHeaderLowerNames.length = 0
|
|
190
|
+
__responseHeaderValues.length = 0
|
|
191
|
+
__responseHeaderModes.length = 0
|
|
192
|
+
__responseHeaderDeletedNames.length = 0
|
|
193
|
+
__responseHeaderDeletedLowerNames.length = 0
|
|
194
|
+
__resultHeadersRef = 0
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function appendHeader(names: Array<string>, lowerNames: Array<string>, values: Array<string>, name: string, value: string): void {
|
|
198
|
+
names.push(name)
|
|
199
|
+
lowerNames.push(lowerAscii(name))
|
|
200
|
+
values.push(value)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function appendResponseHeader(name: string, value: string, mode: i32): void {
|
|
204
|
+
__responseHeaderNames.push(name)
|
|
205
|
+
__responseHeaderLowerNames.push(lowerAscii(name))
|
|
206
|
+
__responseHeaderValues.push(value)
|
|
207
|
+
__responseHeaderModes.push(mode)
|
|
208
|
+
__resultHeadersRef = 1
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function headerCount(lowerNames: Array<string>, lowerName: string): i32 {
|
|
212
|
+
let count: i32 = 0
|
|
213
|
+
for (let i: i32 = 0; i < lowerNames.length; i += 1) {
|
|
214
|
+
if (lowerNames[i] == lowerName) count += 1
|
|
215
|
+
}
|
|
216
|
+
return count
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function matchingHeaderIndex(lowerNames: Array<string>, lowerName: string, ordinal: i32): i32 {
|
|
220
|
+
let seen: i32 = 0
|
|
221
|
+
for (let i: i32 = 0; i < lowerNames.length; i += 1) {
|
|
222
|
+
if (lowerNames[i] == lowerName) {
|
|
223
|
+
if (seen == ordinal) return i
|
|
224
|
+
seen += 1
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return -1
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function deleteResponseHeadersByLower(lowerName: string): void {
|
|
231
|
+
const names = new Array<string>()
|
|
232
|
+
const lowerNames = new Array<string>()
|
|
233
|
+
const values = new Array<string>()
|
|
234
|
+
const modes = new Array<i32>()
|
|
235
|
+
for (let i: i32 = 0; i < __responseHeaderNames.length; i += 1) {
|
|
236
|
+
if (__responseHeaderLowerNames[i] != lowerName) {
|
|
237
|
+
names.push(__responseHeaderNames[i])
|
|
238
|
+
lowerNames.push(__responseHeaderLowerNames[i])
|
|
239
|
+
values.push(__responseHeaderValues[i])
|
|
240
|
+
modes.push(__responseHeaderModes[i])
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
__responseHeaderNames = names
|
|
244
|
+
__responseHeaderLowerNames = lowerNames
|
|
245
|
+
__responseHeaderValues = values
|
|
246
|
+
__responseHeaderModes = modes
|
|
247
|
+
__resultHeadersRef = __responseHeaderNames.length > 0 ? 1 : 0
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function headerStringPtr(value: string): i32 {
|
|
251
|
+
const encoded = String.UTF8.encode(value, true)
|
|
252
|
+
__lastHeaderBuffer = encoded
|
|
253
|
+
return changetype<i32>(encoded)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function resetResult(): void {
|
|
257
|
+
__resultStatus = 0
|
|
258
|
+
__resultKind = RESULT_NONE
|
|
259
|
+
__resultBodyPtr = 0
|
|
260
|
+
__resultBodyLen = 0
|
|
261
|
+
__resultErrorRef = 0
|
|
262
|
+
__resultStreamRef = 0
|
|
263
|
+
__resultResponseRef = 0
|
|
264
|
+
__lastStringBuffer = null
|
|
265
|
+
__lastHeaderBuffer = null
|
|
266
|
+
clearResponseHeaders()
|
|
267
|
+
__lastChannelsRef = 0
|
|
268
|
+
__lastBroadcastResult = 0
|
|
269
|
+
__lastInvokedHandlers = 0
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function setEmptyResult(statusCode: i32): void {
|
|
273
|
+
__resultStatus = statusCode
|
|
274
|
+
__resultKind = RESULT_EMPTY
|
|
275
|
+
__resultBodyPtr = 0
|
|
276
|
+
__resultBodyLen = 0
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function setErrorResult(statusCode: i32, errorRef: usize): void {
|
|
280
|
+
__resultStatus = statusCode
|
|
281
|
+
__resultKind = RESULT_TEXT
|
|
282
|
+
__resultErrorRef = errorRef
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function currentStateFromCtx(ctxRef: usize): PulseWasmDispatchState {
|
|
286
|
+
return changetype<PulseWasmDispatchState>(ctxRef)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function maybeBroadcastForState(state: PulseWasmDispatchState): void {
|
|
290
|
+
const scopeId = state.currentScopeId
|
|
291
|
+
if (scopeId < 0) return
|
|
292
|
+
if (scopeChannelKind(scopeId) != CHANNEL_HANDLER) return
|
|
293
|
+
const slot = scopeChannelSlot(scopeId)
|
|
294
|
+
if (slot < 0) return
|
|
295
|
+
const channelsRef = invokeChannelHandler(slot, changetype<usize>(state))
|
|
296
|
+
if (channelsRef == 0) return
|
|
297
|
+
__lastChannelsRef = channelsRef
|
|
298
|
+
const result = __pulsewasm_broadcast(channelsRef, changetype<usize>(state))
|
|
299
|
+
__lastBroadcastResult = result
|
|
300
|
+
if (result != 0) setErrorResult(HOST_BRIDGE_INTERNAL_ERROR, result)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function executeState(state: PulseWasmDispatchState): i32 {
|
|
304
|
+
__currentState = state
|
|
305
|
+
let guard: i32 = 0
|
|
306
|
+
while (guard < EXECUTION_ENTRY_COUNT + 16) {
|
|
307
|
+
guard += 1
|
|
308
|
+
const step = stepDispatchState(state)
|
|
309
|
+
if (step == STEP_INVOKE) {
|
|
310
|
+
__lastInvokedHandlers += 1
|
|
311
|
+
const invoked = invokeCurrentHandler(state)
|
|
312
|
+
if (!invoked) {
|
|
313
|
+
setErrorResult(HOST_BRIDGE_INTERNAL_ERROR, changetype<usize>(createPulseWasmError('PULSEWASM_HANDLER_NOT_BOUND', 'Handler slot could not be invoked.', HOST_BRIDGE_INTERNAL_ERROR)))
|
|
314
|
+
return __resultStatus
|
|
315
|
+
}
|
|
316
|
+
if (__resultStatus != 0 || __resultKind != RESULT_NONE) {
|
|
317
|
+
maybeBroadcastForState(state)
|
|
318
|
+
return __resultStatus
|
|
319
|
+
}
|
|
320
|
+
if (state.nextOutcome == NEXT_NORMAL || state.nextOutcome == NEXT_ERROR) continue
|
|
321
|
+
setErrorResult(HOST_BRIDGE_INTERNAL_ERROR, changetype<usize>(createPulseWasmError('PULSEWASM_NO_RESULT', 'Handler halted without result or continuation.', HOST_BRIDGE_INTERNAL_ERROR)))
|
|
322
|
+
return __resultStatus
|
|
323
|
+
}
|
|
324
|
+
if (step == STEP_NOT_FOUND) { setEmptyResult(404); return __resultStatus }
|
|
325
|
+
if (step == STEP_UNHANDLED_ERROR) { setErrorResult(500, state.activeError); return __resultStatus }
|
|
326
|
+
if (step == STEP_CONTRACT_VIOLATION) {
|
|
327
|
+
setErrorResult(500, changetype<usize>(createPulseWasmError('PULSEWASM_NO_RESULT', 'Handler halted without result or continuation.', 500)))
|
|
328
|
+
return __resultStatus
|
|
329
|
+
}
|
|
330
|
+
const fallback = defaultStatusForStepResult(step)
|
|
331
|
+
if (fallback != 0) { setEmptyResult(fallback); return __resultStatus }
|
|
332
|
+
setErrorResult(500, changetype<usize>(createPulseWasmError('PULSEWASM_INVALID_STEP', 'Invalid routing step result.', 500)))
|
|
333
|
+
return __resultStatus
|
|
334
|
+
}
|
|
335
|
+
setErrorResult(500, changetype<usize>(createPulseWasmError('PULSEWASM_DISPATCH_GUARD_EXHAUSTED', 'Dispatch guard exhausted.', 500)))
|
|
336
|
+
return __resultStatus
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function findScopeForPath(methodCode: i32, requestPath: string): PulseWasmDispatchState {
|
|
340
|
+
const state = createDispatchState(methodCode, requestPath)
|
|
341
|
+
let guard: i32 = 0
|
|
342
|
+
while (guard < EXECUTION_ENTRY_COUNT + 16) {
|
|
343
|
+
guard += 1
|
|
344
|
+
const step = stepDispatchState(state)
|
|
345
|
+
if (step != STEP_INVOKE) return state
|
|
346
|
+
if (state.currentKind == ENTRY_ROUTE) return state
|
|
347
|
+
continueNormal(state)
|
|
348
|
+
}
|
|
349
|
+
return state
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function executeLifecycleForScope(state: PulseWasmDispatchState, eventKind: i32): i32 {
|
|
353
|
+
__currentState = state
|
|
354
|
+
let invoked: i32 = 0
|
|
355
|
+
const scopeId = state.currentScopeId
|
|
356
|
+
const length = eventKind == 1 ? scopeConnectLength(scopeId) : scopeDisconnectLength(scopeId)
|
|
357
|
+
for (let i: i32 = 0; i < length; i += 1) {
|
|
358
|
+
const slot = eventKind == 1 ? scopeConnectSlotAt(scopeId, i) : scopeDisconnectSlotAt(scopeId, i)
|
|
359
|
+
if (slot >= 0 && invokeLifecycleHandler(slot, changetype<usize>(state), changetype<usize>(state))) invoked += 1
|
|
360
|
+
}
|
|
361
|
+
return invoked
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function pulse_alloc(byteLength: i32): i32 {
|
|
365
|
+
if (byteLength <= 0) return 0
|
|
366
|
+
const buffer = new ArrayBuffer(byteLength)
|
|
367
|
+
return changetype<i32>(buffer)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function pulse_free(ptr: i32, byteLength: i32): void {}
|
|
371
|
+
export function pulse_init(): void { pulse_reset() }
|
|
372
|
+
|
|
373
|
+
export function pulse_reset(): void {
|
|
374
|
+
__currentState = null
|
|
375
|
+
clearRequestHeaders()
|
|
376
|
+
resetResult()
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function pulse_execute_request(methodCode: i32, pathPtr: i32, pathLen: i32): i32 {
|
|
380
|
+
resetResult()
|
|
381
|
+
const state = createDispatchState(methodCode, decodeUtf8(pathPtr, pathLen))
|
|
382
|
+
return executeState(state)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function pulse_execute_connect(methodCode: i32, pathPtr: i32, pathLen: i32): i32 {
|
|
386
|
+
resetResult()
|
|
387
|
+
const state = findScopeForPath(methodCode, decodeUtf8(pathPtr, pathLen))
|
|
388
|
+
return executeLifecycleForScope(state, 1)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function pulse_execute_disconnect(methodCode: i32, pathPtr: i32, pathLen: i32): i32 {
|
|
392
|
+
resetResult()
|
|
393
|
+
const state = findScopeForPath(methodCode, decodeUtf8(pathPtr, pathLen))
|
|
394
|
+
return executeLifecycleForScope(state, 2)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function pulse_result_status(): i32 { return __resultStatus }
|
|
398
|
+
export function pulse_result_kind(): i32 { return __resultKind }
|
|
399
|
+
export function pulse_result_body_ref(): i32 { return __resultBodyPtr == 0 ? 0 : 1 }
|
|
400
|
+
export function pulse_result_body_ptr(): i32 { return __resultBodyPtr as i32 }
|
|
401
|
+
export function pulse_result_body_len(): i32 { return __resultBodyLen }
|
|
402
|
+
export function pulse_result_headers_ref(): i32 { return __resultHeadersRef as i32 }
|
|
403
|
+
export function pulse_result_error_ref(): i32 { return __resultErrorRef as i32 }
|
|
404
|
+
export function pulse_result_stream_ref(): i32 { return __resultStreamRef as i32 }
|
|
405
|
+
export function pulse_result_response_ref(): i32 { return __resultResponseRef as i32 }
|
|
406
|
+
|
|
407
|
+
export function pulse_request_header_first(ctxRef: i32, namePtr: i32, nameLen: i32): i32 {
|
|
408
|
+
const lowerName = lowerAscii(decodeUtf8(namePtr as usize, nameLen))
|
|
409
|
+
const index = matchingHeaderIndex(__requestHeaderLowerNames, lowerName, 0)
|
|
410
|
+
return index < 0 ? 0 : stringRef(__requestHeaderValues[index]) as i32
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function pulse_request_header_count(ctxRef: i32, namePtr: i32, nameLen: i32): i32 {
|
|
414
|
+
return headerCount(__requestHeaderLowerNames, lowerAscii(decodeUtf8(namePtr as usize, nameLen)))
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function pulse_request_header_at(ctxRef: i32, namePtr: i32, nameLen: i32, index: i32): i32 {
|
|
418
|
+
const lowerName = lowerAscii(decodeUtf8(namePtr as usize, nameLen))
|
|
419
|
+
const actual = matchingHeaderIndex(__requestHeaderLowerNames, lowerName, index)
|
|
420
|
+
return actual < 0 ? 0 : stringRef(__requestHeaderValues[actual]) as i32
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function pulse_response_header_set(ctxRef: i32, namePtr: i32, nameLen: i32, valuePtr: i32, valueLen: i32): void {
|
|
424
|
+
const name = decodeUtf8(namePtr as usize, nameLen)
|
|
425
|
+
const value = decodeUtf8(valuePtr as usize, valueLen)
|
|
426
|
+
deleteResponseHeadersByLower(lowerAscii(name))
|
|
427
|
+
appendResponseHeader(name, value, 1)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function pulse_response_header_append(ctxRef: i32, namePtr: i32, nameLen: i32, valuePtr: i32, valueLen: i32): void {
|
|
431
|
+
appendResponseHeader(decodeUtf8(namePtr as usize, nameLen), decodeUtf8(valuePtr as usize, valueLen), 2)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function pulse_response_header_delete(ctxRef: i32, namePtr: i32, nameLen: i32): void {
|
|
435
|
+
const name = decodeUtf8(namePtr as usize, nameLen)
|
|
436
|
+
const lower = lowerAscii(name)
|
|
437
|
+
__responseHeaderDeletedNames.push(name)
|
|
438
|
+
__responseHeaderDeletedLowerNames.push(lower)
|
|
439
|
+
deleteResponseHeadersByLower(lower)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function pulse_result_header_count(): i32 { return __responseHeaderNames.length }
|
|
443
|
+
export function pulse_result_header_name_ptr(index: i32): i32 { return index < 0 || index >= __responseHeaderNames.length ? 0 : headerStringPtr(__responseHeaderNames[index]) }
|
|
444
|
+
export function pulse_result_header_name_len(index: i32): i32 { return index < 0 || index >= __responseHeaderNames.length ? 0 : String.UTF8.byteLength(__responseHeaderNames[index]) }
|
|
445
|
+
export function pulse_result_header_value_ptr(index: i32): i32 { return index < 0 || index >= __responseHeaderValues.length ? 0 : headerStringPtr(__responseHeaderValues[index]) }
|
|
446
|
+
export function pulse_result_header_value_len(index: i32): i32 { return index < 0 || index >= __responseHeaderValues.length ? 0 : String.UTF8.byteLength(__responseHeaderValues[index]) }
|
|
447
|
+
export function pulse_result_header_mode(index: i32): i32 { return index < 0 || index >= __responseHeaderModes.length ? 2 : __responseHeaderModes[index] }
|
|
448
|
+
export function pulse_result_header_delete_count(): i32 { return __responseHeaderDeletedNames.length }
|
|
449
|
+
export function pulse_result_header_delete_name_ptr(index: i32): i32 { return index < 0 || index >= __responseHeaderDeletedNames.length ? 0 : headerStringPtr(__responseHeaderDeletedNames[index]) }
|
|
450
|
+
export function pulse_result_header_delete_name_len(index: i32): i32 { return index < 0 || index >= __responseHeaderDeletedNames.length ? 0 : String.UTF8.byteLength(__responseHeaderDeletedNames[index]) }
|
|
451
|
+
|
|
452
|
+
export function pulse_string_ptr(stringRef: i32): i32 {
|
|
453
|
+
if (stringRef == 0) return 0
|
|
454
|
+
const value = changetype<string>(stringRef)
|
|
455
|
+
const encoded = String.UTF8.encode(value, true)
|
|
456
|
+
__lastStringBuffer = encoded
|
|
457
|
+
return changetype<i32>(encoded)
|
|
458
|
+
}
|
|
459
|
+
export function pulse_string_len(stringRef: i32): i32 { return stringRef == 0 ? 0 : String.UTF8.byteLength(changetype<string>(stringRef)) }
|
|
460
|
+
export function pulse_string_is_null(stringRef: i32): i32 { return stringRef == 0 ? 1 : 0 }
|
|
461
|
+
|
|
462
|
+
export function pulse_ctx_param(ctxRef: i32, namePtr: i32, nameLen: i32): i32 {
|
|
463
|
+
const state = currentStateFromCtx(ctxRef as usize)
|
|
464
|
+
const name = decodeUtf8(namePtr as usize, nameLen)
|
|
465
|
+
const count = currentParamCount(state)
|
|
466
|
+
for (let i: i32 = 0; i < count; i += 1) {
|
|
467
|
+
if (currentParamNameAt(state, i) == name) return stringRef(currentParamValueAt(state, i)) as i32
|
|
468
|
+
}
|
|
469
|
+
return 0
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function pulse_ctx_state_get(ctxRef: i32, keyPtr: i32, keyLen: i32): i32 {
|
|
473
|
+
const state = currentStateFromCtx(ctxRef as usize)
|
|
474
|
+
const key = decodeUtf8(keyPtr as usize, keyLen)
|
|
475
|
+
for (let i: i32 = 0; i < state.stateKeys.length; i += 1) if (state.stateKeys[i] == key) return stringRef(state.stateValues[i]) as i32
|
|
476
|
+
return 0
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function pulse_ctx_state_set(ctxRef: i32, keyPtr: i32, keyLen: i32, valuePtr: i32, valueLen: i32): void {
|
|
480
|
+
const state = currentStateFromCtx(ctxRef as usize)
|
|
481
|
+
const key = decodeUtf8(keyPtr as usize, keyLen)
|
|
482
|
+
const value = decodeUtf8(valuePtr as usize, valueLen)
|
|
483
|
+
for (let i: i32 = 0; i < state.stateKeys.length; i += 1) {
|
|
484
|
+
if (state.stateKeys[i] == key) { state.stateValues[i] = value; return }
|
|
485
|
+
}
|
|
486
|
+
state.stateKeys.push(key)
|
|
487
|
+
state.stateValues.push(value)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function pulse_result_text(ctxRef: i32, statusCode: i32, bodyPtr: i32, bodyLen: i32): void {
|
|
491
|
+
__resultStatus = statusCode
|
|
492
|
+
__resultKind = RESULT_TEXT
|
|
493
|
+
__resultBodyPtr = bodyPtr as usize
|
|
494
|
+
__resultBodyLen = bodyLen
|
|
495
|
+
pulse_set_result(currentStateFromCtx(ctxRef as usize), 1)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function pulse_result_empty(ctxRef: i32, statusCode: i32): void {
|
|
499
|
+
__resultStatus = statusCode
|
|
500
|
+
__resultKind = RESULT_EMPTY
|
|
501
|
+
__resultBodyPtr = 0
|
|
502
|
+
__resultBodyLen = 0
|
|
503
|
+
pulse_set_result(currentStateFromCtx(ctxRef as usize), 1)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function pulse_result_stream(ctxRef: i32, statusCode: i32, streamRef: i32): void {
|
|
507
|
+
__resultStatus = statusCode
|
|
508
|
+
__resultKind = RESULT_STREAM
|
|
509
|
+
__resultStreamRef = streamRef as usize
|
|
510
|
+
__resultResponseRef = 0
|
|
511
|
+
__resultBodyPtr = 0
|
|
512
|
+
__resultBodyLen = 0
|
|
513
|
+
pulse_set_result(currentStateFromCtx(ctxRef as usize), 1)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function pulse_result_from_response(ctxRef: i32, responseRef: i32): void {
|
|
517
|
+
__resultStatus = 0
|
|
518
|
+
__resultKind = RESULT_STREAM
|
|
519
|
+
__resultResponseRef = responseRef as usize
|
|
520
|
+
__resultStreamRef = 0
|
|
521
|
+
__resultBodyPtr = 0
|
|
522
|
+
__resultBodyLen = 0
|
|
523
|
+
pulse_set_result(currentStateFromCtx(ctxRef as usize), 1)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function pulse_next(ctxRef: i32): void { continueNormal(currentStateFromCtx(ctxRef as usize)) }
|
|
527
|
+
export function pulse_next_error(ctxRef: i32, errorRef: i32): void { continueError(currentStateFromCtx(ctxRef as usize), errorRef as usize) }
|
|
528
|
+
|
|
529
|
+
export function pulse_error(codePtr: i32, codeLen: i32, messagePtr: i32, messageLen: i32, statusCode: i32): i32 {
|
|
530
|
+
const error = createPulseWasmError(decodeUtf8(codePtr as usize, codeLen), decodeUtf8(messagePtr as usize, messageLen), statusCode)
|
|
531
|
+
return changetype<i32>(error)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function pulse_error_with_cause(codePtr: i32, codeLen: i32, messagePtr: i32, messageLen: i32, statusCode: i32, causeRef: i32): i32 {
|
|
535
|
+
const error = createPulseWasmError(decodeUtf8(codePtr as usize, codeLen), decodeUtf8(messagePtr as usize, messageLen), statusCode, causeRef as usize)
|
|
536
|
+
return changetype<i32>(error)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function pulse_error_code_ref(errorRef: i32): i32 { return errorRef == 0 ? 0 : stringRef(changetype<PulseWasmErrorRef>(errorRef).code) as i32 }
|
|
540
|
+
export function pulse_error_message_ref(errorRef: i32): i32 { return errorRef == 0 ? 0 : stringRef(changetype<PulseWasmErrorRef>(errorRef).message) as i32 }
|
|
541
|
+
export function pulse_error_status_code(errorRef: i32): i32 { return errorRef == 0 ? 0 : changetype<PulseWasmErrorRef>(errorRef).statusCode }
|
|
542
|
+
export function pulse_error_cause_ref(errorRef: i32): i32 { return errorRef == 0 ? 0 : changetype<PulseWasmErrorRef>(errorRef).cause as i32 }
|
|
543
|
+
|
|
544
|
+
export function pulse_channel_count(channelsRef: i32): i32 { return channelsRef == 0 ? 0 : 1 }
|
|
545
|
+
export function pulse_channel_ref_at(channelsRef: i32, index: i32): i32 { return index == 0 ? channelsRef : 0 }
|
|
546
|
+
|
|
547
|
+
export function pulse_bridge_request_headers_clear(): void { clearRequestHeaders() }
|
|
548
|
+
export function pulse_bridge_request_header_append(namePtr: i32, nameLen: i32, valuePtr: i32, valueLen: i32): void {
|
|
549
|
+
appendHeader(__requestHeaderNames, __requestHeaderLowerNames, __requestHeaderValues, decodeUtf8(namePtr as usize, nameLen), decodeUtf8(valuePtr as usize, valueLen))
|
|
550
|
+
}
|
|
551
|
+
export function pulse_bridge_request_header_total(): i32 { return __requestHeaderNames.length }
|
|
552
|
+
|
|
553
|
+
export function pulse_bridge_route_count(): i32 { return ROUTE_COUNT }
|
|
554
|
+
export function pulse_bridge_execution_entry_count(): i32 { return EXECUTION_ENTRY_COUNT }
|
|
555
|
+
export function pulse_bridge_handler_binding_count(): i32 { return HANDLER_BINDING_COUNT }
|
|
556
|
+
export function pulse_bridge_last_channels_ref(): i32 { return __lastChannelsRef as i32 }
|
|
557
|
+
export function pulse_bridge_last_broadcast_result(): i32 { return __lastBroadcastResult }
|
|
558
|
+
export function pulse_bridge_last_invoked_handlers(): i32 { return __lastInvokedHandlers }
|
|
559
|
+
export function pulse_bridge_method_get(): i32 { return METHOD_GET }
|
|
560
|
+
export function pulse_bridge_method_post(): i32 { return METHOD_POST }
|
|
561
|
+
`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function ascExecutable(cwd) {
|
|
565
|
+
try {
|
|
566
|
+
const ascPath = require.resolve('assemblyscript/bin/asc.js', { paths: [cwd] });
|
|
567
|
+
return { command: process.execPath, argsPrefix: [ascPath], display: `node ${path.relative(cwd, ascPath).replace(/\\/g, '/')}` };
|
|
568
|
+
} catch (_) {
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function compileBridge(stagingDir, cwd, options = {}) {
|
|
574
|
+
const asc = ascExecutable(cwd);
|
|
575
|
+
if (!asc) {
|
|
576
|
+
return {
|
|
577
|
+
asc,
|
|
578
|
+
result: { status: 1, stdout: '', stderr: 'assemblyscript package is not installed.' },
|
|
579
|
+
diagnostics: [diagnosticFromParts({
|
|
580
|
+
phase: 'wasm-host-bridge',
|
|
581
|
+
code: 'PULSEWASM_ASSEMBLYSCRIPT_NOT_INSTALLED',
|
|
582
|
+
message: 'AssemblyScript is required for Phase 10D Wasm host bridge compilation.',
|
|
583
|
+
hint: 'Run npm install before requesting --emit-wasm-host-bridge or --emit-request-result-headers.'
|
|
584
|
+
})]
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const outDir = options.outDir ? path.resolve(cwd, options.outDir) : getDefaultArtifactsDir(cwd);
|
|
589
|
+
const wasmFile = path.join(outDir, BRIDGE_WASM_REL);
|
|
590
|
+
const watFile = path.join(outDir, BRIDGE_WAT_REL);
|
|
591
|
+
fs.mkdirSync(path.dirname(wasmFile), { recursive: true });
|
|
592
|
+
fs.mkdirSync(path.dirname(watFile), { recursive: true });
|
|
593
|
+
const input = path.join(stagingDir, BRIDGE_RUNNER_FILE);
|
|
594
|
+
const args = [...asc.argsPrefix, input, '--outFile', wasmFile, '--textFile', watFile, '--runtime', 'stub', '--noAssert'];
|
|
595
|
+
const started = Date.now();
|
|
596
|
+
const result = spawnSync(asc.command, args, { cwd, encoding: 'utf8', maxBuffer: 1024 * 1024 * 20 });
|
|
597
|
+
const durationMs = Date.now() - started;
|
|
598
|
+
const warnings = parseAscWarnings(result.stderr || '');
|
|
599
|
+
const diagnostics = [];
|
|
600
|
+
if (result.error || result.status !== 0) {
|
|
601
|
+
diagnostics.push(diagnosticFromParts({
|
|
602
|
+
phase: 'wasm-host-bridge',
|
|
603
|
+
code: 'PULSEWASM_WASM_HOST_BRIDGE_COMPILE_FAILED',
|
|
604
|
+
message: 'Phase 10D Wasm host bridge compile failed.',
|
|
605
|
+
hint: 'Inspect wasm-host-bridge.json result.stderr for AssemblyScript diagnostics.',
|
|
606
|
+
details: { exitCode: result.status, stderr: result.stderr || '', stdout: result.stdout || '', error: result.error ? result.error.message : undefined }
|
|
607
|
+
}));
|
|
608
|
+
}
|
|
609
|
+
if (warnings.length > 0) {
|
|
610
|
+
diagnostics.push(diagnosticFromParts({
|
|
611
|
+
phase: 'wasm-host-bridge',
|
|
612
|
+
code: 'PULSEWASM_WASM_HOST_BRIDGE_WARNINGS',
|
|
613
|
+
message: 'Phase 10D Wasm host bridge emitted AssemblyScript warnings.',
|
|
614
|
+
hint: 'Warnings are treated as failures at this ABI boundary.',
|
|
615
|
+
details: { warnings }
|
|
616
|
+
}));
|
|
617
|
+
}
|
|
618
|
+
return { asc, args, wasmFile, watFile, durationMs, warnings, result, diagnostics };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function instantiateWithoutBroadcaster(wasmFile) {
|
|
622
|
+
try {
|
|
623
|
+
const wasm = fs.readFileSync(wasmFile);
|
|
624
|
+
const module = new WebAssembly.Module(wasm);
|
|
625
|
+
const importObject = { env: { abort() { throw new Error('abort'); } }, pulsewasm_handlers: {} };
|
|
626
|
+
for (const imp of WebAssembly.Module.imports(module)) if (imp.module === 'pulsewasm_handlers') importObject.pulsewasm_handlers[imp.name] = () => 0;
|
|
627
|
+
new WebAssembly.Instance(module, importObject);
|
|
628
|
+
return { missingBroadcasterRejected: false, error: undefined };
|
|
629
|
+
} catch (error) {
|
|
630
|
+
return { missingBroadcasterRejected: true, error: error && error.message ? error.message : String(error) };
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function instantiateAndRunBridge(wasmFile) {
|
|
635
|
+
const wasm = fs.readFileSync(wasmFile);
|
|
636
|
+
const module = new WebAssembly.Module(wasm);
|
|
637
|
+
const imports = WebAssembly.Module.imports(module);
|
|
638
|
+
const exports = WebAssembly.Module.exports(module);
|
|
639
|
+
let instance;
|
|
640
|
+
const encoder = new TextEncoder();
|
|
641
|
+
const decoder = new TextDecoder();
|
|
642
|
+
const handlerCalls = {};
|
|
643
|
+
const observedParams = {};
|
|
644
|
+
const observedState = {};
|
|
645
|
+
const observedHeaders = {};
|
|
646
|
+
const broadcasterCalls = [];
|
|
647
|
+
|
|
648
|
+
function memory() { return instance.exports.memory; }
|
|
649
|
+
function allocBytes(text) {
|
|
650
|
+
const bytes = encoder.encode(text);
|
|
651
|
+
const ptr = instance.exports.pulse_alloc(bytes.length || 1);
|
|
652
|
+
new Uint8Array(memory().buffer, ptr, bytes.length).set(bytes);
|
|
653
|
+
return { ptr, len: bytes.length, text };
|
|
654
|
+
}
|
|
655
|
+
function readBytes(ptr, len) { return (!ptr || !len) ? '' : decoder.decode(new Uint8Array(memory().buffer, ptr, len)); }
|
|
656
|
+
function readStringRef(ref) {
|
|
657
|
+
if (!ref) return undefined;
|
|
658
|
+
return readBytes(instance.exports.pulse_string_ptr(ref), instance.exports.pulse_string_len(ref));
|
|
659
|
+
}
|
|
660
|
+
function setResultText(ctx, status, body) {
|
|
661
|
+
const encoded = allocBytes(body);
|
|
662
|
+
instance.exports.pulse_result_text(ctx, status, encoded.ptr, encoded.len);
|
|
663
|
+
}
|
|
664
|
+
function getParam(ctx, name) {
|
|
665
|
+
const encoded = allocBytes(name);
|
|
666
|
+
return readStringRef(instance.exports.pulse_ctx_param(ctx, encoded.ptr, encoded.len));
|
|
667
|
+
}
|
|
668
|
+
function stateSet(ctx, key, value) {
|
|
669
|
+
const k = allocBytes(key); const v = allocBytes(value);
|
|
670
|
+
instance.exports.pulse_ctx_state_set(ctx, k.ptr, k.len, v.ptr, v.len);
|
|
671
|
+
}
|
|
672
|
+
function stateGet(ctx, key) {
|
|
673
|
+
const k = allocBytes(key);
|
|
674
|
+
return readStringRef(instance.exports.pulse_ctx_state_get(ctx, k.ptr, k.len));
|
|
675
|
+
}
|
|
676
|
+
function requestHeaderFirst(ctx, name) {
|
|
677
|
+
const n = allocBytes(name);
|
|
678
|
+
return readStringRef(instance.exports.pulse_request_header_first(ctx, n.ptr, n.len));
|
|
679
|
+
}
|
|
680
|
+
function requestHeaderCount(ctx, name) {
|
|
681
|
+
const n = allocBytes(name);
|
|
682
|
+
return instance.exports.pulse_request_header_count(ctx, n.ptr, n.len);
|
|
683
|
+
}
|
|
684
|
+
function requestHeaderAt(ctx, name, index) {
|
|
685
|
+
const n = allocBytes(name);
|
|
686
|
+
return readStringRef(instance.exports.pulse_request_header_at(ctx, n.ptr, n.len, index));
|
|
687
|
+
}
|
|
688
|
+
function responseHeaderSet(ctx, name, value) {
|
|
689
|
+
const n = allocBytes(name); const v = allocBytes(value);
|
|
690
|
+
instance.exports.pulse_response_header_set(ctx, n.ptr, n.len, v.ptr, v.len);
|
|
691
|
+
}
|
|
692
|
+
function responseHeaderAppend(ctx, name, value) {
|
|
693
|
+
const n = allocBytes(name); const v = allocBytes(value);
|
|
694
|
+
instance.exports.pulse_response_header_append(ctx, n.ptr, n.len, v.ptr, v.len);
|
|
695
|
+
}
|
|
696
|
+
function responseHeaderDelete(ctx, name) {
|
|
697
|
+
const n = allocBytes(name);
|
|
698
|
+
instance.exports.pulse_response_header_delete(ctx, n.ptr, n.len);
|
|
699
|
+
}
|
|
700
|
+
function requestHeadersClear() { instance.exports.pulse_bridge_request_headers_clear(); }
|
|
701
|
+
function requestHeaderAppend(name, value) {
|
|
702
|
+
const n = allocBytes(name); const v = allocBytes(value);
|
|
703
|
+
instance.exports.pulse_bridge_request_header_append(n.ptr, n.len, v.ptr, v.len);
|
|
704
|
+
}
|
|
705
|
+
function readResultHeaders() {
|
|
706
|
+
const headers = [];
|
|
707
|
+
const count = instance.exports.pulse_result_header_count();
|
|
708
|
+
for (let i = 0; i < count; i += 1) {
|
|
709
|
+
headers.push({
|
|
710
|
+
name: readBytes(instance.exports.pulse_result_header_name_ptr(i), instance.exports.pulse_result_header_name_len(i)),
|
|
711
|
+
value: readBytes(instance.exports.pulse_result_header_value_ptr(i), instance.exports.pulse_result_header_value_len(i))
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
return headers;
|
|
715
|
+
}
|
|
716
|
+
function headerValues(headers, name) {
|
|
717
|
+
const lower = String(name).toLowerCase();
|
|
718
|
+
return headers.filter((header) => String(header.name).toLowerCase() === lower).map((header) => header.value);
|
|
719
|
+
}
|
|
720
|
+
function makeError(code, message, status) {
|
|
721
|
+
const c = allocBytes(code); const m = allocBytes(message);
|
|
722
|
+
return instance.exports.pulse_error(c.ptr, c.len, m.ptr, m.len, status);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const importObject = {
|
|
726
|
+
env: { abort(message, file, line, column) { throw new Error(`AssemblyScript abort(${message}, ${file}, ${line}, ${column})`); } },
|
|
727
|
+
pulsewasm_handlers: {},
|
|
728
|
+
pulsewasm_broadcaster: { broadcast(channelsRef, ctxRef) { broadcasterCalls.push({ channelsRef, ctxRef }); return 0; } }
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
for (const imp of imports) {
|
|
732
|
+
if (imp.module === 'pulsewasm_handlers') {
|
|
733
|
+
importObject.pulsewasm_handlers[imp.name] = (a, b, c) => {
|
|
734
|
+
handlerCalls[imp.name] = (handlerCalls[imp.name] || 0) + 1;
|
|
735
|
+
if (imp.name === 'auth') {
|
|
736
|
+
const first = requestHeaderFirst(a, 'X-Pulse-Test');
|
|
737
|
+
const multiCount = requestHeaderCount(a, 'X-Pulse-Multi');
|
|
738
|
+
const multiSecond = requestHeaderAt(a, 'X-Pulse-Multi', 1);
|
|
739
|
+
if (first !== undefined && observedHeaders.requestFirst === undefined) observedHeaders.requestFirst = first;
|
|
740
|
+
if (multiCount > (observedHeaders.requestMultiCount || 0)) observedHeaders.requestMultiCount = multiCount;
|
|
741
|
+
if (multiSecond !== undefined && observedHeaders.requestMultiSecond === undefined) observedHeaders.requestMultiSecond = multiSecond;
|
|
742
|
+
stateSet(a, 'auth', '1');
|
|
743
|
+
instance.exports.pulse_next(a);
|
|
744
|
+
return 0;
|
|
745
|
+
}
|
|
746
|
+
if (imp.name === 'gateway') {
|
|
747
|
+
observedState.gatewaySawAuth = stateGet(a, 'auth');
|
|
748
|
+
instance.exports.pulse_next(a);
|
|
749
|
+
return 0;
|
|
750
|
+
}
|
|
751
|
+
if (imp.name === 'computeHandler') {
|
|
752
|
+
const id = getParam(a, 'id');
|
|
753
|
+
observedParams.computeId = id;
|
|
754
|
+
responseHeaderAppend(a, 'Temp-Header', 'remove-me');
|
|
755
|
+
responseHeaderDelete(a, 'Temp-Header');
|
|
756
|
+
responseHeaderSet(a, 'Content-Type', 'text/plain');
|
|
757
|
+
setResultText(a, 201, `compute:${id}`);
|
|
758
|
+
return 0;
|
|
759
|
+
}
|
|
760
|
+
if (imp.name === 'realtimeHandler') {
|
|
761
|
+
const id = getParam(a, 'id');
|
|
762
|
+
observedParams.realtimeId = id;
|
|
763
|
+
responseHeaderSet(a, 'Content-Type', 'text/plain');
|
|
764
|
+
responseHeaderSet(a, 'Grip-Hold', 'stream');
|
|
765
|
+
responseHeaderAppend(a, 'Grip-Channel', 'realtime');
|
|
766
|
+
responseHeaderAppend(a, 'Grip-Channel', `realtime:${id}`);
|
|
767
|
+
setResultText(a, 200, `realtime:${id}`);
|
|
768
|
+
return 0;
|
|
769
|
+
}
|
|
770
|
+
if (imp.name === 'bundleChannel') return 0xC0DE;
|
|
771
|
+
if (imp.name === 'onConnect' || imp.name === 'onDisconnect') return 0;
|
|
772
|
+
if (imp.name.toLowerCase().includes('error')) { instance.exports.pulse_next(a || b || c || 0); return 0; }
|
|
773
|
+
return 0;
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
instance = new WebAssembly.Instance(module, importObject);
|
|
779
|
+
|
|
780
|
+
function executeRequest(methodCode, requestPath, headers = []) {
|
|
781
|
+
requestHeadersClear();
|
|
782
|
+
for (const header of headers) requestHeaderAppend(header.name, header.value);
|
|
783
|
+
const pathBytes = allocBytes(requestPath);
|
|
784
|
+
const status = instance.exports.pulse_execute_request(methodCode, pathBytes.ptr, pathBytes.len);
|
|
785
|
+
const resultHeaders = readResultHeaders();
|
|
786
|
+
return {
|
|
787
|
+
status,
|
|
788
|
+
resultStatus: instance.exports.pulse_result_status(),
|
|
789
|
+
resultKind: instance.exports.pulse_result_kind(),
|
|
790
|
+
body: readBytes(instance.exports.pulse_result_body_ptr(), instance.exports.pulse_result_body_len()),
|
|
791
|
+
headers: resultHeaders,
|
|
792
|
+
headerCount: resultHeaders.length,
|
|
793
|
+
gripChannels: headerValues(resultHeaders, 'Grip-Channel'),
|
|
794
|
+
errorRef: instance.exports.pulse_result_error_ref(),
|
|
795
|
+
streamRef: instance.exports.pulse_result_stream_ref ? instance.exports.pulse_result_stream_ref() : 0,
|
|
796
|
+
responseRef: instance.exports.pulse_result_response_ref ? instance.exports.pulse_result_response_ref() : 0,
|
|
797
|
+
lastChannelsRef: instance.exports.pulse_bridge_last_channels_ref(),
|
|
798
|
+
lastBroadcastResult: instance.exports.pulse_bridge_last_broadcast_result(),
|
|
799
|
+
invokedHandlers: instance.exports.pulse_bridge_last_invoked_handlers()
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function executeLifecycle(kind, methodCode, requestPath) {
|
|
804
|
+
const pathBytes = allocBytes(requestPath);
|
|
805
|
+
const fn = kind === 'connect' ? instance.exports.pulse_execute_connect : instance.exports.pulse_execute_disconnect;
|
|
806
|
+
return fn(methodCode, pathBytes.ptr, pathBytes.len);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const methodGet = instance.exports.pulse_bridge_method_get();
|
|
810
|
+
const methodPost = instance.exports.pulse_bridge_method_post();
|
|
811
|
+
const realtime = executeRequest(methodGet, '/realtime/abc', [
|
|
812
|
+
{ name: 'X-Pulse-Test', value: 'edge' },
|
|
813
|
+
{ name: 'X-Pulse-Multi', value: 'one' },
|
|
814
|
+
{ name: 'x-pulse-multi', value: 'two' }
|
|
815
|
+
]);
|
|
816
|
+
const compute = executeRequest(methodPost, '/compute/42', [{ name: 'X-Pulse-Test', value: 'edge' }]);
|
|
817
|
+
const missing = executeRequest(methodGet, '/missing');
|
|
818
|
+
const connectInvoked = executeLifecycle('connect', methodGet, '/realtime/abc');
|
|
819
|
+
const disconnectInvoked = executeLifecycle('disconnect', methodGet, '/realtime/abc');
|
|
820
|
+
const error = makeError('PULSEWASM_TEST', 'host bridge error', 599);
|
|
821
|
+
const errorCode = readStringRef(instance.exports.pulse_error_code_ref(error));
|
|
822
|
+
const errorMessage = readStringRef(instance.exports.pulse_error_message_ref(error));
|
|
823
|
+
|
|
824
|
+
const checks = [
|
|
825
|
+
{ name: 'route_count', expected: 2, actual: instance.exports.pulse_bridge_route_count() },
|
|
826
|
+
{ name: 'execution_entry_count', expected: 8, actual: instance.exports.pulse_bridge_execution_entry_count() },
|
|
827
|
+
{ name: 'handler_binding_count', expected: 7, actual: instance.exports.pulse_bridge_handler_binding_count() },
|
|
828
|
+
{ name: 'request_realtime_status', expected: 200, actual: realtime.status },
|
|
829
|
+
{ name: 'request_realtime_body', expected: 'realtime:abc', actual: realtime.body },
|
|
830
|
+
{ name: 'request_realtime_param', expected: 'abc', actual: observedParams.realtimeId },
|
|
831
|
+
{ name: 'request_realtime_broadcaster', expected: 1, actual: broadcasterCalls.length >= 1 ? 1 : 0 },
|
|
832
|
+
{ name: 'request_header_first', expected: 'edge', actual: observedHeaders.requestFirst },
|
|
833
|
+
{ name: 'request_header_multi_count', expected: 2, actual: observedHeaders.requestMultiCount },
|
|
834
|
+
{ name: 'request_header_multi_second', expected: 'two', actual: observedHeaders.requestMultiSecond },
|
|
835
|
+
{ name: 'response_header_count', expected: 4, actual: realtime.headerCount },
|
|
836
|
+
{ name: 'response_header_grip_channel_count', expected: 2, actual: realtime.gripChannels.length },
|
|
837
|
+
{ name: 'response_header_grip_channel_second', expected: 'realtime:abc', actual: realtime.gripChannels[1] },
|
|
838
|
+
{ name: 'request_compute_status', expected: 201, actual: compute.status },
|
|
839
|
+
{ name: 'request_compute_body', expected: 'compute:42', actual: compute.body },
|
|
840
|
+
{ name: 'response_header_delete', expected: 0, actual: headerValues(compute.headers, 'Temp-Header').length },
|
|
841
|
+
{ name: 'request_compute_param', expected: '42', actual: observedParams.computeId },
|
|
842
|
+
{ name: 'ctx_state_map_bridge', expected: '1', actual: observedState.gatewaySawAuth },
|
|
843
|
+
{ name: 'missing_fallthrough_status', expected: 404, actual: missing.status },
|
|
844
|
+
{ name: 'connect_lifecycle_invoked', expected: 1, actual: connectInvoked },
|
|
845
|
+
{ name: 'disconnect_lifecycle_invoked', expected: 1, actual: disconnectInvoked },
|
|
846
|
+
{ name: 'error_code_ref', expected: 'PULSEWASM_TEST', actual: errorCode },
|
|
847
|
+
{ name: 'error_message_ref', expected: 'host bridge error', actual: errorMessage },
|
|
848
|
+
{ name: 'handler_auth_called', expected: 1, actual: handlerCalls.auth >= 1 ? 1 : 0 },
|
|
849
|
+
{ name: 'handler_gateway_called', expected: 1, actual: handlerCalls.gateway >= 1 ? 1 : 0 },
|
|
850
|
+
{ name: 'handler_realtime_called', expected: 1, actual: handlerCalls.realtimeHandler >= 1 ? 1 : 0 },
|
|
851
|
+
{ name: 'handler_compute_called', expected: 1, actual: handlerCalls.computeHandler >= 1 ? 1 : 0 },
|
|
852
|
+
{ name: 'handler_channel_called', expected: 1, actual: handlerCalls.bundleChannel >= 1 ? 1 : 0 }
|
|
853
|
+
].map((check) => ({ ...check, status: check.actual === check.expected ? 'ok' : 'error', message: check.actual === check.expected ? undefined : `Expected ${check.expected}, got ${check.actual}.` }));
|
|
854
|
+
|
|
855
|
+
return {
|
|
856
|
+
imports,
|
|
857
|
+
exports,
|
|
858
|
+
checks,
|
|
859
|
+
handlerCalls,
|
|
860
|
+
broadcasterCalls,
|
|
861
|
+
observedParams,
|
|
862
|
+
observedState,
|
|
863
|
+
observedHeaders,
|
|
864
|
+
results: { realtime, compute, missing, connectInvoked, disconnectInvoked, errorCode, errorMessage },
|
|
865
|
+
memoryExported: Boolean(instance.exports.memory),
|
|
866
|
+
instanceExports: Object.keys(instance.exports)
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function buildArtifact(options = {}) {
|
|
871
|
+
const cwd = options.cwd || process.cwd();
|
|
872
|
+
const diagnostics = options.diagnostics || [];
|
|
873
|
+
const checks = options.bridge?.checks || [];
|
|
874
|
+
const failedChecks = checks.filter((check) => check.status !== 'ok');
|
|
875
|
+
return normalizeArtifact({
|
|
876
|
+
version: WASM_BRIDGE_VERSION,
|
|
877
|
+
generatedBy: options.generatedBy || PACKAGE_VERSION,
|
|
878
|
+
status: diagnostics.length || failedChecks.length ? 'error' : 'ok',
|
|
879
|
+
sourceWasmHostAbiVersion: options.wasmHostAbi?.artifact?.version,
|
|
880
|
+
sourceAssemblyScriptCoreVersion: options.core?.artifact?.version,
|
|
881
|
+
policy: {
|
|
882
|
+
phase: WASM_BRIDGE_PHASE,
|
|
883
|
+
sourceOfTruth: 'wasm-host-abi.json + request-result-headers.json + generated/as/*.as.ts',
|
|
884
|
+
reReadsSource: false,
|
|
885
|
+
reInfersRouteSemantics: false,
|
|
886
|
+
compiler: 'assemblyscript asc',
|
|
887
|
+
wasmCompiled: true,
|
|
888
|
+
wasmInstantiated: Boolean(options.bridge),
|
|
889
|
+
wasmExecutedThroughHostAbi: Boolean(options.bridge),
|
|
890
|
+
hostBridgeImplemented: true,
|
|
891
|
+
platformBindings: false,
|
|
892
|
+
witBindings: false,
|
|
893
|
+
wasiHttp: false,
|
|
894
|
+
stringAbi: 'host writes UTF-8 through pulse_alloc; wasm decodes ptr + byteLen',
|
|
895
|
+
handlers: 'host imports call exported continuation/result/ctx/header operations',
|
|
896
|
+
headers: 'ordered repeated request/response headers implemented; result headers exported by iteration',
|
|
897
|
+
bodyPolicy: 'request body deferred; RESULT_TEXT/JSON/EMPTY implemented; RESULT_STREAM implemented for host-owned pass-through; RESULT_BINARY reserved',
|
|
898
|
+
broadcaster: 'host import required; missing broadcaster rejected during instantiation'
|
|
899
|
+
},
|
|
900
|
+
compiler: {
|
|
901
|
+
package: 'assemblyscript',
|
|
902
|
+
version: options.assemblyScriptVersion,
|
|
903
|
+
executable: options.asc?.display
|
|
904
|
+
},
|
|
905
|
+
command: options.command,
|
|
906
|
+
result: options.result ? { exitCode: options.result.status, signal: options.result.signal, durationMs: options.durationMs, stdout: String(options.result.stdout || ''), stderr: String(options.result.stderr || '') } : undefined,
|
|
907
|
+
generated: { inputFiles: options.inputFiles || [], outputFiles: options.outputFiles || [] },
|
|
908
|
+
wasmModule: options.bridge ? { imports: options.bridge.imports, exports: options.bridge.exports, memoryExported: options.bridge.memoryExported, instanceExports: options.bridge.instanceExports } : undefined,
|
|
909
|
+
bridge: options.bridge ? {
|
|
910
|
+
checks: options.bridge.checks,
|
|
911
|
+
failedChecks,
|
|
912
|
+
handlerCalls: options.bridge.handlerCalls,
|
|
913
|
+
broadcasterCalls: options.bridge.broadcasterCalls.length,
|
|
914
|
+
observedParams: options.bridge.observedParams,
|
|
915
|
+
observedState: options.bridge.observedState,
|
|
916
|
+
observedHeaders: options.bridge.observedHeaders,
|
|
917
|
+
results: options.bridge.results
|
|
918
|
+
} : undefined,
|
|
919
|
+
missingBroadcaster: options.missingBroadcaster,
|
|
920
|
+
diagnostics,
|
|
921
|
+
warnings: options.warnings || [],
|
|
922
|
+
summary: {
|
|
923
|
+
compiled: Boolean(options.outputFiles && options.outputFiles.length),
|
|
924
|
+
instantiated: Boolean(options.bridge),
|
|
925
|
+
executed: Boolean(options.bridge),
|
|
926
|
+
hostBridge: Boolean(options.bridge),
|
|
927
|
+
checks: checks.length,
|
|
928
|
+
failedChecks: failedChecks.length,
|
|
929
|
+
handlerCalls: options.bridge ? Object.values(options.bridge.handlerCalls).reduce((sum, value) => sum + Number(value || 0), 0) : 0,
|
|
930
|
+
broadcasterCalls: options.bridge ? options.bridge.broadcasterCalls.length : 0,
|
|
931
|
+
headersAdapted: Boolean(options.bridge),
|
|
932
|
+
requestHeaderChecks: checks.filter((check) => check.name.startsWith('request_header_')).length,
|
|
933
|
+
responseHeaderChecks: checks.filter((check) => check.name.startsWith('response_header_')).length,
|
|
934
|
+
repeatedHeadersPreserved: Boolean(options.bridge?.results?.realtime?.gripChannels?.length === 2),
|
|
935
|
+
missingBroadcasterRejected: Boolean(options.missingBroadcaster?.missingBroadcasterRejected),
|
|
936
|
+
wasmBytes: options.outputFiles?.find((file) => file.kind === 'wasm-host-bridge-output')?.bytes || 0,
|
|
937
|
+
watBytes: options.outputFiles?.find((file) => file.kind === 'wat-host-bridge-output')?.bytes || 0,
|
|
938
|
+
diagnostics: diagnostics.length,
|
|
939
|
+
warnings: (options.warnings || []).length,
|
|
940
|
+
platformBindings: false,
|
|
941
|
+
witBindings: false
|
|
942
|
+
}
|
|
943
|
+
}, cwd);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function buildWasmHostBridge(shape, core, wasmHostAbi, options = {}) {
|
|
947
|
+
const cwd = options.cwd || process.cwd();
|
|
948
|
+
const generatedBy = options.generatedBy || PACKAGE_VERSION;
|
|
949
|
+
const diagnostics = [];
|
|
950
|
+
const bridgeRunnerText = buildBridgeRunnerSource();
|
|
951
|
+
const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulsewasm-host-bridge-'));
|
|
952
|
+
const inputFiles = writeStagedFiles(stagingDir, shape, core, bridgeRunnerText);
|
|
953
|
+
const compile = compileBridge(stagingDir, cwd, options);
|
|
954
|
+
diagnostics.push(...(compile.diagnostics || []));
|
|
955
|
+
|
|
956
|
+
let outputFiles = [];
|
|
957
|
+
let bridge;
|
|
958
|
+
let missingBroadcaster;
|
|
959
|
+
if (diagnostics.length === 0 && compile.wasmFile && fs.existsSync(compile.wasmFile)) {
|
|
960
|
+
outputFiles = [fileRecord(compile.wasmFile, cwd, 'wasm-host-bridge-output'), fileRecord(compile.watFile, cwd, 'wat-host-bridge-output')];
|
|
961
|
+
try {
|
|
962
|
+
missingBroadcaster = instantiateWithoutBroadcaster(compile.wasmFile);
|
|
963
|
+
bridge = instantiateAndRunBridge(compile.wasmFile);
|
|
964
|
+
const failed = bridge.checks.filter((check) => check.status !== 'ok');
|
|
965
|
+
if (failed.length > 0) diagnostics.push(diagnosticFromParts({ phase: 'wasm-host-bridge', code: 'PULSEWASM_WASM_HOST_BRIDGE_CHECK_FAILED', message: 'Phase 10D Wasm host bridge checks failed.', hint: 'Inspect wasm-host-bridge.json bridge.failedChecks for details.', details: { failed } }));
|
|
966
|
+
if (!missingBroadcaster.missingBroadcasterRejected) diagnostics.push(diagnosticFromParts({ phase: 'wasm-host-bridge', code: 'PULSEWASM_WASM_HOST_BRIDGE_BROADCASTER_NOT_REQUIRED', message: 'The Phase 10D bridge did not reject a missing broadcaster import.', hint: 'Channel-resolving builds must require the broadcaster import at this boundary.', details: missingBroadcaster }));
|
|
967
|
+
} catch (error) {
|
|
968
|
+
diagnostics.push(diagnosticFromParts({ phase: 'wasm-host-bridge', code: 'PULSEWASM_WASM_HOST_BRIDGE_EXECUTION_FAILED', message: 'Phase 10D Wasm host bridge execution failed.', hint: 'Inspect details for the host-side execution error.', details: { message: error && error.message ? error.message : String(error), stack: error && error.stack ? error.stack : undefined } }));
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const command = compile.asc ? {
|
|
973
|
+
executable: compile.asc.display,
|
|
974
|
+
args: [path.relative(cwd, path.join(stagingDir, BRIDGE_RUNNER_FILE)).replace(/\\/g, '/'), '--outFile', path.join(options.outDir ? path.relative(cwd, path.resolve(cwd, options.outDir)) : 'artifacts', BRIDGE_WASM_REL).replace(/\\/g, '/'), '--textFile', path.join(options.outDir ? path.relative(cwd, path.resolve(cwd, options.outDir)) : 'artifacts', BRIDGE_WAT_REL).replace(/\\/g, '/'), '--runtime', 'stub', '--noAssert']
|
|
975
|
+
} : undefined;
|
|
976
|
+
|
|
977
|
+
const artifact = buildArtifact({ cwd, generatedBy, shape, core, wasmHostAbi, diagnostics, warnings: compile.warnings || [], asc: compile.asc, command, result: compile.result, durationMs: compile.durationMs, inputFiles, outputFiles, bridge, missingBroadcaster, assemblyScriptVersion: assemblyScriptVersion(cwd) });
|
|
978
|
+
return { artifact, files: [{ file: BRIDGE_RUNNER_FILE, text: bridgeRunnerText }], outputFiles, diagnostics };
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
module.exports = {
|
|
982
|
+
WASM_BRIDGE_VERSION,
|
|
983
|
+
buildWasmHostBridge
|
|
984
|
+
};
|