@pulse-compute/wasm-compiler 0.0.0 → 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +53 -1
  2. package/bin/provider-proof-composition.js +34 -0
  3. package/bin/pulsewasm-extract.js +20 -0
  4. package/package.json +60 -5
  5. package/src/artifacts-dir.js +13 -0
  6. package/src/ast-json.js +52 -0
  7. package/src/build-manifest.js +354 -0
  8. package/src/canonical-api-compiler.js +269 -0
  9. package/src/canonical-native-compiler.js +411 -0
  10. package/src/canonical-native-plan.js +1478 -0
  11. package/src/canonical-project-compiler.js +1224 -0
  12. package/src/canonical-router-compiler.js +25 -0
  13. package/src/cli-intents.js +1235 -0
  14. package/src/cli.js +1927 -0
  15. package/src/codegen/assemblyscript-compile.js +3 -0
  16. package/src/codegen/assemblyscript-core.js +3 -0
  17. package/src/codegen/assemblyscript-shape.js +3 -0
  18. package/src/codegen/assemblyscript-wasm-smoke.js +3 -0
  19. package/src/codegen/backend-capabilities.js +3 -0
  20. package/src/codegen/channel-broadcaster.js +3 -0
  21. package/src/codegen/compiled-handlers.js +3 -0
  22. package/src/codegen/compiled-wasm-runtime.js +3 -0
  23. package/src/codegen/config-references.js +248 -0
  24. package/src/codegen/dispatch-ts.js +145 -0
  25. package/src/codegen/effect-composition.js +331 -0
  26. package/src/codegen/effect-runtime.js +418 -0
  27. package/src/codegen/execution-harness-ts.js +467 -0
  28. package/src/codegen/handler-bindings-ts.js +298 -0
  29. package/src/codegen/handler-library-contracts.js +3 -0
  30. package/src/codegen/host-capabilities.js +3 -0
  31. package/src/codegen/host-runtime-kernel.js +3 -0
  32. package/src/codegen/integrated-compiled-app.js +3 -0
  33. package/src/codegen/json-body.js +3 -0
  34. package/src/codegen/library-sidecars.js +3 -0
  35. package/src/codegen/local-harness-ts.js +453 -0
  36. package/src/codegen/pulse-wrapper.js +217 -0
  37. package/src/codegen/request-result-headers.js +3 -0
  38. package/src/codegen/schema-json-compile.js +3 -0
  39. package/src/codegen/schema-json-sidecar-v2.js +3 -0
  40. package/src/codegen/schema-json-sidecar.js +3 -0
  41. package/src/codegen/streaming-passthrough.js +3 -0
  42. package/src/codegen/wasm-host-abi.js +3 -0
  43. package/src/codegen/wasm-host-bridge.js +3 -0
  44. package/src/compiled-wasm-host-runtime-kv.js +3 -0
  45. package/src/config-resolver.js +813 -0
  46. package/src/crypto-requirement-planner.js +89 -0
  47. package/src/definitions/config-schema.js +14 -0
  48. package/src/definitions/handler-roles.js +14 -0
  49. package/src/definitions/path-grammar.js +14 -0
  50. package/src/definitions/router-api.js +14 -0
  51. package/src/diagnostics/codes.js +14 -0
  52. package/src/diagnostics/reporter.js +14 -0
  53. package/src/diagnostics.js +14 -0
  54. package/src/dispatch-table.js +400 -0
  55. package/src/events/event-emit.js +265 -0
  56. package/src/events/event-topology.js +127 -0
  57. package/src/execution-plan.js +463 -0
  58. package/src/extractor.js +2816 -0
  59. package/src/handler-eval.js +1154 -0
  60. package/src/handler-table.js +326 -0
  61. package/src/index.js +19 -0
  62. package/src/javascript-application-plan.js +181 -0
  63. package/src/kv-provider.js +3 -0
  64. package/src/path-table.js +60 -0
  65. package/src/path.js +14 -0
  66. package/src/patterns/config-define.js +30 -0
  67. package/src/patterns/dependency-call.js +18 -0
  68. package/src/patterns/env-lookup.js +13 -0
  69. package/src/patterns/handler-reference.js +29 -0
  70. package/src/patterns/path-literal.js +35 -0
  71. package/src/patterns/result.js +15 -0
  72. package/src/patterns/router-chain-call.js +50 -0
  73. package/src/patterns/router-construction.js +19 -0
  74. package/src/project/package-reachability.js +782 -0
  75. package/src/project/reachable-graph-builder.js +992 -0
  76. package/src/project/reachable-graph-contract.js +54 -0
  77. package/src/project/reachable-graph-implementation.js +36 -0
  78. package/src/project/router-module-linker.js +710 -0
  79. package/src/project-config-compiler.js +222 -0
  80. package/src/project-target-support.js +500 -0
  81. package/src/provider-toolchain.js +299 -0
  82. package/src/spine/async-surface-normalizer.js +328 -0
  83. package/src/spine/canonical-handler-ir.js +336 -0
  84. package/src/spine/canonical-native-module.js +87 -0
  85. package/src/spine/canonical-native-plan.js +89 -0
  86. package/src/spine/canonical-project.js +76 -0
  87. package/src/spine/canonical-router.js +155 -0
  88. package/src/spine/canonical-source.js +165 -0
  89. package/src/spine/diagnostic-authority.js +290 -0
  90. package/src/spine/equivalence.js +262 -0
  91. package/src/spine/guest-unit-stage.js +87 -0
  92. package/src/spine/handler-ir-emitter.js +455 -0
  93. package/src/spine/handler-ir-managed.js +1598 -0
  94. package/src/spine/handler-ir.js +797 -0
  95. package/src/spine/handler-surface-authority.js +588 -0
  96. package/src/spine/package-operation-seam.js +1015 -0
  97. package/src/spine/pipeline.js +202 -0
  98. package/src/spine/plain-handler-frontend.js +545 -0
  99. package/src/spine/provider-requirement-authority.js +208 -0
  100. package/src/spine/router-control-contract.js +17 -0
  101. package/src/spine/router-handler-frontend.js +514 -0
  102. package/src/spine/router-handler-ir.js +372 -0
  103. package/src/spine/router-topology-frontend.js +638 -0
  104. 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
+ };