@pulse-compute/wasm-compiler 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 +52 -1
- package/bin/provider-proof-composition.js +34 -0
- package/bin/pulsewasm-extract.js +20 -0
- package/package.json +60 -5
- package/src/artifacts-dir.js +13 -0
- package/src/ast-json.js +52 -0
- package/src/build-manifest.js +354 -0
- package/src/canonical-api-compiler.js +245 -0
- package/src/canonical-native-compiler.js +411 -0
- package/src/canonical-native-plan.js +1477 -0
- package/src/canonical-project-compiler.js +1218 -0
- package/src/canonical-router-compiler.js +25 -0
- package/src/cli-intents.js +1235 -0
- package/src/cli.js +1927 -0
- package/src/codegen/assemblyscript-compile.js +3 -0
- package/src/codegen/assemblyscript-core.js +3 -0
- package/src/codegen/assemblyscript-shape.js +3 -0
- package/src/codegen/assemblyscript-wasm-smoke.js +3 -0
- package/src/codegen/backend-capabilities.js +3 -0
- package/src/codegen/channel-broadcaster.js +3 -0
- package/src/codegen/compiled-handlers.js +3 -0
- package/src/codegen/compiled-wasm-runtime.js +3 -0
- package/src/codegen/config-references.js +248 -0
- package/src/codegen/dispatch-ts.js +145 -0
- package/src/codegen/effect-composition.js +331 -0
- package/src/codegen/effect-runtime.js +418 -0
- package/src/codegen/execution-harness-ts.js +467 -0
- package/src/codegen/handler-bindings-ts.js +298 -0
- package/src/codegen/handler-library-contracts.js +3 -0
- package/src/codegen/host-capabilities.js +3 -0
- package/src/codegen/host-runtime-kernel.js +3 -0
- package/src/codegen/integrated-compiled-app.js +3 -0
- package/src/codegen/json-body.js +3 -0
- package/src/codegen/library-sidecars.js +3 -0
- package/src/codegen/local-harness-ts.js +453 -0
- package/src/codegen/pulse-wrapper.js +217 -0
- package/src/codegen/request-result-headers.js +3 -0
- package/src/codegen/schema-json-compile.js +3 -0
- package/src/codegen/schema-json-sidecar-v2.js +3 -0
- package/src/codegen/schema-json-sidecar.js +3 -0
- package/src/codegen/streaming-passthrough.js +3 -0
- package/src/codegen/wasm-host-abi.js +3 -0
- package/src/codegen/wasm-host-bridge.js +3 -0
- package/src/compiled-wasm-host-runtime-kv.js +3 -0
- package/src/config-resolver.js +813 -0
- package/src/crypto-requirement-planner.js +89 -0
- package/src/definitions/config-schema.js +14 -0
- package/src/definitions/handler-roles.js +14 -0
- package/src/definitions/path-grammar.js +14 -0
- package/src/definitions/router-api.js +14 -0
- package/src/diagnostics/codes.js +14 -0
- package/src/diagnostics/reporter.js +14 -0
- package/src/diagnostics.js +14 -0
- package/src/dispatch-table.js +400 -0
- package/src/events/event-emit.js +265 -0
- package/src/events/event-topology.js +127 -0
- package/src/execution-plan.js +463 -0
- package/src/extractor.js +2816 -0
- package/src/handler-eval.js +1154 -0
- package/src/handler-table.js +326 -0
- package/src/index.js +19 -0
- package/src/javascript-application-plan.js +181 -0
- package/src/kv-provider.js +3 -0
- package/src/path-table.js +60 -0
- package/src/path.js +14 -0
- package/src/patterns/config-define.js +30 -0
- package/src/patterns/dependency-call.js +18 -0
- package/src/patterns/env-lookup.js +13 -0
- package/src/patterns/handler-reference.js +29 -0
- package/src/patterns/path-literal.js +35 -0
- package/src/patterns/result.js +15 -0
- package/src/patterns/router-chain-call.js +50 -0
- package/src/patterns/router-construction.js +19 -0
- package/src/project/package-reachability.js +782 -0
- package/src/project/reachable-graph-builder.js +992 -0
- package/src/project/reachable-graph-contract.js +54 -0
- package/src/project/reachable-graph-implementation.js +36 -0
- package/src/project/router-module-linker.js +710 -0
- package/src/project-config-compiler.js +222 -0
- package/src/project-target-support.js +500 -0
- package/src/provider-toolchain.js +299 -0
- package/src/spine/async-surface-normalizer.js +312 -0
- package/src/spine/canonical-handler-ir.js +335 -0
- package/src/spine/canonical-native-module.js +87 -0
- package/src/spine/canonical-native-plan.js +89 -0
- package/src/spine/canonical-project.js +76 -0
- package/src/spine/canonical-router.js +155 -0
- package/src/spine/canonical-source.js +165 -0
- package/src/spine/diagnostic-authority.js +290 -0
- package/src/spine/equivalence.js +262 -0
- package/src/spine/guest-unit-stage.js +87 -0
- package/src/spine/handler-ir-emitter.js +454 -0
- package/src/spine/handler-ir-managed.js +1597 -0
- package/src/spine/handler-ir.js +797 -0
- package/src/spine/handler-surface-authority.js +585 -0
- package/src/spine/package-operation-seam.js +1015 -0
- package/src/spine/pipeline.js +202 -0
- package/src/spine/plain-handler-frontend.js +537 -0
- package/src/spine/provider-requirement-authority.js +208 -0
- package/src/spine/router-control-contract.js +17 -0
- package/src/spine/router-handler-frontend.js +513 -0
- package/src/spine/router-handler-ir.js +372 -0
- package/src/spine/router-topology-frontend.js +635 -0
- package/src/stable-id.js +14 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
CRYPTO_REQUIREMENTS_VERSION,
|
|
5
|
+
CRYPTO_SEMANTIC_OWNER,
|
|
6
|
+
normalizeCryptoRequirements,
|
|
7
|
+
planCryptoRealizations
|
|
8
|
+
} = require('@pulse-compute/wasm-contracts/crypto/contracts');
|
|
9
|
+
|
|
10
|
+
function requirementError(code, message, details = {}) {
|
|
11
|
+
const error = new TypeError(message);
|
|
12
|
+
error.code = code;
|
|
13
|
+
error.details = Object.freeze({ ...details });
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Collect package-owned requirements after reachability is known. Entries not
|
|
19
|
+
* explicitly marked reachable are ignored so installation alone cannot create
|
|
20
|
+
* capability demand.
|
|
21
|
+
*/
|
|
22
|
+
function collectReachableCryptoRequirements(inputs = []) {
|
|
23
|
+
if (!Array.isArray(inputs)) {
|
|
24
|
+
throw requirementError(
|
|
25
|
+
'PULSE_CRYPTO_REQUIREMENT_COLLECTION_ARRAY_REQUIRED',
|
|
26
|
+
'Package crypto requirement declarations must be an array.'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const normalizedInputs = [];
|
|
30
|
+
for (const [index, input] of inputs.entries()) {
|
|
31
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
32
|
+
throw requirementError(
|
|
33
|
+
'PULSE_CRYPTO_REQUIREMENT_COLLECTION_OBJECT_REQUIRED',
|
|
34
|
+
`Package crypto requirement declaration ${index + 1} must be an object.`,
|
|
35
|
+
{ index }
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (input.reachable !== true) continue;
|
|
39
|
+
if (typeof input.requestedBy !== 'string' || input.requestedBy.trim().length === 0) {
|
|
40
|
+
throw requirementError(
|
|
41
|
+
'PULSE_CRYPTO_REQUIREMENT_OWNER_REQUIRED',
|
|
42
|
+
`Reachable package crypto requirement declaration ${index + 1} must identify its owner.`,
|
|
43
|
+
{ index }
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (input.semanticOwner !== undefined && input.semanticOwner !== CRYPTO_SEMANTIC_OWNER) {
|
|
47
|
+
throw requirementError(
|
|
48
|
+
'PULSE_CRYPTO_REQUIREMENT_SEMANTIC_OWNER_INVALID',
|
|
49
|
+
`Reachable package crypto requirement declaration ${index + 1} must name ${CRYPTO_SEMANTIC_OWNER} as semantic owner.`,
|
|
50
|
+
{ index, requestedBy: input.requestedBy.trim(), semanticOwner: input.semanticOwner }
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (!Array.isArray(input.algorithms)) {
|
|
54
|
+
throw requirementError(
|
|
55
|
+
'PULSE_CRYPTO_REQUIREMENT_ALGORITHMS_ARRAY_REQUIRED',
|
|
56
|
+
`Reachable package crypto requirement declaration ${index + 1} must provide an algorithms array.`,
|
|
57
|
+
{ index, requestedBy: input.requestedBy.trim() }
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
for (const algorithm of input.algorithms) {
|
|
61
|
+
normalizedInputs.push({
|
|
62
|
+
algorithm,
|
|
63
|
+
requestedBy: input.requestedBy.trim(),
|
|
64
|
+
semanticOwner: CRYPTO_SEMANTIC_OWNER
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return normalizeCryptoRequirements(normalizedInputs);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function planProjectCrypto(input = {}) {
|
|
72
|
+
const requirements = input.requirements && input.requirements.version === CRYPTO_REQUIREMENTS_VERSION
|
|
73
|
+
? input.requirements
|
|
74
|
+
: collectReachableCryptoRequirements(input.packageRequirements || []);
|
|
75
|
+
return planCryptoRealizations({
|
|
76
|
+
declaration: input.declaration,
|
|
77
|
+
requirements,
|
|
78
|
+
targetDescriptor: input.targetDescriptor,
|
|
79
|
+
target: input.target,
|
|
80
|
+
profile: input.profile,
|
|
81
|
+
profileSelectionSource: input.profileSelectionSource,
|
|
82
|
+
configurationSource: input.configurationSource
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = Object.freeze({
|
|
87
|
+
collectReachableCryptoRequirements,
|
|
88
|
+
planProjectCrypto
|
|
89
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function loadContractsConfigSchema() {
|
|
4
|
+
try {
|
|
5
|
+
return require('@pulse-compute/wasm-contracts/definitions/config-schema');
|
|
6
|
+
} catch (error) {
|
|
7
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
8
|
+
return require('../../../contracts/src/definitions/config-schema.js');
|
|
9
|
+
}
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = loadContractsConfigSchema();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function loadContractsHandlerRoles() {
|
|
4
|
+
try {
|
|
5
|
+
return require('@pulse-compute/wasm-contracts/definitions/handler-roles');
|
|
6
|
+
} catch (error) {
|
|
7
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
8
|
+
return require('../../../contracts/src/definitions/handler-roles.js');
|
|
9
|
+
}
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = loadContractsHandlerRoles();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function loadContractsPathGrammar() {
|
|
4
|
+
try {
|
|
5
|
+
return require('@pulse-compute/wasm-contracts/definitions/path-grammar');
|
|
6
|
+
} catch (error) {
|
|
7
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
8
|
+
return require('../../../contracts/src/definitions/path-grammar.js');
|
|
9
|
+
}
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = loadContractsPathGrammar();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function loadContractsRouterApi() {
|
|
4
|
+
try {
|
|
5
|
+
return require('@pulse-compute/wasm-contracts/definitions/router-api');
|
|
6
|
+
} catch (error) {
|
|
7
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
8
|
+
return require('../../../contracts/src/definitions/router-api.js');
|
|
9
|
+
}
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = loadContractsRouterApi();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function loadContractsDiagnosticCodes() {
|
|
4
|
+
try {
|
|
5
|
+
return require('@pulse-compute/wasm-contracts/diagnostics/codes');
|
|
6
|
+
} catch (error) {
|
|
7
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
8
|
+
return require('../../../contracts/src/diagnostics/codes.js');
|
|
9
|
+
}
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = loadContractsDiagnosticCodes();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function loadContractsDiagnosticReporter() {
|
|
4
|
+
try {
|
|
5
|
+
return require('@pulse-compute/wasm-contracts/diagnostics/reporter');
|
|
6
|
+
} catch (error) {
|
|
7
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
8
|
+
return require('../../../contracts/src/diagnostics/reporter.js');
|
|
9
|
+
}
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = loadContractsDiagnosticReporter();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function loadContractsDiagnostics() {
|
|
4
|
+
try {
|
|
5
|
+
return require('@pulse-compute/wasm-contracts/diagnostics');
|
|
6
|
+
} catch (error) {
|
|
7
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
8
|
+
return require('../../contracts/src/diagnostics.js');
|
|
9
|
+
}
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = loadContractsDiagnostics();
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { PACKAGE_VERSION, normalizeArtifact } = require('./diagnostics.js');
|
|
4
|
+
const { matchRoutePath } = require('./path.js');
|
|
5
|
+
const { DISPATCH_POLICY, DISPATCH_TABLE_VERSION } = loadContractsDispatchTable();
|
|
6
|
+
|
|
7
|
+
function loadContractsDispatchTable() {
|
|
8
|
+
try {
|
|
9
|
+
return require('@pulse-compute/wasm-contracts/dispatch-table');
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
|
|
12
|
+
return require('../../contracts/src/dispatch-table.js');
|
|
13
|
+
}
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function compareHandlers(a, b) {
|
|
19
|
+
const ao = Number.isInteger(a.order) ? a.order : Number.MAX_SAFE_INTEGER;
|
|
20
|
+
const bo = Number.isInteger(b.order) ? b.order : Number.MAX_SAFE_INTEGER;
|
|
21
|
+
if (ao !== bo) return ao - bo;
|
|
22
|
+
return String(a.id).localeCompare(String(b.id));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeDiagnostic(code, message, hint, details = {}, loc) {
|
|
26
|
+
return {
|
|
27
|
+
pass: 'dispatch-table',
|
|
28
|
+
code,
|
|
29
|
+
severity: 'error',
|
|
30
|
+
message,
|
|
31
|
+
hint,
|
|
32
|
+
loc: loc || { file: '<dispatch-table>' },
|
|
33
|
+
details
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function handlerDisplayName(handler) {
|
|
38
|
+
return handler?.localName || handler?.exportName || handler?.id;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findEvalEntry(handlerEval, handlerId) {
|
|
42
|
+
if (!handlerEval || !Array.isArray(handlerEval.handlers)) return undefined;
|
|
43
|
+
return handlerEval.handlers.find((entry) => entry.handlerId === handlerId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function summarizeEval(entry) {
|
|
47
|
+
if (!entry) return { status: 'unknown' };
|
|
48
|
+
return {
|
|
49
|
+
status: entry.status,
|
|
50
|
+
captures: (entry.captures || []).length,
|
|
51
|
+
calls: (entry.calls || []).length,
|
|
52
|
+
deps: (entry.deps || []).length,
|
|
53
|
+
dependencyChain: (entry.dependencyChain || []).length,
|
|
54
|
+
globals: (entry.globals || []).length,
|
|
55
|
+
imports: (entry.imports || []).length,
|
|
56
|
+
unsupported: (entry.unsupported || []).length
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildHandlerSlots(handlerTable, handlerEval, diagnostics) {
|
|
61
|
+
const handlers = Array.isArray(handlerTable?.handlers) ? [...handlerTable.handlers] : [];
|
|
62
|
+
handlers.sort(compareHandlers);
|
|
63
|
+
|
|
64
|
+
const slots = [];
|
|
65
|
+
const byId = new Map();
|
|
66
|
+
handlers.forEach((handler, slot) => {
|
|
67
|
+
const evalEntry = findEvalEntry(handlerEval, handler.id);
|
|
68
|
+
const slotEntry = {
|
|
69
|
+
slot,
|
|
70
|
+
id: handler.id,
|
|
71
|
+
name: handlerDisplayName(handler),
|
|
72
|
+
roles: handler.roles || [],
|
|
73
|
+
kind: handler.kind,
|
|
74
|
+
file: handler.file,
|
|
75
|
+
sourceTextHash: handler.sourceTextHash,
|
|
76
|
+
signature: handler.signature,
|
|
77
|
+
eval: summarizeEval(evalEntry),
|
|
78
|
+
loc: handler.loc
|
|
79
|
+
};
|
|
80
|
+
slots.push(slotEntry);
|
|
81
|
+
byId.set(handler.id, slotEntry);
|
|
82
|
+
|
|
83
|
+
if (!evalEntry) {
|
|
84
|
+
diagnostics.push(makeDiagnostic(
|
|
85
|
+
'PULSEWASM_DISPATCH_HANDLER_EVAL_MISSING',
|
|
86
|
+
`Handler "${handlerDisplayName(handler)}" is missing handler evaluation metadata.`,
|
|
87
|
+
'Run handler-eval before dispatch-table generation.',
|
|
88
|
+
{ handlerId: handler.id },
|
|
89
|
+
handler.loc
|
|
90
|
+
));
|
|
91
|
+
} else if (evalEntry.status !== 'ok') {
|
|
92
|
+
diagnostics.push(makeDiagnostic(
|
|
93
|
+
'PULSEWASM_DISPATCH_HANDLER_EVAL_ERROR',
|
|
94
|
+
`Handler "${handlerDisplayName(handler)}" did not pass handler evaluation.`,
|
|
95
|
+
'Dispatch metadata can only reference handlers with handler-eval status "ok".',
|
|
96
|
+
{ handlerId: handler.id, status: evalEntry.status, unsupported: evalEntry.unsupported || [] },
|
|
97
|
+
handler.loc
|
|
98
|
+
));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return { slots, byId };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function routeLoc(route) {
|
|
106
|
+
return route?.loc || { file: '<route-plan>' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createHandlerUse(handlerId, role, slotById, diagnostics, route, index) {
|
|
110
|
+
if (!handlerId) return undefined;
|
|
111
|
+
const slot = slotById.get(handlerId);
|
|
112
|
+
if (!slot) {
|
|
113
|
+
diagnostics.push(makeDiagnostic(
|
|
114
|
+
'PULSEWASM_DISPATCH_UNKNOWN_HANDLER',
|
|
115
|
+
`Route ${route?.method || '<method>'} ${route?.path || '<path>'} references handler "${handlerId}", but it is not in handler-table.json.`,
|
|
116
|
+
'Every dispatch handler reference must resolve through handler-table.json.',
|
|
117
|
+
{ handlerId, role, runtimeId: route?.runtimeId ?? route?.id, stableId: route?.stableId },
|
|
118
|
+
routeLoc(route)
|
|
119
|
+
));
|
|
120
|
+
return { slot: null, id: handlerId, name: undefined, role, unresolved: true, index };
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
slot: slot.slot,
|
|
124
|
+
id: slot.id,
|
|
125
|
+
name: slot.name,
|
|
126
|
+
role,
|
|
127
|
+
index,
|
|
128
|
+
evalStatus: slot.eval.status
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function createHandlerUses(handlerIds, role, slotById, diagnostics, route) {
|
|
133
|
+
return (handlerIds || [])
|
|
134
|
+
.map((handlerId, index) => createHandlerUse(handlerId, role, slotById, diagnostics, route, index))
|
|
135
|
+
.filter(Boolean);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function createChannelUse(channel, slotById, diagnostics, route) {
|
|
139
|
+
if (!channel) return undefined;
|
|
140
|
+
if (channel.kind === 'static') {
|
|
141
|
+
return { kind: 'static', value: channel.value };
|
|
142
|
+
}
|
|
143
|
+
if (channel.kind === 'handler') {
|
|
144
|
+
const use = createHandlerUse(channel.value, 'channel', slotById, diagnostics, route, 0);
|
|
145
|
+
return {
|
|
146
|
+
kind: 'handler',
|
|
147
|
+
slot: use?.slot ?? null,
|
|
148
|
+
id: channel.value,
|
|
149
|
+
name: channel.name || use?.name,
|
|
150
|
+
evalStatus: use?.evalStatus,
|
|
151
|
+
unresolved: Boolean(use?.unresolved)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
diagnostics.push(makeDiagnostic(
|
|
155
|
+
'PULSEWASM_DISPATCH_UNSUPPORTED_CHANNEL',
|
|
156
|
+
`Unsupported channel metadata kind "${channel.kind}" in route ${route?.method || '<method>'} ${route?.path || '<path>'}.`,
|
|
157
|
+
'Phase 6 accepts static channels and handler channels only.',
|
|
158
|
+
{ channel, runtimeId: route?.runtimeId ?? route?.id, stableId: route?.stableId },
|
|
159
|
+
routeLoc(route)
|
|
160
|
+
));
|
|
161
|
+
return { kind: channel.kind || 'unknown', value: channel.value, unsupported: true };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createParamSlots(route) {
|
|
165
|
+
const params = route.params || [];
|
|
166
|
+
const segments = route.pathPattern?.segments || [];
|
|
167
|
+
return params.map((name) => {
|
|
168
|
+
const segmentIndex = segments.findIndex((segment) => segment.kind === 'param' && segment.name === name);
|
|
169
|
+
return { name, segmentIndex };
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function buildExecution(normal, error, lifecycle, channel) {
|
|
174
|
+
return {
|
|
175
|
+
normal: [
|
|
176
|
+
...normal.middleware.map((use) => ({ kind: 'middleware', slot: use.slot, handlerId: use.id, name: use.name, order: use.index })),
|
|
177
|
+
{ kind: 'handler', slot: normal.handler?.slot ?? null, handlerId: normal.handler?.id, name: normal.handler?.name, order: normal.middleware.length }
|
|
178
|
+
],
|
|
179
|
+
error: error.map((use) => ({ kind: 'error', slot: use.slot, handlerId: use.id, name: use.name, order: use.index })),
|
|
180
|
+
lifecycle: {
|
|
181
|
+
connect: lifecycle.connect.map((use) => ({ kind: 'connect', slot: use.slot, handlerId: use.id, name: use.name, order: use.index })),
|
|
182
|
+
disconnect: lifecycle.disconnect.map((use) => ({ kind: 'disconnect', slot: use.slot, handlerId: use.id, name: use.name, order: use.index }))
|
|
183
|
+
},
|
|
184
|
+
channel
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function validateDenseRuntimeIds(routes, diagnostics) {
|
|
189
|
+
const ids = routes.map((route) => route.runtimeId ?? route.id);
|
|
190
|
+
const seen = new Set();
|
|
191
|
+
ids.forEach((id, index) => {
|
|
192
|
+
if (!Number.isInteger(id)) {
|
|
193
|
+
diagnostics.push(makeDiagnostic(
|
|
194
|
+
'PULSEWASM_DISPATCH_INVALID_RUNTIME_ID',
|
|
195
|
+
`Route at index ${index} has a non-integer runtimeId.`,
|
|
196
|
+
'runtimeId must be a dense integer assigned by the flatten pass.',
|
|
197
|
+
{ index, runtimeId: id },
|
|
198
|
+
routeLoc(routes[index])
|
|
199
|
+
));
|
|
200
|
+
} else if (seen.has(id)) {
|
|
201
|
+
diagnostics.push(makeDiagnostic(
|
|
202
|
+
'PULSEWASM_DISPATCH_DUPLICATE_RUNTIME_ID',
|
|
203
|
+
`Duplicate runtimeId ${id} in route plan.`,
|
|
204
|
+
'runtimeId values must be unique before dispatch generation.',
|
|
205
|
+
{ runtimeId: id },
|
|
206
|
+
routeLoc(routes[index])
|
|
207
|
+
));
|
|
208
|
+
}
|
|
209
|
+
seen.add(id);
|
|
210
|
+
});
|
|
211
|
+
for (let expected = 0; expected < ids.length; expected += 1) {
|
|
212
|
+
if (!seen.has(expected)) {
|
|
213
|
+
diagnostics.push(makeDiagnostic(
|
|
214
|
+
'PULSEWASM_DISPATCH_NON_DENSE_RUNTIME_IDS',
|
|
215
|
+
`Missing runtimeId ${expected}; dispatch route IDs must be dense.`,
|
|
216
|
+
'Regenerate route-plan.json from the flattened route plan.',
|
|
217
|
+
{ expected, runtimeIds: ids },
|
|
218
|
+
{ file: '<route-plan>' }
|
|
219
|
+
));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function validateStableIds(routes, diagnostics) {
|
|
225
|
+
const seen = new Map();
|
|
226
|
+
for (const route of routes) {
|
|
227
|
+
if (!route.stableId) {
|
|
228
|
+
diagnostics.push(makeDiagnostic(
|
|
229
|
+
'PULSEWASM_DISPATCH_MISSING_STABLE_ID',
|
|
230
|
+
`Route ${route.method || '<method>'} ${route.path || '<path>'} is missing stableId.`,
|
|
231
|
+
'Run stable-ids before dispatch-table generation.',
|
|
232
|
+
{ runtimeId: route.runtimeId ?? route.id },
|
|
233
|
+
routeLoc(route)
|
|
234
|
+
));
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (seen.has(route.stableId)) {
|
|
238
|
+
diagnostics.push(makeDiagnostic(
|
|
239
|
+
'PULSEWASM_DISPATCH_DUPLICATE_STABLE_ID',
|
|
240
|
+
`Duplicate stableId ${route.stableId} in route plan.`,
|
|
241
|
+
'Stable route IDs must be unique before dispatch generation.',
|
|
242
|
+
{ stableId: route.stableId, firstRuntimeId: seen.get(route.stableId), runtimeId: route.runtimeId ?? route.id },
|
|
243
|
+
routeLoc(route)
|
|
244
|
+
));
|
|
245
|
+
}
|
|
246
|
+
seen.set(route.stableId, route.runtimeId ?? route.id);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function findPathTableRoute(pathTable, route) {
|
|
251
|
+
if (!pathTable || !Array.isArray(pathTable.routes)) return undefined;
|
|
252
|
+
return pathTable.routes.find((entry) => entry.stableId === route.stableId && (entry.runtimeId === (route.runtimeId ?? route.id)));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function buildDispatchTable(routePlan, handlerTable, handlerEval, pathTable, options = {}) {
|
|
256
|
+
const cwd = options.cwd || process.cwd();
|
|
257
|
+
const diagnostics = [];
|
|
258
|
+
const routes = [...(routePlan?.routes || [])].sort((a, b) => (a.runtimeId ?? a.id) - (b.runtimeId ?? b.id));
|
|
259
|
+
validateDenseRuntimeIds(routes, diagnostics);
|
|
260
|
+
validateStableIds(routes, diagnostics);
|
|
261
|
+
|
|
262
|
+
const { slots: handlerSlots, byId: slotById } = buildHandlerSlots(handlerTable, handlerEval, diagnostics);
|
|
263
|
+
const methodBuckets = {};
|
|
264
|
+
const dispatchRoutes = [];
|
|
265
|
+
|
|
266
|
+
for (const route of routes) {
|
|
267
|
+
const runtimeId = route.runtimeId ?? route.id;
|
|
268
|
+
const pathEntry = findPathTableRoute(pathTable, route);
|
|
269
|
+
if (!pathEntry) {
|
|
270
|
+
diagnostics.push(makeDiagnostic(
|
|
271
|
+
'PULSEWASM_DISPATCH_PATH_METADATA_MISSING',
|
|
272
|
+
`Route ${route.method} ${route.path} is missing corresponding path-table metadata.`,
|
|
273
|
+
'Run path-table generation before dispatch-table generation.',
|
|
274
|
+
{ runtimeId, stableId: route.stableId },
|
|
275
|
+
routeLoc(route)
|
|
276
|
+
));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const middleware = createHandlerUses(route.middleware, 'middleware', slotById, diagnostics, route);
|
|
280
|
+
const errorHandlers = createHandlerUses(route.errorHandlers, 'error', slotById, diagnostics, route);
|
|
281
|
+
const connectHandlers = createHandlerUses(route.connectHandlers, 'lifecycle:connect', slotById, diagnostics, route);
|
|
282
|
+
const disconnectHandlers = createHandlerUses(route.disconnectHandlers, 'lifecycle:disconnect', slotById, diagnostics, route);
|
|
283
|
+
const handler = createHandlerUse(route.handler, 'route', slotById, diagnostics, route, 0);
|
|
284
|
+
const channel = createChannelUse(route.channel, slotById, diagnostics, route);
|
|
285
|
+
const lifecycle = { connect: connectHandlers, disconnect: disconnectHandlers };
|
|
286
|
+
const pattern = route.pathPattern || pathEntry?.pattern;
|
|
287
|
+
const paramSlots = createParamSlots({ ...route, pathPattern: pattern });
|
|
288
|
+
|
|
289
|
+
const entry = {
|
|
290
|
+
stableId: route.stableId,
|
|
291
|
+
runtimeId,
|
|
292
|
+
id: runtimeId,
|
|
293
|
+
order: route.order,
|
|
294
|
+
method: route.method,
|
|
295
|
+
path: route.path,
|
|
296
|
+
params: route.params || [],
|
|
297
|
+
paramSlots,
|
|
298
|
+
pattern,
|
|
299
|
+
match: {
|
|
300
|
+
method: route.method,
|
|
301
|
+
path: route.path,
|
|
302
|
+
wildcard: Boolean(pattern && pattern.wildcard),
|
|
303
|
+
segments: pattern?.segments || [],
|
|
304
|
+
paramSlots
|
|
305
|
+
},
|
|
306
|
+
handlers: {
|
|
307
|
+
middleware,
|
|
308
|
+
route: handler,
|
|
309
|
+
error: errorHandlers,
|
|
310
|
+
lifecycle,
|
|
311
|
+
channel
|
|
312
|
+
},
|
|
313
|
+
execution: buildExecution({ middleware, handler }, errorHandlers, lifecycle, channel),
|
|
314
|
+
scopedMiddleware: route.scopedMiddleware || [],
|
|
315
|
+
sourceRouter: route.sourceRouter,
|
|
316
|
+
loc: route.loc
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
dispatchRoutes.push(entry);
|
|
320
|
+
if (!methodBuckets[route.method]) methodBuckets[route.method] = [];
|
|
321
|
+
methodBuckets[route.method].push(runtimeId);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const uniqueHandlerRefs = new Set();
|
|
325
|
+
for (const route of dispatchRoutes) {
|
|
326
|
+
for (const use of route.handlers.middleware || []) uniqueHandlerRefs.add(use.id);
|
|
327
|
+
if (route.handlers.route?.id) uniqueHandlerRefs.add(route.handlers.route.id);
|
|
328
|
+
for (const use of route.handlers.error || []) uniqueHandlerRefs.add(use.id);
|
|
329
|
+
for (const use of route.handlers.lifecycle?.connect || []) uniqueHandlerRefs.add(use.id);
|
|
330
|
+
for (const use of route.handlers.lifecycle?.disconnect || []) uniqueHandlerRefs.add(use.id);
|
|
331
|
+
if (route.handlers.channel?.kind === 'handler' && route.handlers.channel.id) uniqueHandlerRefs.add(route.handlers.channel.id);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const summary = {
|
|
335
|
+
routes: dispatchRoutes.length,
|
|
336
|
+
methods: Object.keys(methodBuckets).sort(),
|
|
337
|
+
handlerSlots: handlerSlots.length,
|
|
338
|
+
usedHandlers: uniqueHandlerRefs.size,
|
|
339
|
+
middlewareLinks: dispatchRoutes.reduce((sum, route) => sum + route.handlers.middleware.length, 0),
|
|
340
|
+
errorLinks: dispatchRoutes.reduce((sum, route) => sum + route.handlers.error.length, 0),
|
|
341
|
+
lifecycleLinks: dispatchRoutes.reduce((sum, route) => sum + route.handlers.lifecycle.connect.length + route.handlers.lifecycle.disconnect.length, 0),
|
|
342
|
+
channelHandlers: dispatchRoutes.filter((route) => route.handlers.channel?.kind === 'handler').length,
|
|
343
|
+
staticChannels: dispatchRoutes.filter((route) => route.handlers.channel?.kind === 'static').length,
|
|
344
|
+
diagnostics: diagnostics.length
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const dispatchTable = normalizeArtifact({
|
|
348
|
+
version: DISPATCH_TABLE_VERSION,
|
|
349
|
+
generatedBy: options.generatedBy || routePlan?.generatedBy || PACKAGE_VERSION,
|
|
350
|
+
source: routePlan?.source,
|
|
351
|
+
entryRouter: routePlan?.entryRouter,
|
|
352
|
+
policy: DISPATCH_POLICY,
|
|
353
|
+
routePlanVersion: routePlan?.version,
|
|
354
|
+
handlerTableVersion: handlerTable?.version,
|
|
355
|
+
handlerEvalVersion: handlerEval?.version,
|
|
356
|
+
pathTableVersion: pathTable?.version,
|
|
357
|
+
idPolicy: routePlan?.idPolicy,
|
|
358
|
+
pathPolicy: routePlan?.pathPolicy,
|
|
359
|
+
handlerSlots,
|
|
360
|
+
handlerSlotMap: Object.fromEntries(handlerSlots.map((handler) => [handler.id, handler.slot])),
|
|
361
|
+
methodBuckets,
|
|
362
|
+
routes: dispatchRoutes,
|
|
363
|
+
summary
|
|
364
|
+
}, cwd);
|
|
365
|
+
|
|
366
|
+
return { dispatchTable, diagnostics };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function matchDispatchRoute(dispatchTable, method, requestPath) {
|
|
370
|
+
const normalizedMethod = String(method || '').toUpperCase();
|
|
371
|
+
const routeIds = dispatchTable?.methodBuckets?.[normalizedMethod] || [];
|
|
372
|
+
const routesById = new Map((dispatchTable?.routes || []).map((route) => [route.runtimeId, route]));
|
|
373
|
+
|
|
374
|
+
for (const runtimeId of routeIds) {
|
|
375
|
+
const route = routesById.get(runtimeId);
|
|
376
|
+
if (!route) continue;
|
|
377
|
+
const match = matchRoutePath(route.pattern, requestPath);
|
|
378
|
+
if (!match) continue;
|
|
379
|
+
return {
|
|
380
|
+
stableId: route.stableId,
|
|
381
|
+
runtimeId: route.runtimeId,
|
|
382
|
+
id: route.id,
|
|
383
|
+
method: route.method,
|
|
384
|
+
path: route.path,
|
|
385
|
+
params: match.params,
|
|
386
|
+
rest: match.rest,
|
|
387
|
+
handlers: route.handlers,
|
|
388
|
+
execution: route.execution,
|
|
389
|
+
route
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
module.exports = {
|
|
396
|
+
DISPATCH_POLICY,
|
|
397
|
+
DISPATCH_TABLE_VERSION,
|
|
398
|
+
buildDispatchTable,
|
|
399
|
+
matchDispatchRoute
|
|
400
|
+
};
|