@znight/messaging 0.0.0-stage → 0.1.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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +622 -2
  3. package/dist/async-CFh_bvex.mjs +216 -0
  4. package/dist/async-CFh_bvex.mjs.map +1 -0
  5. package/dist/codec-Dku-NXXU.mjs +83 -0
  6. package/dist/codec-Dku-NXXU.mjs.map +1 -0
  7. package/dist/compiler.d.mts +75 -0
  8. package/dist/compiler.mjs +1023 -0
  9. package/dist/compiler.mjs.map +1 -0
  10. package/dist/driver-ZxLqy_Ap.d.mts +216 -0
  11. package/dist/index.d.mts +459 -0
  12. package/dist/index.mjs +870 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/kafka.d.mts +51 -0
  15. package/dist/kafka.mjs +517 -0
  16. package/dist/kafka.mjs.map +1 -0
  17. package/dist/memory.d.mts +124 -0
  18. package/dist/memory.mjs +429 -0
  19. package/dist/memory.mjs.map +1 -0
  20. package/dist/rabbitmq.d.mts +65 -0
  21. package/dist/rabbitmq.mjs +686 -0
  22. package/dist/rabbitmq.mjs.map +1 -0
  23. package/dist/shared-BJBpLS8V.mjs +52 -0
  24. package/dist/shared-BJBpLS8V.mjs.map +1 -0
  25. package/dist/sqs.d.mts +38 -0
  26. package/dist/sqs.mjs +421 -0
  27. package/dist/sqs.mjs.map +1 -0
  28. package/package.json +123 -4
  29. package/src/broker.ts +156 -0
  30. package/src/codec.ts +98 -0
  31. package/src/compiler/analyze.ts +800 -0
  32. package/src/compiler/constants.ts +73 -0
  33. package/src/compiler/data.ts +75 -0
  34. package/src/compiler/generate.ts +131 -0
  35. package/src/compiler/index.ts +52 -0
  36. package/src/compiler/patterns.ts +54 -0
  37. package/src/compiler/retry.ts +136 -0
  38. package/src/compiler/validate.ts +57 -0
  39. package/src/compiler/values.ts +76 -0
  40. package/src/consumers.ts +260 -0
  41. package/src/decorators.ts +131 -0
  42. package/src/driver.ts +142 -0
  43. package/src/drivers/kafka.ts +765 -0
  44. package/src/drivers/memory.ts +609 -0
  45. package/src/drivers/rabbitmq.ts +944 -0
  46. package/src/drivers/shared.ts +81 -0
  47. package/src/drivers/sqs.ts +583 -0
  48. package/src/errors.ts +95 -0
  49. package/src/headers.ts +70 -0
  50. package/src/hooks.ts +2 -0
  51. package/src/index.ts +89 -0
  52. package/src/internal/async.ts +115 -0
  53. package/src/message.ts +96 -0
  54. package/src/metadata.ts +65 -0
  55. package/src/plugin.ts +236 -0
  56. package/src/retry.ts +129 -0
  57. package/src/runtime.ts +424 -0
  58. package/src/standard-schema.ts +68 -0
  59. package/src/tokens.ts +14 -0
  60. package/src/validation.ts +61 -0
@@ -0,0 +1,1023 @@
1
+ // src/compiler/constants.ts
2
+ /** Name of the extension, also the key of `metadata.extensions.messaging`. */
3
+ const EXTENSION_NAME = "messaging";
4
+ /** Module the generated code imports its helpers and types from. */
5
+ const RUNTIME_MODULE = "@znight/messaging";
6
+ /** Canonical identities of the decorators owned by the extension. */
7
+ const MessagingDecorator = Object.freeze({
8
+ Consumer: "@znight/messaging#Consumer",
9
+ OnMessage: "@znight/messaging#OnMessage",
10
+ Retry: "@znight/messaging#Retry",
11
+ OnError: "@znight/messaging#OnError",
12
+ Message: "@znight/messaging#Message",
13
+ Headers: "@znight/messaging#Headers",
14
+ Ctx: "@znight/messaging#Ctx"
15
+ });
16
+ /** Decorators applied to methods. */
17
+ const METHOD_DECORATORS = /* @__PURE__ */ new Set([
18
+ MessagingDecorator.OnMessage,
19
+ MessagingDecorator.Retry,
20
+ MessagingDecorator.OnError
21
+ ]);
22
+ /** Decorators applied to handler parameters. */
23
+ const PARAMETER_DECORATORS = /* @__PURE__ */ new Set([
24
+ MessagingDecorator.Message,
25
+ MessagingDecorator.Headers,
26
+ MessagingDecorator.Ctx
27
+ ]);
28
+ /** ZNIGHT005: invalid decorator usage (target, arguments, several decorators...). */
29
+ const INVALID_DECORATOR = "ZNIGHT005";
30
+ /** ZNIGHT011: invalid message consumer (no handler, overlapping types, retry options...). */
31
+ const INVALID_CONSUMER = "ZNIGHT011";
32
+ /** ZNIGHT017: non-primitive `@Message()` payload without schema. */
33
+ const UNVALIDATED_INPUT = "ZNIGHT017";
34
+ /**
35
+ * Default backoff of `@Retry` (must equal the runtime `DEFAULT_BACKOFF`; checked by the tests, the
36
+ * runtime module cannot be imported here).
37
+ */
38
+ const DEFAULT_BACKOFF = "exponential";
39
+ /** Default base delay of `@Retry`, in milliseconds (the runtime `DEFAULT_RETRY_DELAY_MS`). */
40
+ const DEFAULT_RETRY_DELAY_MS = 1e3;
41
+ /** Largest `attempts` accepted by the runtime (`normalizeRetry`). */
42
+ const MAX_RETRY_ATTEMPTS = 1e4;
43
+ /** Scopes a consumer provider may have. */
44
+ const CONSUMER_SCOPES = [
45
+ "singleton",
46
+ "request",
47
+ "transient"
48
+ ];
49
+ /** Deterministic string comparison (never `localeCompare`). */
50
+ function compareStrings(a, b) {
51
+ if (a === b) return 0;
52
+ return a < b ? -1 : 1;
53
+ }
54
+ // src/compiler/patterns.ts
55
+ /**
56
+ * Message type patterns of `@OnMessage(type)`, with the semantics of the runtime handler selection
57
+ * (`consumers.ts`): a type containing `*` is a pattern where `*` matches any sequence of characters;
58
+ * any other type is matched exactly. Exact types are tried first, then patterns in declaration order.
59
+ */
60
+ /** Whether `type` is a pattern (contains `*`). */
61
+ function isPattern(type) {
62
+ return type.includes("*");
63
+ }
64
+ /**
65
+ * Whether every message type matched by `inner` is also matched by `outer`, so that `inner`, tried
66
+ * after `outer`, can never be selected. The check is sound (never reports a pattern that can be
67
+ * selected): `outer` must match `inner` where each `*` of `inner` can only be consumed by a `*` of
68
+ * `outer`.
69
+ */
70
+ function patternCovers(outer, inner) {
71
+ return covers(outer, inner, true);
72
+ }
73
+ /** Glob matching of `subject` by `pattern`; with `starsAreWildcards`, `*` of `subject` only match `*` of `pattern`. */
74
+ function covers(pattern, subject, starsAreWildcards) {
75
+ const p = collapseStars(pattern);
76
+ const s = starsAreWildcards ? collapseStars(subject) : subject;
77
+ let previous = new Array(s.length + 1).fill(false);
78
+ previous[0] = true;
79
+ for (let i = 1; i <= p.length; i++) {
80
+ const char = p[i - 1];
81
+ const current = new Array(s.length + 1).fill(false);
82
+ current[0] = char === "*" && previous[0] === true;
83
+ for (let j = 1; j <= s.length; j++) if (char === "*") current[j] = previous[j] === true || current[j - 1] === true;
84
+ else {
85
+ const subjectChar = s[j - 1];
86
+ current[j] = previous[j - 1] === true && subjectChar === char && !(starsAreWildcards && subjectChar === "*");
87
+ }
88
+ previous = current;
89
+ }
90
+ return previous[s.length] === true;
91
+ }
92
+ /** `a**b` → `a*b` (same language). */
93
+ function collapseStars(pattern) {
94
+ return pattern.replace(/\*{2,}/g, "*");
95
+ }
96
+ // src/compiler/values.ts
97
+ /** An evaluated object literal (not an array, not a reference to an exported binding). */
98
+ function isRecord(value) {
99
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
100
+ const prototype = Object.getPrototypeOf(value);
101
+ return prototype === Object.prototype || prototype === null;
102
+ }
103
+ /** Short description of a value for messages (`"orders"`, `42`, `an array`...). */
104
+ function describeValue(value) {
105
+ if (value === void 0) return "undefined";
106
+ if (value === null) return "null";
107
+ if (typeof value === "string") return JSON.stringify(value);
108
+ if (typeof value === "bigint") return `${value}n`;
109
+ if (typeof value !== "object") return String(value);
110
+ if (Array.isArray(value)) return "an array";
111
+ return isRecord(value) ? "an object" : `a reference to '${value.text ?? "?"}'`;
112
+ }
113
+ /**
114
+ * Location of the property `key` of an object literal argument (`attempts: 0` in
115
+ * `@Retry({ attempts: 0 })`), or of the whole argument when it is not an inline object literal.
116
+ */
117
+ function propertyLocation(project, arg, key) {
118
+ let node = arg.node;
119
+ while (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSNonNullExpression" || node.type === "ParenthesizedExpression") node = node.expression;
120
+ if (node.type !== "ObjectExpression") return arg.location;
121
+ for (const property of node.properties) {
122
+ if (property.type !== "Property" || property.computed) continue;
123
+ const { key: name } = property;
124
+ if ((name.type === "Identifier" ? name.name : name.type === "Literal" && typeof name.value === "string" && name.value) === key) return project.getFile(arg.location.file)?.location(property) ?? arg.location;
125
+ }
126
+ return arg.location;
127
+ }
128
+ /** Closest candidate of `name` among `candidates` (typos: `attempt` → `attempts`), if any. */
129
+ function closest(name, candidates) {
130
+ let best;
131
+ let bestDistance = Math.max(2, Math.floor(name.length / 3)) + 1;
132
+ for (const candidate of candidates) {
133
+ const distance = editDistance(name.toLowerCase(), candidate.toLowerCase());
134
+ if (distance < bestDistance) {
135
+ best = candidate;
136
+ bestDistance = distance;
137
+ }
138
+ }
139
+ return best;
140
+ }
141
+ function editDistance(a, b) {
142
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
143
+ for (let i = 1; i <= a.length; i++) {
144
+ const current = [i];
145
+ for (let j = 1; j <= b.length; j++) {
146
+ const substitution = (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
147
+ current[j] = Math.min((previous[j] ?? 0) + 1, (current[j - 1] ?? 0) + 1, substitution);
148
+ }
149
+ previous = current;
150
+ }
151
+ return previous[b.length] ?? 0;
152
+ }
153
+ // src/compiler/retry.ts
154
+ const RETRY_OPTIONS = [
155
+ "attempts",
156
+ "backoff",
157
+ "delay",
158
+ "maxDelay"
159
+ ];
160
+ /**
161
+ * Validates the argument of a `@Retry` decorator and fills the runtime defaults. Returns `undefined`
162
+ * (after reporting the problems) when the options are invalid.
163
+ *
164
+ * @param where the handler (`OrderConsumer.placed()`), used in messages
165
+ */
166
+ function normalizeRetryDecorator(decorator, where, ctx) {
167
+ if (!decorator.called) return void 0;
168
+ const arg = decorator.args[0];
169
+ if (arg === void 0) {
170
+ ctx.report({
171
+ code: INVALID_DECORATOR,
172
+ severity: "error",
173
+ message: `@Retry on ${where} requires options: @Retry({ attempts: 3 })`,
174
+ location: decorator.location,
175
+ hints: ["attempts counts the first delivery: { attempts: 3 } means 1 delivery + 2 retries"]
176
+ });
177
+ return;
178
+ }
179
+ if (!arg.evaluated) return void 0;
180
+ const options = arg.value;
181
+ if (!isRecord(options)) {
182
+ ctx.report({
183
+ code: INVALID_DECORATOR,
184
+ severity: "error",
185
+ message: `@Retry on ${where} expects an options object ({ attempts, backoff?, delay?, maxDelay? }), got ${describeValue(options)}`,
186
+ location: arg.location
187
+ });
188
+ return;
189
+ }
190
+ let valid = true;
191
+ const fail = (key, message, hints) => {
192
+ valid = false;
193
+ ctx.report({
194
+ code: INVALID_CONSUMER,
195
+ severity: "error",
196
+ message: `@Retry on ${where}: ${message}`,
197
+ location: propertyLocation(ctx.project, arg, key),
198
+ ...hints === void 0 ? {} : { hints }
199
+ });
200
+ };
201
+ for (const key of Object.keys(options)) {
202
+ if (RETRY_OPTIONS.includes(key)) continue;
203
+ const suggestion = closest(key, RETRY_OPTIONS);
204
+ fail(key, `unknown option "${key}"`, [suggestion === void 0 ? `Valid options: ${RETRY_OPTIONS.join(", ")}` : `Did you mean "${suggestion}"? Valid options: ${RETRY_OPTIONS.join(", ")}`]);
205
+ }
206
+ const attempts = options["attempts"];
207
+ if (attempts === void 0) fail("attempts", "\"attempts\" is required", ["attempts counts the first delivery: { attempts: 3 } means 1 delivery + 2 retries"]);
208
+ else if (!isInteger(attempts) || attempts < 1 || attempts > 1e4) fail("attempts", `"attempts" must be an integer between 1 and ${MAX_RETRY_ATTEMPTS}, got ${describeValue(attempts)}`, ["attempts counts the first delivery: { attempts: 3 } means 1 delivery + 2 retries"]);
209
+ const rawBackoff = options["backoff"];
210
+ const backoff = rawBackoff === void 0 ? DEFAULT_BACKOFF : rawBackoff;
211
+ if (backoff !== "fixed" && backoff !== "exponential") fail("backoff", `"backoff" must be 'fixed' or 'exponential', got ${describeValue(backoff)}`);
212
+ const rawDelay = options["delay"];
213
+ const delay = rawDelay === void 0 ? DEFAULT_RETRY_DELAY_MS : rawDelay;
214
+ const delayValid = isDuration(delay);
215
+ if (!delayValid) fail("delay", `"delay" must be a finite number of milliseconds >= 0, got ${describeValue(delay)}`);
216
+ const maxDelay = options["maxDelay"];
217
+ if (maxDelay !== void 0) {
218
+ if (!isDuration(maxDelay) || delayValid && maxDelay < delay) fail("maxDelay", `"maxDelay" must be a finite number of milliseconds >= delay${delayValid ? ` (${delay})` : ""}, got ${describeValue(maxDelay)}`);
219
+ else if (backoff === "fixed") ctx.report({
220
+ code: INVALID_CONSUMER,
221
+ severity: "warning",
222
+ message: `@Retry on ${where}: "maxDelay" has no effect with backoff 'fixed' (every retry waits "delay")`,
223
+ location: propertyLocation(ctx.project, arg, "maxDelay"),
224
+ hints: ["Remove maxDelay, or use backoff: 'exponential'"]
225
+ });
226
+ }
227
+ if (!valid || !isInteger(attempts) || backoff !== "fixed" && backoff !== "exponential" || !isDuration(delay)) return;
228
+ const policy = isDuration(maxDelay) ? {
229
+ attempts,
230
+ backoff,
231
+ delay,
232
+ maxDelay
233
+ } : {
234
+ attempts,
235
+ backoff,
236
+ delay
237
+ };
238
+ return Object.freeze(policy);
239
+ }
240
+ function isInteger(value) {
241
+ return typeof value === "number" && Number.isInteger(value);
242
+ }
243
+ function isDuration(value) {
244
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
245
+ }
246
+ // src/compiler/analyze.ts
247
+ /** Types injected as is by `@Message()` without being "unvalidated input" (binary payloads). */
248
+ const BINARY_TYPES = /* @__PURE__ */ new Set([
249
+ "globalThis#Uint8Array",
250
+ "globalThis#ArrayBuffer",
251
+ "globalThis#Buffer",
252
+ "node:buffer#Buffer",
253
+ "buffer#Buffer"
254
+ ]);
255
+ /** Maximum depth of the base class chain walked for inherited handlers. */
256
+ const MAX_CHAIN = 32;
257
+ /** Analyses a class carrying at least one decorator of the extension. */
258
+ function analyzeMessagingClass(cls, ctx) {
259
+ const own = analyzeMembers(cls, ctx);
260
+ const consumers = cls.decorators.filter((decorator) => decorator.identity === MessagingDecorator.Consumer);
261
+ const consumer = consumers[0];
262
+ if (consumer === void 0) {
263
+ const decorators = cls.methods.flatMap((method) => method.decorators.filter((decorator) => decorator.identity !== void 0 && METHOD_DECORATORS.has(decorator.identity)).map((decorator) => ({
264
+ name: decorator.name,
265
+ location: decorator.location
266
+ })));
267
+ if (decorators.length > 0) ctx.store({
268
+ kind: "members",
269
+ identity: cls.identity,
270
+ displayName: cls.displayName,
271
+ abstract: cls.abstract,
272
+ decorators
273
+ });
274
+ return;
275
+ }
276
+ for (const duplicate of consumers.slice(1)) ctx.report({
277
+ code: INVALID_DECORATOR,
278
+ severity: "error",
279
+ message: `@${duplicate.name} is applied several times on ${cls.displayName}`,
280
+ location: duplicate.location,
281
+ hints: [`Keep a single @${duplicate.name}: a class consumes one queue`]
282
+ });
283
+ const options = readConsumerOptions(consumer, cls, ctx);
284
+ const token = ctx.registerProvider(cls, { scope: options.scope });
285
+ const chain = classChain(cls, ctx);
286
+ const merged = /* @__PURE__ */ new Map();
287
+ for (let index = chain.length - 1; index >= 0; index--) {
288
+ const owner = chain[index];
289
+ for (const [name, member] of index === 0 ? own : analyzeMembers(owner, ctx)) merged.set(name, member);
290
+ }
291
+ const members = [...merged.values()];
292
+ const handlers = members.flatMap((member) => member.handlers);
293
+ if (!members.some((member) => member.onMessage > 0)) ctx.report({
294
+ code: INVALID_CONSUMER,
295
+ severity: "error",
296
+ message: `@Consumer ${cls.displayName} has no @OnMessage handler`,
297
+ location: consumer.location,
298
+ hints: ["Decorate at least one public method with @OnMessage() (default handler) or @OnMessage(\"type\")"]
299
+ });
300
+ checkHandlerTypes(cls.displayName, handlers, ctx);
301
+ const errorHandlers = members.filter((member) => member.errorHandler !== void 0);
302
+ const errorHandler = errorHandlers[0];
303
+ for (const extra of errorHandlers.slice(1)) ctx.report({
304
+ code: INVALID_CONSUMER,
305
+ severity: "error",
306
+ message: `@Consumer ${cls.displayName} has several @OnError methods: ${errorHandlers.map((member) => `${member.declaredBy}.${member.method}()`).join(", ")}`,
307
+ location: extra.errorHandler,
308
+ hints: ["Keep a single @OnError method per consumer"],
309
+ related: [{
310
+ message: "First @OnError method",
311
+ location: errorHandler?.errorHandler
312
+ }]
313
+ });
314
+ ctx.store({
315
+ kind: "consumer",
316
+ token,
317
+ identity: cls.identity,
318
+ displayName: cls.displayName,
319
+ queue: options.queue,
320
+ options: options.options,
321
+ handlers,
322
+ errorHandler: errorHandler?.method,
323
+ chain: chain.map((owner) => owner.identity),
324
+ location: consumer.location
325
+ });
326
+ }
327
+ /** `@Consumer(queue)` / `@Consumer({ queue, scope?, concurrency?, deadLetter?, ...driver options })`. */
328
+ function readConsumerOptions(decorator, cls, ctx) {
329
+ const invalid = {
330
+ queue: "",
331
+ scope: "singleton",
332
+ options: void 0
333
+ };
334
+ const prefix = `@Consumer ${cls.displayName}`;
335
+ if (!decorator.called) return invalid;
336
+ const arg = decorator.args[0];
337
+ if (arg === void 0) {
338
+ ctx.report({
339
+ code: INVALID_DECORATOR,
340
+ severity: "error",
341
+ message: `${prefix}: the queue to consume is required`,
342
+ location: decorator.location,
343
+ hints: ["Write @Consumer('orders') or @Consumer({ queue: 'orders', concurrency: 5 })"]
344
+ });
345
+ return invalid;
346
+ }
347
+ if (!arg.evaluated) return invalid;
348
+ const value = arg.value;
349
+ if (typeof value === "string") {
350
+ if (value === "") ctx.report({
351
+ code: INVALID_CONSUMER,
352
+ severity: "error",
353
+ message: `${prefix}: the queue must be a non-empty string`,
354
+ location: arg.location
355
+ });
356
+ return {
357
+ queue: value,
358
+ scope: "singleton",
359
+ options: void 0
360
+ };
361
+ }
362
+ if (!isRecord(value)) {
363
+ ctx.report({
364
+ code: INVALID_DECORATOR,
365
+ severity: "error",
366
+ message: `${prefix}: expected a queue name or an options object ({ queue, scope?, concurrency?, deadLetter? }), got ${describeValue(value)}`,
367
+ location: arg.location
368
+ });
369
+ return invalid;
370
+ }
371
+ const fail = (key, message, hints) => {
372
+ ctx.report({
373
+ code: INVALID_CONSUMER,
374
+ severity: "error",
375
+ message: `${prefix}: ${message}`,
376
+ location: propertyLocation(ctx.project, arg, key),
377
+ ...hints === void 0 ? {} : { hints }
378
+ });
379
+ };
380
+ let queue = "";
381
+ const rawQueue = value["queue"];
382
+ if (rawQueue === void 0) fail("queue", "the \"queue\" option is required", ["Write @Consumer({ queue: 'orders', ... })"]);
383
+ else if (typeof rawQueue !== "string" || rawQueue === "") fail("queue", `"queue" must be a non-empty string, got ${describeValue(rawQueue)}`);
384
+ else queue = rawQueue;
385
+ let scope = "singleton";
386
+ const rawScope = value["scope"];
387
+ if (rawScope !== void 0) {
388
+ if (typeof rawScope === "string" && CONSUMER_SCOPES.includes(rawScope)) scope = rawScope;
389
+ else fail("scope", `"scope" must be 'singleton', 'request' or 'transient', got ${describeValue(rawScope)}`);
390
+ }
391
+ const concurrency = value["concurrency"];
392
+ if (concurrency !== void 0 && !(typeof concurrency === "number" && Number.isSafeInteger(concurrency) && concurrency > 0)) fail("concurrency", `"concurrency" must be a positive integer, got ${describeValue(concurrency)}`);
393
+ const deadLetter = value["deadLetter"];
394
+ if (deadLetter !== void 0 && deadLetter !== false && (typeof deadLetter !== "string" || deadLetter === "")) fail("deadLetter", `"deadLetter" must be a queue name or false, got ${describeValue(deadLetter)}`);
395
+ else if (deadLetter === queue && queue !== "") fail("deadLetter", `the dead-letter queue cannot be the consumed queue "${queue}"`, ["Use another queue (default: the driver naming, e.g. \"orders.dlq\"), or false to disable dead-lettering"]);
396
+ const options = {};
397
+ for (const [key, option] of Object.entries(value)) {
398
+ if (key === "queue" || key === "scope" || option === void 0) continue;
399
+ options[key] = option;
400
+ }
401
+ return {
402
+ queue,
403
+ scope,
404
+ options: Object.keys(options).length === 0 ? void 0 : Object.freeze(options)
405
+ };
406
+ }
407
+ /** The class and its analysable base classes (nearest first). */
408
+ function classChain(cls, ctx) {
409
+ const chain = [cls];
410
+ const seen = /* @__PURE__ */ new Set([cls.identity]);
411
+ let current = cls;
412
+ while (current.superClass !== void 0 && chain.length < MAX_CHAIN) {
413
+ const base = ctx.classInfo(current.superClass.identity);
414
+ if (base === void 0 || seen.has(base.identity)) break;
415
+ chain.push(base);
416
+ seen.add(base.identity);
417
+ current = base;
418
+ }
419
+ return chain;
420
+ }
421
+ /**
422
+ * Handler selection checks, mirroring the runtime (`consumers.ts`): exact types first, then patterns
423
+ * in declaration order, then the single default handler.
424
+ */
425
+ function checkHandlerTypes(consumer, handlers, ctx) {
426
+ let fallback;
427
+ const exact = /* @__PURE__ */ new Map();
428
+ const patterns = [];
429
+ const label = (handler) => `${handler.declaredBy}.${handler.method}()`;
430
+ for (const handler of handlers) {
431
+ const type = handler.type;
432
+ if (type === void 0) {
433
+ if (fallback === void 0) {
434
+ fallback = handler;
435
+ continue;
436
+ }
437
+ ctx.report({
438
+ code: INVALID_CONSUMER,
439
+ severity: "error",
440
+ message: `@Consumer ${consumer}: ${label(fallback)} and ${label(handler)} are both default @OnMessage() handlers`,
441
+ location: handler.location,
442
+ hints: ["Give one of them a message type: @OnMessage(\"order.created\")"],
443
+ related: [{
444
+ message: "First default handler",
445
+ location: fallback.location
446
+ }]
447
+ });
448
+ continue;
449
+ }
450
+ const previous = isPattern(type) ? patterns.find((pattern) => pattern.type === type) : exact.get(type);
451
+ if (previous !== void 0) {
452
+ ctx.report({
453
+ code: INVALID_CONSUMER,
454
+ severity: "error",
455
+ message: `@Consumer ${consumer}: several @OnMessage handlers for type "${type}" (${label(previous)} and ${label(handler)})`,
456
+ location: handler.location,
457
+ hints: ["A message type is handled by a single method: merge the handlers"],
458
+ related: [{
459
+ message: `First handler of "${type}"`,
460
+ location: previous.location
461
+ }]
462
+ });
463
+ continue;
464
+ }
465
+ if (!isPattern(type)) {
466
+ exact.set(type, handler);
467
+ continue;
468
+ }
469
+ const covering = patterns.find((pattern) => patternCovers(pattern.type, type));
470
+ if (covering !== void 0) ctx.report({
471
+ code: INVALID_CONSUMER,
472
+ severity: "error",
473
+ message: `@Consumer ${consumer}: the pattern "${type}" of ${label(handler)} never matches: every type it matches is matched first by "${covering.type}" (${label(covering)})`,
474
+ location: handler.location,
475
+ hints: ["Patterns are tried in declaration order (after exact types): declare the more specific pattern first"],
476
+ related: [{
477
+ message: `Pattern "${covering.type}"`,
478
+ location: covering.location
479
+ }]
480
+ });
481
+ patterns.push({
482
+ ...handler,
483
+ type
484
+ });
485
+ }
486
+ }
487
+ /** Messaging methods declared by `cls` itself, by name, in declaration order. */
488
+ function analyzeMembers(cls, ctx) {
489
+ const members = /* @__PURE__ */ new Map();
490
+ for (const method of cls.methods) {
491
+ const member = analyzeMethod(cls, method, ctx);
492
+ if (member !== void 0) members.set(method.name, member);
493
+ }
494
+ return members;
495
+ }
496
+ function analyzeMethod(cls, method, ctx) {
497
+ const onMessage = decoratorsOf(method.decorators, MessagingDecorator.OnMessage);
498
+ const retry = decoratorsOf(method.decorators, MessagingDecorator.Retry);
499
+ const onError = decoratorsOf(method.decorators, MessagingDecorator.OnError);
500
+ const decoratedParams = method.params.some((param) => param.decorators.some(isParameterDecorator));
501
+ if (onMessage.length === 0 && retry.length === 0 && onError.length === 0 && !decoratedParams) return void 0;
502
+ const where = `${cls.displayName}.${method.name}()`;
503
+ const primary = onMessage[0] ?? onError[0];
504
+ if (primary !== void 0) checkMethodShape(method, primary, where, ctx);
505
+ for (const duplicate of [...retry.slice(1), ...onError.slice(1)]) ctx.report({
506
+ code: INVALID_DECORATOR,
507
+ severity: "error",
508
+ message: `@${duplicate.name} is applied several times on ${where}`,
509
+ location: duplicate.location,
510
+ hints: [`Keep a single @${duplicate.name}`]
511
+ });
512
+ if (onMessage.length > 0 && onError[0] !== void 0) ctx.report({
513
+ code: INVALID_DECORATOR,
514
+ severity: "error",
515
+ message: `${where} cannot be both an @OnMessage handler and the @OnError method`,
516
+ location: onError[0].location,
517
+ hints: ["Declare the error handler in a separate method"]
518
+ });
519
+ if (onMessage.length === 0 && retry[0] !== void 0) ctx.report({
520
+ code: INVALID_DECORATOR,
521
+ severity: "error",
522
+ message: `@Retry has no effect on ${where}: it is not an @OnMessage handler`,
523
+ location: retry[0].location,
524
+ hints: ["Add @OnMessage() to the method, or remove @Retry"]
525
+ });
526
+ let handlers = [];
527
+ if (onMessage.length > 0) {
528
+ const policy = retry[0] === void 0 ? void 0 : normalizeRetryDecorator(retry[0], where, ctx);
529
+ const params = analyzeHandlerParams(method, where, ctx);
530
+ handlers = onMessage.flatMap((decorator) => {
531
+ const type = messageType(decorator, where, ctx);
532
+ if (type === false) return [];
533
+ return [Object.freeze({
534
+ method: method.name,
535
+ type,
536
+ retry: policy,
537
+ params,
538
+ declaredBy: cls.displayName,
539
+ location: decorator.location
540
+ })];
541
+ });
542
+ } else reportStrayParameterDecorators(method, where, onError.length > 0, ctx);
543
+ const errorHandler = onMessage.length === 0 ? onError[0] : void 0;
544
+ if (errorHandler !== void 0) checkErrorHandler(method, errorHandler, where, ctx);
545
+ return {
546
+ method: method.name,
547
+ declaredBy: cls.displayName,
548
+ onMessage: onMessage.length,
549
+ handlers,
550
+ errorHandler: errorHandler?.location
551
+ };
552
+ }
553
+ /** Handlers are called by the generated code: `consumer.method(...)`. */
554
+ function checkMethodShape(method, decorator, where, ctx) {
555
+ const name = `@${decorator.name}`;
556
+ let problem;
557
+ if (method.kind !== "method") problem = {
558
+ message: `${name} cannot be applied to the accessor ${where}`,
559
+ hint: "Use a regular method"
560
+ };
561
+ else if (method.static) problem = {
562
+ message: `${name} cannot be applied to the static method ${where}`,
563
+ hint: "Handlers are called on the consumer instance: remove `static`"
564
+ };
565
+ else if (method.privateName) problem = {
566
+ message: `${name} cannot be applied to the #private method ${where}`,
567
+ hint: "Use a public method: the generated code calls it"
568
+ };
569
+ else if (method.visibility !== "public") problem = {
570
+ message: `${where} must be public: ${name} methods are called by the generated code`,
571
+ hint: `Remove the '${method.visibility}' modifier`
572
+ };
573
+ else if (method.computed) problem = {
574
+ message: `${name} cannot be applied to ${where}: computed method names are not supported`,
575
+ hint: "Use a plain method name"
576
+ };
577
+ else if (method.generator) problem = {
578
+ message: `${name} cannot be applied to the generator method ${where}`,
579
+ hint: "Use a regular (or async) method"
580
+ };
581
+ if (problem !== void 0) ctx.report({
582
+ code: INVALID_DECORATOR,
583
+ severity: "error",
584
+ message: problem.message,
585
+ location: decorator.location,
586
+ hints: [problem.hint]
587
+ });
588
+ }
589
+ /** Type of an `@OnMessage` decorator: a string, `undefined` (default handler) or `false` (invalid). */
590
+ function messageType(decorator, where, ctx) {
591
+ const arg = decorator.args[0];
592
+ if (arg === void 0 || arg.evaluated && arg.value === void 0) return void 0;
593
+ if (!arg.evaluated) return false;
594
+ if (typeof arg.value !== "string") {
595
+ ctx.report({
596
+ code: INVALID_DECORATOR,
597
+ severity: "error",
598
+ message: `@OnMessage on ${where} expects a message type (a string), got ${describeValue(arg.value)}`,
599
+ location: arg.location,
600
+ hints: ["Use @OnMessage() for the default handler"]
601
+ });
602
+ return false;
603
+ }
604
+ if (arg.value === "") {
605
+ ctx.report({
606
+ code: INVALID_CONSUMER,
607
+ severity: "error",
608
+ message: `The @OnMessage type of ${where} must be a non-empty string`,
609
+ location: arg.location,
610
+ hints: ["Use @OnMessage() for the default handler"]
611
+ });
612
+ return false;
613
+ }
614
+ return arg.value;
615
+ }
616
+ /** The `@OnError` method is called as `consumer[method](error, message)`. */
617
+ function checkErrorHandler(method, decorator, where, ctx) {
618
+ const required = method.params.filter((param) => !param.optional && !param.hasDefault && !param.rest).length;
619
+ if (required > 2) ctx.report({
620
+ code: INVALID_CONSUMER,
621
+ severity: "error",
622
+ message: `The @OnError method ${where} has ${required} required parameters: it is called with (error, message)`,
623
+ location: method.location,
624
+ hints: ["Declare onError(error: unknown, message: InboundMessage)"]
625
+ });
626
+ if (method.returnType !== void 0 && !isErrorHandlerResult(ctx.types.awaited(method.returnType))) ctx.report({
627
+ code: INVALID_CONSUMER,
628
+ severity: "error",
629
+ message: `The @OnError method ${where} returns '${ctx.types.format(method.returnType)}': return 'ack' to acknowledge the message, or nothing to dead-letter it`,
630
+ location: decorator.location,
631
+ hints: ["Declare the return type `'ack' | undefined` (or `void`)"]
632
+ });
633
+ }
634
+ function analyzeHandlerParams(method, where, ctx) {
635
+ return Object.freeze(method.params.map((param) => analyzeHandlerParam(param, where, ctx)));
636
+ }
637
+ function analyzeHandlerParam(param, where, ctx) {
638
+ const decorators = parameterDecorators(param);
639
+ const decorator = decorators[0];
640
+ if (decorator === void 0) {
641
+ const foreign = foreignParameterDecorator(param);
642
+ if (foreign !== void 0) {
643
+ ctx.report({
644
+ code: INVALID_DECORATOR,
645
+ severity: "error",
646
+ message: `@${foreign.decorator.name} on parameter '${param.name}' of ${where} is imported from '${foreign.module}': @OnMessage handler parameters use ${foreign.name} from '${RUNTIME_MODULE}'`,
647
+ location: foreign.decorator.location,
648
+ hints: [`Import it from '${RUNTIME_MODULE}': import { ${foreign.name} } from '${RUNTIME_MODULE}'`]
649
+ });
650
+ return { kind: "undefined" };
651
+ }
652
+ if (param.optional || param.hasDefault || param.rest) return { kind: "undefined" };
653
+ ctx.report({
654
+ code: INVALID_CONSUMER,
655
+ severity: "error",
656
+ message: `Parameter '${param.name}' of ${where} has no @Message(), @Headers() or @Ctx() decorator`,
657
+ location: param.location,
658
+ hints: ["Every handler parameter is extracted from the message: @Message(schema?) for the payload, @Headers(name?) for the headers, @Ctx() for the InboundMessage"]
659
+ });
660
+ return { kind: "undefined" };
661
+ }
662
+ if (decorators.length > 1) ctx.report({
663
+ code: INVALID_DECORATOR,
664
+ severity: "error",
665
+ message: `Parameter '${param.name}' of ${where} has several messaging decorators (${decorators.map((found) => `@${found.name}`).join(", ")})`,
666
+ location: decorators[1].location,
667
+ hints: ["Keep a single one"]
668
+ });
669
+ if (param.rest) {
670
+ ctx.report({
671
+ code: INVALID_DECORATOR,
672
+ severity: "error",
673
+ message: `@${decorator.name} cannot be applied to the rest parameter '...${param.name}' of ${where}`,
674
+ location: decorator.location
675
+ });
676
+ return { kind: "undefined" };
677
+ }
678
+ switch (decorator.identity) {
679
+ case MessagingDecorator.Message: return messageParam(param, decorator, where, ctx);
680
+ case MessagingDecorator.Headers: return headersParam(param, decorator, where, ctx);
681
+ default: return {
682
+ kind: "message",
683
+ narrow: narrowsPayload(param.type, ctx.types)
684
+ };
685
+ }
686
+ }
687
+ /** `@Message()` / `@Message(schema)`. */
688
+ function messageParam(param, decorator, where, ctx) {
689
+ const arg = decorator.args[0];
690
+ if (arg === void 0 || arg.evaluated && arg.value === void 0) {
691
+ checkUnvalidated(param, decorator, where, ctx);
692
+ return { kind: "payload" };
693
+ }
694
+ if (arg.ref !== void 0) {
695
+ if (isClassReference(arg.ref)) {
696
+ ctx.report({
697
+ code: INVALID_DECORATOR,
698
+ severity: "error",
699
+ message: `@Message(${arg.text}) on ${where}: '${arg.ref.name}' is a class, not a Standard Schema`,
700
+ location: arg.location,
701
+ hints: ["Pass a Standard Schema (Zod, Valibot, ArkType...): export const OrderPlaced = z.object({ ... })", "Or inject the raw payload with @Message() and validate it in the handler"]
702
+ });
703
+ return { kind: "undefined" };
704
+ }
705
+ return {
706
+ kind: "validated",
707
+ schema: arg.ref
708
+ };
709
+ }
710
+ if (!arg.evaluated) return { kind: "undefined" };
711
+ ctx.report({
712
+ code: INVALID_DECORATOR,
713
+ severity: "error",
714
+ message: `@Message on ${where} expects a Standard Schema (Zod, Valibot, ArkType...), got ${describeValue(arg.value)}`,
715
+ location: arg.location,
716
+ hints: ["Declare the schema with `export const OrderPlaced = z.object({ ... })` and pass it: @Message(OrderPlaced)"]
717
+ });
718
+ return { kind: "undefined" };
719
+ }
720
+ /** `@Headers()` / `@Headers(name)`. */
721
+ function headersParam(param, decorator, where, ctx) {
722
+ const arg = decorator.args[0];
723
+ if (arg === void 0 || arg.evaluated && arg.value === void 0) return { kind: "headers" };
724
+ if (!arg.evaluated) return { kind: "undefined" };
725
+ const name = arg.value;
726
+ if (typeof name !== "string" || name === "") {
727
+ ctx.report({
728
+ code: INVALID_DECORATOR,
729
+ severity: "error",
730
+ message: `@Headers on ${where} expects a header name (a non-empty string), got ${describeValue(name)}`,
731
+ location: arg.location,
732
+ hints: ["Use @Headers() to inject every header"]
733
+ });
734
+ return { kind: "undefined" };
735
+ }
736
+ if (!param.optional && !param.hasDefault && !acceptsUndefined(param.type)) ctx.report({
737
+ code: INVALID_DECORATOR,
738
+ severity: "warning",
739
+ message: `@Headers(${JSON.stringify(name)}) injects 'string | undefined' (the header may be missing) but parameter '${param.name}' of ${where} is typed '${ctx.types.format(param.type)}'`,
740
+ location: param.typeLocation,
741
+ hints: [`Declare it '${param.name}?: string' or '${param.name}: string | undefined', or give it a default value`]
742
+ });
743
+ return {
744
+ kind: "header",
745
+ name
746
+ };
747
+ }
748
+ /** ZNIGHT017: a structured payload injected without schema is trusted blindly. */
749
+ function checkUnvalidated(param, decorator, where, ctx) {
750
+ const { type } = ctx.types.stripNullish(param.type);
751
+ if (isSafePayloadType(type)) return;
752
+ const typed = type.kind === "opaque" && type.reason === "missing-annotation" ? "has no type annotation" : `is typed '${ctx.types.format(param.type)}'`;
753
+ ctx.report({
754
+ code: UNVALIDATED_INPUT,
755
+ severity: ctx.options.strictValidation ? "error" : "warning",
756
+ message: `Unvalidated message payload: parameter '${param.name}' of ${where} ${typed} but @Message() has no schema`,
757
+ location: decorator.location,
758
+ hints: ["Validate it with a Standard Schema (Zod, Valibot, ArkType...): @Message(OrderPlaced)", "Or declare the parameter as 'unknown' and check it in the handler"]
759
+ });
760
+ }
761
+ /** Methods whose parameters carry messaging decorators without being `@OnMessage` handlers. */
762
+ function reportStrayParameterDecorators(method, where, errorHandler, ctx) {
763
+ const owner = errorHandler ? void 0 : foreignHandler(method);
764
+ for (const param of method.params) for (const decorator of parameterDecorators(param)) {
765
+ let hint = "Add @OnMessage() to the method, or remove the decorator";
766
+ if (errorHandler) hint = "@OnError methods are called with (error, message): remove the decorator";
767
+ else if (owner !== void 0) hint = `${where} is a @${owner.decorator.name} handler: use the parameter decorators of '${owner.module}'`;
768
+ ctx.report({
769
+ code: INVALID_DECORATOR,
770
+ severity: "error",
771
+ message: owner === void 0 ? `@${decorator.name} has no effect on parameter '${param.name}' of ${where}: it is not an @OnMessage handler` : `@${decorator.name} (from '${RUNTIME_MODULE}') has no effect on parameter '${param.name}' of ${where}: it is not an @OnMessage handler`,
772
+ location: decorator.location,
773
+ hints: [hint]
774
+ });
775
+ }
776
+ }
777
+ function splitIdentity(decorator) {
778
+ const identity = decorator.identity;
779
+ const separator = identity?.lastIndexOf("#") ?? -1;
780
+ if (identity === void 0 || separator <= 0) return void 0;
781
+ return {
782
+ decorator,
783
+ module: identity.slice(0, separator),
784
+ name: identity.slice(separator + 1)
785
+ };
786
+ }
787
+ /**
788
+ * A parameter decorator named like a messaging one (`Headers`, `Ctx`, `Message`) but imported from
789
+ * another package (`@znight/http` has `Headers` and `Ctx` too).
790
+ */
791
+ function foreignParameterDecorator(param) {
792
+ for (const decorator of param.decorators) {
793
+ const split = splitIdentity(decorator);
794
+ if (split === void 0 || split.module === "@znight/messaging") continue;
795
+ if (PARAMETER_DECORATORS.has(`@znight/messaging#${split.name}`)) return split;
796
+ }
797
+ }
798
+ /** The decorator through which another extension handles the method (`@Get` of `@znight/http`...). */
799
+ function foreignHandler(method) {
800
+ for (const decorator of method.decorators) {
801
+ const owner = decorator.owner;
802
+ if (owner === void 0 || owner === "messaging" || owner === "core" || owner === "manifest") continue;
803
+ const split = splitIdentity(decorator);
804
+ if (split !== void 0) return split;
805
+ }
806
+ }
807
+ function decoratorsOf(decorators, identity) {
808
+ return decorators.filter((decorator) => decorator.identity === identity);
809
+ }
810
+ /**
811
+ * Messaging decorators of a parameter. De-duplicated by position: the compiler (0.1) reports the
812
+ * decorators of a parameter with a default value (`@Headers('x') x = 'a'`) twice.
813
+ */
814
+ function parameterDecorators(param) {
815
+ const seen = /* @__PURE__ */ new Set();
816
+ return param.decorators.filter((decorator) => {
817
+ if (!isParameterDecorator(decorator) || seen.has(decorator.location.start)) return false;
818
+ seen.add(decorator.location.start);
819
+ return true;
820
+ });
821
+ }
822
+ function isParameterDecorator(decorator) {
823
+ return decorator.identity !== void 0 && PARAMETER_DECORATORS.has(decorator.identity);
824
+ }
825
+ /** A reference to a class declaration (`@Message(OrderDto)`). */
826
+ function isClassReference(ref) {
827
+ const symbol = ref.symbol;
828
+ return symbol.kind === "declaration" && symbol.declaration.kind === "class";
829
+ }
830
+ /** Primitive and binary payload types need no schema (`unknown` is honest, `any` is not). */
831
+ function isSafePayloadType(type) {
832
+ switch (type.kind) {
833
+ case "primitive": return type.name !== "any" && type.name !== "object";
834
+ case "literal": return true;
835
+ case "union": return type.types.every(isSafePayloadType);
836
+ case "reference": return BINARY_TYPES.has(type.identity);
837
+ default: return false;
838
+ }
839
+ }
840
+ /** Whether `undefined` is assignable to the type (unknown types are given the benefit of the doubt). */
841
+ function acceptsUndefined(type) {
842
+ switch (type.kind) {
843
+ case "primitive": return type.name === "any" || type.name === "unknown" || type.name === "undefined" || type.name === "void";
844
+ case "union": return type.types.some(acceptsUndefined);
845
+ case "opaque":
846
+ case "typeParameter": return true;
847
+ default: return false;
848
+ }
849
+ }
850
+ /** Valid results of an `@OnError` method: `'ack'`, nothing (`void`, `undefined`), or unknown types. */
851
+ function isErrorHandlerResult(type) {
852
+ switch (type.kind) {
853
+ case "primitive": return [
854
+ "void",
855
+ "undefined",
856
+ "any",
857
+ "unknown",
858
+ "never"
859
+ ].includes(type.name);
860
+ case "literal": return type.value === "ack";
861
+ case "union": return type.types.every(isErrorHandlerResult);
862
+ case "promise": return isErrorHandlerResult(type.inner);
863
+ case "opaque":
864
+ case "typeParameter": return true;
865
+ default: return false;
866
+ }
867
+ }
868
+ /** `@Ctx() message: InboundMessage<Order>` narrows the payload: the invoker casts `m`. */
869
+ function narrowsPayload(type, types) {
870
+ const { type: inner } = types.stripNullish(type);
871
+ if (inner.kind !== "reference" || inner.identity !== "@znight/messaging#InboundMessage") return false;
872
+ const payload = inner.typeArgs[0];
873
+ return payload !== void 0 && !(payload.kind === "primitive" && (payload.name === "unknown" || payload.name === "any"));
874
+ }
875
+ // src/compiler/generate.ts
876
+ /** Builds the messaging section; `undefined` (no section) when the application has no consumer. */
877
+ function generateMessaging(ctx) {
878
+ const consumers = ctx.stored.filter((data) => data.kind === "consumer").sort((a, b) => compareStrings(a.token, b.token));
879
+ if (consumers.length === 0) return void 0;
880
+ const helpers = lazyHelpers(ctx);
881
+ return ctx.obj([["consumers", ctx.arr(consumers.map((consumer) => consumerNode(ctx, consumer, helpers)))]]);
882
+ }
883
+ function consumerNode(ctx, consumer, helpers) {
884
+ let classType;
885
+ const consumerType = () => {
886
+ classType ??= ctx.importType(consumer.identity);
887
+ return classType;
888
+ };
889
+ const entries = [["token", consumer.token], ["queue", consumer.queue]];
890
+ if (consumer.options !== void 0) entries.push(["options", ctx.lit(consumer.options)]);
891
+ entries.push(["handlers", ctx.arr(consumer.handlers.map((handler) => handlerNode(ctx, handler, consumerType, helpers)))]);
892
+ if (consumer.errorHandler !== void 0) entries.push(["errorHandler", consumer.errorHandler]);
893
+ return ctx.obj(entries);
894
+ }
895
+ function handlerNode(ctx, handler, consumerType, helpers) {
896
+ const entries = [["method", handler.method]];
897
+ if (handler.type !== void 0) entries.push(["type", handler.type]);
898
+ if (handler.retry !== void 0) entries.push(["retry", ctx.lit({ ...handler.retry })]);
899
+ entries.push(["invoke", invokeNode(ctx, handler, consumerType, helpers)]);
900
+ return ctx.obj(entries);
901
+ }
902
+ /**
903
+ * `(c: Consumer, m: InboundMessage) => c.method(...args)`, `async` only when it awaits a schema
904
+ * validation. `m` is omitted when no argument uses it (`noUnusedParameters`).
905
+ */
906
+ function invokeNode(ctx, handler, consumerType, helpers) {
907
+ const params = [...handler.params];
908
+ while (params.length > 0 && params[params.length - 1]?.kind === "undefined") params.pop();
909
+ if (params.length === 0) return ctx.arrow([["c", consumerType()]], (c) => ctx.call(ctx.member(c, handler.method)));
910
+ return ctx.arrow([["c", consumerType()], ["m", helpers.inboundMessage()]], (c, m) => ctx.call(ctx.member(c, handler.method), params.map((param) => argumentNode(ctx, param, m, helpers))), { async: params.some((param) => param.kind === "validated") });
911
+ }
912
+ function argumentNode(ctx, param, m, helpers) {
913
+ switch (param.kind) {
914
+ case "payload": return ctx.code`${ctx.member(m, "payload")} as never`;
915
+ case "validated": return ctx.code`await ${ctx.call(helpers.validateMessage(), [ctx.importValue(param.schema), ctx.member(m, "payload")])}`;
916
+ case "headers": return ctx.member(m, "headers");
917
+ case "header": return ctx.call(helpers.messageHeader(), [m, param.name]);
918
+ case "message": return param.narrow ? ctx.code`${m} as ${helpers.inboundMessage()}<never>` : m;
919
+ case "undefined": return ctx.lit(void 0);
920
+ }
921
+ }
922
+ function lazyHelpers(ctx) {
923
+ const cache = /* @__PURE__ */ new Map();
924
+ const get = (name, type) => {
925
+ let node = cache.get(name);
926
+ if (node === void 0) {
927
+ node = ctx.importFrom(RUNTIME_MODULE, name, type ? { type: true } : {});
928
+ cache.set(name, node);
929
+ }
930
+ return node;
931
+ };
932
+ return {
933
+ inboundMessage: () => get("InboundMessage", true),
934
+ validateMessage: () => get("validateMessage", false),
935
+ messageHeader: () => get("messageHeader", false)
936
+ };
937
+ }
938
+ // src/compiler/validate.ts
939
+ /** Cross-consumer checks: one consumer per queue, messaging decorators outside of consumers. */
940
+ function validateMessaging(ctx) {
941
+ const consumers = ctx.stored.filter((data) => data.kind === "consumer").sort((a, b) => compareStrings(a.token, b.token));
942
+ const byQueue = /* @__PURE__ */ new Map();
943
+ for (const consumer of consumers) {
944
+ if (consumer.queue === "") continue;
945
+ const first = byQueue.get(consumer.queue);
946
+ if (first === void 0) {
947
+ byQueue.set(consumer.queue, consumer);
948
+ continue;
949
+ }
950
+ ctx.report({
951
+ code: INVALID_CONSUMER,
952
+ severity: "error",
953
+ message: `${first.displayName} and ${consumer.displayName} both consume "${consumer.queue}"`,
954
+ location: consumer.location,
955
+ hints: ["A queue is consumed by a single @Consumer class: merge the handlers, or route by type with @OnMessage(type)"],
956
+ related: [{
957
+ message: `${first.displayName} consumes "${consumer.queue}"`,
958
+ location: first.location
959
+ }]
960
+ });
961
+ }
962
+ const bases = new Set(consumers.flatMap((consumer) => consumer.chain));
963
+ for (const data of ctx.stored) {
964
+ if (data.kind !== "members" || data.abstract || bases.has(data.identity)) continue;
965
+ reportNotAConsumer(data, ctx);
966
+ }
967
+ }
968
+ function reportNotAConsumer(data, ctx) {
969
+ for (const decorator of data.decorators) ctx.report({
970
+ code: INVALID_DECORATOR,
971
+ severity: "error",
972
+ message: `@${decorator.name} has no effect: ${data.displayName} is not a @Consumer`,
973
+ location: decorator.location,
974
+ hints: [`Add @Consumer('queue') to ${data.displayName}, or extend it from a @Consumer class`, "Base classes holding shared handlers should be abstract"]
975
+ });
976
+ }
977
+ // src/compiler/index.ts
978
+ /**
979
+ * Decorators owned by the extension. `static` arguments must be statically evaluable (queue names,
980
+ * types, retry options, header names); the `@Message` schema is a `value` (an exported binding).
981
+ */
982
+ const decorators = Object.freeze({
983
+ [MessagingDecorator.Consumer]: {
984
+ targets: ["class"],
985
+ params: ["static"]
986
+ },
987
+ [MessagingDecorator.OnMessage]: {
988
+ targets: ["method"],
989
+ params: ["static"]
990
+ },
991
+ [MessagingDecorator.Retry]: {
992
+ targets: ["method"],
993
+ params: ["static"]
994
+ },
995
+ [MessagingDecorator.OnError]: {
996
+ targets: ["method"],
997
+ params: []
998
+ },
999
+ [MessagingDecorator.Message]: {
1000
+ targets: ["parameter"],
1001
+ params: ["value"]
1002
+ },
1003
+ [MessagingDecorator.Headers]: {
1004
+ targets: ["parameter"],
1005
+ params: ["static"]
1006
+ },
1007
+ [MessagingDecorator.Ctx]: {
1008
+ targets: ["parameter"],
1009
+ params: []
1010
+ }
1011
+ });
1012
+ /** The `@znight/messaging` compiler extension. */
1013
+ const messagingCompilerExtension = Object.freeze({
1014
+ apiVersion: 1,
1015
+ name: EXTENSION_NAME,
1016
+ decorators,
1017
+ analyzeClass: analyzeMessagingClass,
1018
+ validate: validateMessaging,
1019
+ generate: generateMessaging
1020
+ });
1021
+ export { messagingCompilerExtension as default, messagingCompilerExtension };
1022
+
1023
+ //# sourceMappingURL=compiler.mjs.map