@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,216 @@
1
+ import { ErrorCode, ZnightError } from "@znight/core";
2
+ // src/errors.ts
3
+ const NON_RETRYABLE = Symbol.for("znight.messaging.nonRetryable");
4
+ /**
5
+ * Thrown by a handler to dead-letter the message immediately, without further attempts (invalid
6
+ * business data, permanent failure...). Subclass it for domain errors.
7
+ *
8
+ * ```ts
9
+ * if (order.total < 0) throw new NonRetryableError('Negative order total')
10
+ * ```
11
+ */
12
+ var NonRetryableError = class extends Error {
13
+ constructor(message, options) {
14
+ super(message, options);
15
+ this.name = "NonRetryableError";
16
+ }
17
+ };
18
+ Object.defineProperty(NonRetryableError.prototype, NON_RETRYABLE, { value: true });
19
+ /**
20
+ * Whether retrying `error` is pointless: a {@link NonRetryableError} (also recognised across copies of
21
+ * this package, through a global symbol brand).
22
+ */
23
+ function isNonRetryable(error) {
24
+ return error instanceof NonRetryableError || error !== null && typeof error === "object" && error[NON_RETRYABLE] === true;
25
+ }
26
+ /** The payload does not match the `@Message(schema)` of the handler. Never retried. */
27
+ var MessageValidationError = class extends NonRetryableError {
28
+ issues;
29
+ constructor(label, issues) {
30
+ super(`Invalid ${label}: ${formatIssues(issues)}`);
31
+ this.name = "MessageValidationError";
32
+ this.issues = issues;
33
+ }
34
+ };
35
+ /** The body cannot be decoded (malformed JSON...). Never retried. */
36
+ var MessageDecodeError = class extends NonRetryableError {
37
+ constructor(message, options) {
38
+ super(message, options);
39
+ this.name = "MessageDecodeError";
40
+ }
41
+ };
42
+ /** No `@OnMessage` handler of the consumer matches the message type. Never retried. */
43
+ var NoHandlerError = class extends NonRetryableError {
44
+ messageType;
45
+ constructor(consumer, type) {
46
+ super(type === void 0 ? `${consumer} has no default @OnMessage() handler for a message without type` : `${consumer} has no @OnMessage handler for message type "${type}"`);
47
+ this.name = "NoHandlerError";
48
+ this.messageType = type;
49
+ }
50
+ };
51
+ /** A delivery arrived after its last allowed attempt (e.g. redelivered after a dead-letter failure). */
52
+ var AttemptsExhaustedError = class extends NonRetryableError {
53
+ constructor(attempt, maxAttempts) {
54
+ super(`Delivery attempt ${attempt} exceeds the ${maxAttempts} attempt(s) allowed by the retry policy`);
55
+ this.name = "AttemptsExhaustedError";
56
+ }
57
+ };
58
+ function formatIssues(issues) {
59
+ const shown = issues.slice(0, 5).map((issue) => issue.path === "" ? issue.message : `${issue.path}: ${issue.message}`);
60
+ const more = issues.length > 5 ? ` (+${issues.length - 5} more)` : "";
61
+ return `${shown.join("; ")}${more}`;
62
+ }
63
+ /** A messaging configuration error (ZNIGHT011): invalid consumer, retry policy, driver options... */
64
+ function consumerError(message, hints) {
65
+ return new ZnightError(ErrorCode.InvalidConsumer, message, hints === void 0 ? {} : { hints });
66
+ }
67
+ /** An operation on the broker or a driver that is not available (not connected, closed...). */
68
+ function brokerUnavailableError(message, cause) {
69
+ return new ZnightError(ErrorCode.LifecycleFailed, message, cause === void 0 ? {} : { cause });
70
+ }
71
+ // src/headers.ts
72
+ /**
73
+ * Header names used by Znight on the wire. They are plain message headers (AMQP headers, SQS message
74
+ * attributes, Kafka record headers), so non-Znight producers and consumers can read and write them.
75
+ */
76
+ const MessagingHeaders = Object.freeze({
77
+ Id: "x-znight-id",
78
+ Type: "x-znight-type",
79
+ ContentType: "content-type",
80
+ Attempt: "x-znight-attempt",
81
+ NotBefore: "x-znight-not-before",
82
+ Encoding: "x-znight-encoding",
83
+ DeadLetterReason: "x-znight-dead-letter-reason",
84
+ Error: "x-znight-error",
85
+ OriginalQueue: "x-znight-original-queue",
86
+ Attempts: "x-znight-attempts",
87
+ ConsumerGroup: "x-znight-consumer-group"
88
+ });
89
+ /** A record safe to fill with untrusted keys (`__proto__` is stored as a regular key). */
90
+ function headerRecord() {
91
+ return Object.create(null);
92
+ }
93
+ /**
94
+ * Reads a header: exact name first, then a case-insensitive match. Used by the code generated for
95
+ * `@Headers('name')` parameters.
96
+ */
97
+ function messageHeader(message, name) {
98
+ const headers = message.headers;
99
+ if (Object.hasOwn(headers, name)) return headers[name];
100
+ const lower = name.toLowerCase();
101
+ for (const key of Object.keys(headers)) if (key.toLowerCase() === lower) return headers[key];
102
+ }
103
+ /** Parses the {@link MessagingHeaders.Attempt} header: a positive integer, `1` when absent or invalid. */
104
+ function attemptFromHeaders(headers) {
105
+ const value = Object.hasOwn(headers, MessagingHeaders.Attempt) ? headers[MessagingHeaders.Attempt] : void 0;
106
+ if (value === void 0) return 1;
107
+ const attempt = Number(value);
108
+ return Number.isSafeInteger(attempt) && attempt >= 1 ? attempt : 1;
109
+ }
110
+ // src/internal/async.ts
111
+ /** Internal async helpers (not part of the public API). */
112
+ /** Rejection reason of operations interrupted by an `AbortSignal`. */
113
+ var AbortedError = class extends Error {
114
+ constructor(message = "The operation was aborted") {
115
+ super(message);
116
+ this.name = "AbortError";
117
+ }
118
+ };
119
+ /**
120
+ * Longest delay of one timer (2^31 - 1 ms, about 24.8 days): longer `setTimeout` delays overflow and
121
+ * fire after 1 ms. Longer waits chain several timers.
122
+ */
123
+ const MAX_TIMER_DELAY_MS = 2147483647;
124
+ /**
125
+ * Resolves after `ms` milliseconds (any duration: long waits chain timers), or rejects with
126
+ * {@link AbortedError} as soon as `signal` aborts.
127
+ */
128
+ function sleep(ms, signal) {
129
+ if (signal?.aborted === true) return Promise.reject(new AbortedError());
130
+ if (ms <= 0 && signal === void 0) return Promise.resolve();
131
+ return new Promise((resolve, reject) => {
132
+ let timer;
133
+ const onAbort = () => {
134
+ clearTimeout(timer);
135
+ reject(new AbortedError());
136
+ };
137
+ const arm = (remaining) => {
138
+ const wait = Math.min(remaining, MAX_TIMER_DELAY_MS);
139
+ timer = setTimeout(() => {
140
+ if (remaining > wait) return arm(remaining - wait);
141
+ signal?.removeEventListener("abort", onAbort);
142
+ resolve();
143
+ }, wait);
144
+ };
145
+ arm(ms);
146
+ signal?.addEventListener("abort", onAbort, { once: true });
147
+ });
148
+ }
149
+ /**
150
+ * Counting semaphore. `acquire()` resolves when a permit is available, in FIFO order, or rejects with
151
+ * {@link AbortedError} when `signal` aborts while waiting.
152
+ */
153
+ var Semaphore = class {
154
+ #available;
155
+ #waiters = [];
156
+ constructor(permits) {
157
+ this.#available = permits;
158
+ }
159
+ /** Permits currently available. */
160
+ get available() {
161
+ return this.#available;
162
+ }
163
+ /** Number of callers waiting for a permit. */
164
+ get waiting() {
165
+ return this.#waiters.length;
166
+ }
167
+ acquire(signal) {
168
+ if (signal?.aborted === true) return Promise.reject(new AbortedError());
169
+ if (this.#available > 0) {
170
+ this.#available--;
171
+ return Promise.resolve();
172
+ }
173
+ return new Promise((resolve, reject) => {
174
+ const waiter = {
175
+ resolve: () => {
176
+ signal?.removeEventListener("abort", onAbort);
177
+ resolve();
178
+ },
179
+ reject
180
+ };
181
+ const onAbort = () => {
182
+ const index = this.#waiters.indexOf(waiter);
183
+ if (index !== -1) this.#waiters.splice(index, 1);
184
+ reject(new AbortedError());
185
+ };
186
+ this.#waiters.push(waiter);
187
+ signal?.addEventListener("abort", onAbort, { once: true });
188
+ });
189
+ }
190
+ release() {
191
+ const next = this.#waiters.shift();
192
+ if (next === void 0) this.#available++;
193
+ else next.resolve();
194
+ }
195
+ };
196
+ /** Message of any thrown value. */
197
+ function errorMessage(error) {
198
+ if (error instanceof Error) return error.message;
199
+ if (typeof error === "string") return error;
200
+ try {
201
+ return String(error);
202
+ } catch {
203
+ return Object.prototype.toString.call(error);
204
+ }
205
+ }
206
+ /** Truncates `text` to `max` characters (with an ellipsis). */
207
+ function truncate(text, max) {
208
+ return text.length <= max ? text : `${text.slice(0, Math.max(0, max - 1))}…`;
209
+ }
210
+ /** Whether `value` is a positive integer. */
211
+ function isPositiveInteger(value) {
212
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
213
+ }
214
+ export { consumerError as _, sleep as a, attemptFromHeaders as c, AttemptsExhaustedError as d, MessageDecodeError as f, brokerUnavailableError as g, NonRetryableError as h, isPositiveInteger as i, headerRecord as l, NoHandlerError as m, Semaphore as n, truncate as o, MessageValidationError as p, errorMessage as r, MessagingHeaders as s, MAX_TIMER_DELAY_MS as t, messageHeader as u, isNonRetryable as v };
215
+
216
+ //# sourceMappingURL=async-CFh_bvex.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"async-CFh_bvex.mjs","names":[],"sources":["../src/errors.ts","../src/headers.ts","../src/internal/async.ts"],"sourcesContent":["import { ErrorCode, ZnightError } from '@znight/core'\n\nconst NON_RETRYABLE = Symbol.for('znight.messaging.nonRetryable')\n\n/**\n * Thrown by a handler to dead-letter the message immediately, without further attempts (invalid\n * business data, permanent failure...). Subclass it for domain errors.\n *\n * ```ts\n * if (order.total < 0) throw new NonRetryableError('Negative order total')\n * ```\n */\nexport class NonRetryableError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options)\n this.name = 'NonRetryableError'\n }\n}\nObject.defineProperty(NonRetryableError.prototype, NON_RETRYABLE, { value: true })\n\n/**\n * Whether retrying `error` is pointless: a {@link NonRetryableError} (also recognised across copies of\n * this package, through a global symbol brand).\n */\nexport function isNonRetryable(error: unknown): boolean {\n return (\n error instanceof NonRetryableError ||\n (error !== null && typeof error === 'object' && (error as Record<symbol, unknown>)[NON_RETRYABLE] === true)\n )\n}\n\n/** One failed check of a `@Message(schema)` validation. */\nexport interface MessageValidationIssue {\n /** Dotted path of the invalid value (`items.0.quantity`), empty for the root. */\n readonly path: string\n readonly message: string\n}\n\n/** The payload does not match the `@Message(schema)` of the handler. Never retried. */\nexport class MessageValidationError extends NonRetryableError {\n readonly issues: readonly MessageValidationIssue[]\n\n constructor(label: string, issues: readonly MessageValidationIssue[]) {\n super(`Invalid ${label}: ${formatIssues(issues)}`)\n this.name = 'MessageValidationError'\n this.issues = issues\n }\n}\n\n/** The body cannot be decoded (malformed JSON...). Never retried. */\nexport class MessageDecodeError extends NonRetryableError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options)\n this.name = 'MessageDecodeError'\n }\n}\n\n/** No `@OnMessage` handler of the consumer matches the message type. Never retried. */\nexport class NoHandlerError extends NonRetryableError {\n readonly messageType: string | undefined\n\n constructor(consumer: string, type: string | undefined) {\n super(\n type === undefined\n ? `${consumer} has no default @OnMessage() handler for a message without type`\n : `${consumer} has no @OnMessage handler for message type \"${type}\"`,\n )\n this.name = 'NoHandlerError'\n this.messageType = type\n }\n}\n\n/** A delivery arrived after its last allowed attempt (e.g. redelivered after a dead-letter failure). */\nexport class AttemptsExhaustedError extends NonRetryableError {\n constructor(attempt: number, maxAttempts: number) {\n super(`Delivery attempt ${attempt} exceeds the ${maxAttempts} attempt(s) allowed by the retry policy`)\n this.name = 'AttemptsExhaustedError'\n }\n}\n\nfunction formatIssues(issues: readonly MessageValidationIssue[]): string {\n const shown = issues.slice(0, 5).map(issue => (issue.path === '' ? issue.message : `${issue.path}: ${issue.message}`))\n const more = issues.length > 5 ? ` (+${issues.length - 5} more)` : ''\n return `${shown.join('; ')}${more}`\n}\n\n/** A messaging configuration error (ZNIGHT011): invalid consumer, retry policy, driver options... */\nexport function consumerError(message: string, hints?: readonly string[]): ZnightError {\n return new ZnightError(ErrorCode.InvalidConsumer, message, hints === undefined ? {} : { hints })\n}\n\n/** An operation on the broker or a driver that is not available (not connected, closed...). */\nexport function brokerUnavailableError(message: string, cause?: unknown): ZnightError {\n return new ZnightError(ErrorCode.LifecycleFailed, message, cause === undefined ? {} : { cause })\n}\n","/**\n * Header names used by Znight on the wire. They are plain message headers (AMQP headers, SQS message\n * attributes, Kafka record headers), so non-Znight producers and consumers can read and write them.\n */\nexport const MessagingHeaders: {\n /** Znight message id (Kafka and SQS; AMQP uses the `message-id` property). */\n readonly Id: 'x-znight-id'\n /** Message type used to select the handler (Kafka and SQS; AMQP uses the `type` property). */\n readonly Type: 'x-znight-type'\n /** Content type of the body (Kafka and SQS; AMQP uses the `content-type` property). */\n readonly ContentType: 'content-type'\n /** 1-based attempt number carried by republished retries (RabbitMQ, Kafka). */\n readonly Attempt: 'x-znight-attempt'\n /** Epoch milliseconds before which a Kafka retry record must not be processed. */\n readonly NotBefore: 'x-znight-not-before'\n /** Body encoding when the transport only carries text (SQS: `base64` for binary payloads). */\n readonly Encoding: 'x-znight-encoding'\n /** Why the message was dead-lettered (`DeadLetterReason`). */\n readonly DeadLetterReason: 'x-znight-dead-letter-reason'\n /** Message of the error that dead-lettered the message (truncated). */\n readonly Error: 'x-znight-error'\n /** Queue the dead-lettered message was consumed from. */\n readonly OriginalQueue: 'x-znight-original-queue'\n /** Number of attempts made before the message was dead-lettered. */\n readonly Attempts: 'x-znight-attempts'\n /** Kafka consumer group that dead-lettered the record. */\n readonly ConsumerGroup: 'x-znight-consumer-group'\n} = Object.freeze({\n Id: 'x-znight-id',\n Type: 'x-znight-type',\n ContentType: 'content-type',\n Attempt: 'x-znight-attempt',\n NotBefore: 'x-znight-not-before',\n Encoding: 'x-znight-encoding',\n DeadLetterReason: 'x-znight-dead-letter-reason',\n Error: 'x-znight-error',\n OriginalQueue: 'x-znight-original-queue',\n Attempts: 'x-znight-attempts',\n ConsumerGroup: 'x-znight-consumer-group',\n})\n\n/** A record safe to fill with untrusted keys (`__proto__` is stored as a regular key). */\nexport function headerRecord(): Record<string, string> {\n return Object.create(null) as Record<string, string>\n}\n\n/**\n * Reads a header: exact name first, then a case-insensitive match. Used by the code generated for\n * `@Headers('name')` parameters.\n */\nexport function messageHeader(\n message: { readonly headers: Readonly<Record<string, string>> },\n name: string,\n): string | undefined {\n const headers = message.headers\n if (Object.hasOwn(headers, name)) return headers[name]\n const lower = name.toLowerCase()\n for (const key of Object.keys(headers)) {\n if (key.toLowerCase() === lower) return headers[key]\n }\n return undefined\n}\n\n/** Parses the {@link MessagingHeaders.Attempt} header: a positive integer, `1` when absent or invalid. */\nexport function attemptFromHeaders(headers: Readonly<Record<string, string>>): number {\n const value = Object.hasOwn(headers, MessagingHeaders.Attempt) ? headers[MessagingHeaders.Attempt] : undefined\n if (value === undefined) return 1\n const attempt = Number(value)\n return Number.isSafeInteger(attempt) && attempt >= 1 ? attempt : 1\n}\n","/** Internal async helpers (not part of the public API). */\n\n/** Rejection reason of operations interrupted by an `AbortSignal`. */\nexport class AbortedError extends Error {\n constructor(message = 'The operation was aborted') {\n super(message)\n this.name = 'AbortError'\n }\n}\n\n/**\n * Longest delay of one timer (2^31 - 1 ms, about 24.8 days): longer `setTimeout` delays overflow and\n * fire after 1 ms. Longer waits chain several timers.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647\n\n/**\n * Resolves after `ms` milliseconds (any duration: long waits chain timers), or rejects with\n * {@link AbortedError} as soon as `signal` aborts.\n */\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted === true) return Promise.reject(new AbortedError())\n if (ms <= 0 && signal === undefined) return Promise.resolve()\n return new Promise((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | undefined\n const onAbort = (): void => {\n clearTimeout(timer)\n reject(new AbortedError())\n }\n const arm = (remaining: number): void => {\n const wait = Math.min(remaining, MAX_TIMER_DELAY_MS)\n timer = setTimeout(() => {\n if (remaining > wait) return arm(remaining - wait)\n signal?.removeEventListener('abort', onAbort)\n resolve()\n }, wait)\n }\n arm(ms)\n signal?.addEventListener('abort', onAbort, { once: true })\n })\n}\n\n/**\n * Counting semaphore. `acquire()` resolves when a permit is available, in FIFO order, or rejects with\n * {@link AbortedError} when `signal` aborts while waiting.\n */\nexport class Semaphore {\n #available: number\n readonly #waiters: { resolve: () => void; reject: (error: unknown) => void }[] = []\n\n constructor(permits: number) {\n this.#available = permits\n }\n\n /** Permits currently available. */\n get available(): number {\n return this.#available\n }\n\n /** Number of callers waiting for a permit. */\n get waiting(): number {\n return this.#waiters.length\n }\n\n acquire(signal?: AbortSignal): Promise<void> {\n if (signal?.aborted === true) return Promise.reject(new AbortedError())\n if (this.#available > 0) {\n this.#available--\n return Promise.resolve()\n }\n return new Promise((resolve, reject) => {\n const waiter = {\n resolve: (): void => {\n signal?.removeEventListener('abort', onAbort)\n resolve()\n },\n reject,\n }\n const onAbort = (): void => {\n const index = this.#waiters.indexOf(waiter)\n if (index !== -1) this.#waiters.splice(index, 1)\n reject(new AbortedError())\n }\n this.#waiters.push(waiter)\n signal?.addEventListener('abort', onAbort, { once: true })\n })\n }\n\n release(): void {\n const next = this.#waiters.shift()\n if (next === undefined) this.#available++\n else next.resolve()\n }\n}\n\n/** Message of any thrown value. */\nexport function errorMessage(error: unknown): string {\n if (error instanceof Error) return error.message\n if (typeof error === 'string') return error\n try {\n return String(error)\n } catch {\n return Object.prototype.toString.call(error)\n }\n}\n\n/** Truncates `text` to `max` characters (with an ellipsis). */\nexport function truncate(text: string, max: number): string {\n return text.length <= max ? text : `${text.slice(0, Math.max(0, max - 1))}…`\n}\n\n/** Whether `value` is a positive integer. */\nexport function isPositiveInteger(value: unknown): value is number {\n return typeof value === 'number' && Number.isSafeInteger(value) && value > 0\n}\n"],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ import { f as MessageDecodeError } from "./async-CFh_bvex.mjs";
2
+ // src/codec.ts
3
+ const JSON_CONTENT_TYPE = "application/json";
4
+ const TEXT_CONTENT_TYPE = "text/plain; charset=utf-8";
5
+ const BINARY_CONTENT_TYPE = "application/octet-stream";
6
+ const encoder = new TextEncoder();
7
+ const decoder = new TextDecoder("utf-8");
8
+ /** Body as text (UTF-8). */
9
+ function bodyToText(body) {
10
+ return typeof body === "string" ? body : decoder.decode(body);
11
+ }
12
+ /** Body as bytes (UTF-8 for strings). */
13
+ function bodyToBytes(body) {
14
+ return typeof body === "string" ? encoder.encode(body) : body;
15
+ }
16
+ /** Lower-cased media type without parameters (`application/json; charset=utf-8` → `application/json`). */
17
+ function mediaType(contentType) {
18
+ if (contentType === void 0) return void 0;
19
+ const value = contentType.split(";", 1)[0]?.trim().toLowerCase();
20
+ return value === "" ? void 0 : value;
21
+ }
22
+ function isJson(type) {
23
+ return type === "application/json" || type.endsWith("+json");
24
+ }
25
+ /**
26
+ * The default codec.
27
+ *
28
+ * - encode: `Uint8Array` → `application/octet-stream`; a `string` with an explicit text/binary
29
+ * `contentType` is sent as is; everything else is JSON (`application/json`), including strings.
30
+ * `undefined` is an empty JSON body.
31
+ * - decode: JSON (`application/json`, `*+json`) is parsed (empty body → `undefined`; malformed →
32
+ * `MessageDecodeError`); `text/*` → string; other declared types → `Uint8Array`. Without content
33
+ * type, JSON is attempted and the text is returned when it is not valid JSON.
34
+ */
35
+ const jsonCodec = Object.freeze({
36
+ encode(payload, contentType) {
37
+ const type = mediaType(contentType);
38
+ if (payload instanceof Uint8Array) return {
39
+ body: payload,
40
+ contentType: contentType ?? "application/octet-stream"
41
+ };
42
+ if (typeof payload === "string" && type !== void 0 && !isJson(type)) return {
43
+ body: payload,
44
+ contentType
45
+ };
46
+ if (payload === void 0) return {
47
+ body: "",
48
+ contentType: contentType ?? "application/json"
49
+ };
50
+ const json = JSON.stringify(payload);
51
+ if (json === void 0) throw new TypeError(`[znight] Cannot encode a ${typeof payload} payload as JSON`);
52
+ return {
53
+ body: json,
54
+ contentType: contentType ?? "application/json"
55
+ };
56
+ },
57
+ decode(body, contentType) {
58
+ const type = mediaType(contentType);
59
+ if (type === void 0) {
60
+ const text = bodyToText(body);
61
+ if (text === "") return void 0;
62
+ try {
63
+ return JSON.parse(text);
64
+ } catch {
65
+ return text;
66
+ }
67
+ }
68
+ if (isJson(type)) {
69
+ const text = bodyToText(body);
70
+ if (text.trim() === "") return void 0;
71
+ try {
72
+ return JSON.parse(text);
73
+ } catch (error) {
74
+ throw new MessageDecodeError(`Malformed JSON body: ${error.message}`, { cause: error });
75
+ }
76
+ }
77
+ if (type.startsWith("text/")) return bodyToText(body);
78
+ return bodyToBytes(body);
79
+ }
80
+ });
81
+ export { bodyToText as a, bodyToBytes as i, JSON_CONTENT_TYPE as n, jsonCodec as o, TEXT_CONTENT_TYPE as r, mediaType as s, BINARY_CONTENT_TYPE as t };
82
+
83
+ //# sourceMappingURL=codec-Dku-NXXU.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec-Dku-NXXU.mjs","names":[],"sources":["../src/codec.ts"],"sourcesContent":["import { MessageDecodeError } from './errors.ts'\n\n/** An encoded body and its content type. */\nexport interface EncodedBody {\n readonly body: Uint8Array | string\n readonly contentType: string\n}\n\n/**\n * Converts payloads to bodies and back. The default {@link jsonCodec} handles JSON, text and binary\n * payloads; provide your own (MessagePack, Avro...) with `MessagingPlugin.configure({ codec })`.\n */\nexport interface MessageCodec {\n /**\n * Encodes a payload for publication. `contentType` is the one requested by the publisher, if any\n * (`publish(queue, payload, { contentType })`).\n */\n encode(payload: unknown, contentType: string | undefined): EncodedBody\n /**\n * Decodes a received body. Throw a {@link MessageDecodeError} (a `NonRetryableError`) for malformed\n * bodies: the message is dead-lettered with reason `decode`.\n */\n decode(body: Uint8Array | string, contentType: string | undefined): unknown\n}\n\nexport const JSON_CONTENT_TYPE = 'application/json'\nexport const TEXT_CONTENT_TYPE = 'text/plain; charset=utf-8'\nexport const BINARY_CONTENT_TYPE = 'application/octet-stream'\n\nconst encoder = new TextEncoder()\nconst decoder = new TextDecoder('utf-8')\n\n/** Body as text (UTF-8). */\nexport function bodyToText(body: Uint8Array | string): string {\n return typeof body === 'string' ? body : decoder.decode(body)\n}\n\n/** Body as bytes (UTF-8 for strings). */\nexport function bodyToBytes(body: Uint8Array | string): Uint8Array {\n return typeof body === 'string' ? encoder.encode(body) : body\n}\n\n/** Lower-cased media type without parameters (`application/json; charset=utf-8` → `application/json`). */\nexport function mediaType(contentType: string | undefined): string | undefined {\n if (contentType === undefined) return undefined\n const value = contentType.split(';', 1)[0]?.trim().toLowerCase()\n return value === '' ? undefined : value\n}\n\nfunction isJson(type: string): boolean {\n return type === JSON_CONTENT_TYPE || type.endsWith('+json')\n}\n\n/**\n * The default codec.\n *\n * - encode: `Uint8Array` → `application/octet-stream`; a `string` with an explicit text/binary\n * `contentType` is sent as is; everything else is JSON (`application/json`), including strings.\n * `undefined` is an empty JSON body.\n * - decode: JSON (`application/json`, `*+json`) is parsed (empty body → `undefined`; malformed →\n * `MessageDecodeError`); `text/*` → string; other declared types → `Uint8Array`. Without content\n * type, JSON is attempted and the text is returned when it is not valid JSON.\n */\nexport const jsonCodec: MessageCodec = Object.freeze({\n encode(payload: unknown, contentType: string | undefined): EncodedBody {\n const type = mediaType(contentType)\n if (payload instanceof Uint8Array) return { body: payload, contentType: contentType ?? BINARY_CONTENT_TYPE }\n if (typeof payload === 'string' && type !== undefined && !isJson(type))\n return { body: payload, contentType: contentType as string }\n if (payload === undefined) return { body: '', contentType: contentType ?? JSON_CONTENT_TYPE }\n const json = JSON.stringify(payload)\n if (json === undefined) throw new TypeError(`[znight] Cannot encode a ${typeof payload} payload as JSON`)\n return { body: json, contentType: contentType ?? JSON_CONTENT_TYPE }\n },\n decode(body: Uint8Array | string, contentType: string | undefined): unknown {\n const type = mediaType(contentType)\n if (type === undefined) {\n const text = bodyToText(body)\n if (text === '') return undefined\n try {\n return JSON.parse(text)\n } catch {\n return text\n }\n }\n if (isJson(type)) {\n const text = bodyToText(body)\n if (text.trim() === '') return undefined\n try {\n return JSON.parse(text)\n } catch (error) {\n throw new MessageDecodeError(`Malformed JSON body: ${(error as Error).message}`, { cause: error })\n }\n }\n if (type.startsWith('text/')) return bodyToText(body)\n return bodyToBytes(body)\n },\n})\n"],"mappings":";;AAyBA,MAAa,oBAAoB;AACjC,MAAa,oBAAoB;AACjC,MAAa,sBAAsB;AAEnC,MAAM,UAAU,IAAI,YAAY;AAChC,MAAM,UAAU,IAAI,YAAY,OAAO;;AAGvC,SAAgB,WAAW,MAAmC;CAC5D,OAAO,OAAO,SAAS,WAAW,OAAO,QAAQ,OAAO,IAAI;AAC9D;;AAGA,SAAgB,YAAY,MAAuC;CACjE,OAAO,OAAO,SAAS,WAAW,QAAQ,OAAO,IAAI,IAAI;AAC3D;;AAGA,SAAgB,UAAU,aAAqD;CAC7E,IAAI,gBAAgB,KAAA,GAAW,OAAO,KAAA;CACtC,MAAM,QAAQ,YAAY,MAAM,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,YAAY;CAC/D,OAAO,UAAU,KAAK,KAAA,IAAY;AACpC;AAEA,SAAS,OAAO,MAAuB;CACrC,OAAO,SAAA,sBAA8B,KAAK,SAAS,OAAO;AAC5D;;;;;;;;;;;AAYA,MAAa,YAA0B,OAAO,OAAO;CACnD,OAAO,SAAkB,aAA8C;EACrE,MAAM,OAAO,UAAU,WAAW;EAClC,IAAI,mBAAmB,YAAY,OAAO;GAAE,MAAM;GAAS,aAAa,eAAA;EAAmC;EAC3G,IAAI,OAAO,YAAY,YAAY,SAAS,KAAA,KAAa,CAAC,OAAO,IAAI,GACnE,OAAO;GAAE,MAAM;GAAsB;EAAsB;EAC7D,IAAI,YAAY,KAAA,GAAW,OAAO;GAAE,MAAM;GAAI,aAAa,eAAA;EAAiC;EAC5F,MAAM,OAAO,KAAK,UAAU,OAAO;EACnC,IAAI,SAAS,KAAA,GAAW,MAAM,IAAI,UAAU,4BAA4B,OAAO,QAAQ,iBAAiB;EACxG,OAAO;GAAE,MAAM;GAAM,aAAa,eAAA;EAAiC;CACrE;CACA,OAAO,MAA2B,aAA0C;EAC1E,MAAM,OAAO,UAAU,WAAW;EAClC,IAAI,SAAS,KAAA,GAAW;GACtB,MAAM,OAAO,WAAW,IAAI;GAC5B,IAAI,SAAS,IAAI,OAAO,KAAA;GACxB,IAAI;IACF,OAAO,KAAK,MAAM,IAAI;GACxB,QAAQ;IACN,OAAO;GACT;EACF;EACA,IAAI,OAAO,IAAI,GAAG;GAChB,MAAM,OAAO,WAAW,IAAI;GAC5B,IAAI,KAAK,KAAK,MAAM,IAAI,OAAO,KAAA;GAC/B,IAAI;IACF,OAAO,KAAK,MAAM,IAAI;GACxB,SAAS,OAAO;IACd,MAAM,IAAI,mBAAmB,wBAAyB,MAAgB,WAAW,EAAE,OAAO,MAAM,CAAC;GACnG;EACF;EACA,IAAI,KAAK,WAAW,OAAO,GAAG,OAAO,WAAW,IAAI;EACpD,OAAO,YAAY,IAAI;CACzB;AACF,CAAC"}
@@ -0,0 +1,75 @@
1
+ import { Token } from "@znight/core";
2
+ import { CompilerExtension, Location, StaticValue, ValueRef } from "@znight/compiler";
3
+ /** A normalized `@Retry` policy (`MessageHandlerDefinition.retry`). */
4
+ interface RetryPolicyData {
5
+ readonly attempts: number;
6
+ readonly backoff: "fixed" | "exponential";
7
+ readonly delay: number;
8
+ readonly maxDelay?: number;
9
+ }
10
+ /** How the invoker computes one argument of a handler. */
11
+ type HandlerParamData = {
12
+ readonly kind: "payload";
13
+ } | {
14
+ readonly kind: "validated";
15
+ readonly schema: ValueRef;
16
+ } | {
17
+ readonly kind: "headers";
18
+ } | {
19
+ readonly kind: "header";
20
+ readonly name: string;
21
+ } | {
22
+ readonly kind: "message";
23
+ readonly narrow: boolean;
24
+ } | {
25
+ readonly kind: "undefined";
26
+ };
27
+ /** One `@OnMessage` decorator of a method. */
28
+ interface HandlerData {
29
+ readonly method: string;
30
+ /** `@OnMessage(type)`; `undefined` for the default handler. */
31
+ readonly type: string | undefined;
32
+ readonly retry: RetryPolicyData | undefined;
33
+ readonly params: readonly HandlerParamData[];
34
+ /** Display name of the class declaring the method (messages). */
35
+ readonly declaredBy: string;
36
+ /** Location of the `@OnMessage` decorator. */
37
+ readonly location: Location;
38
+ }
39
+ /** A `@Consumer` class. */
40
+ interface ConsumerData {
41
+ readonly kind: "consumer";
42
+ /** Provider token of the consumer. */
43
+ readonly token: Token;
44
+ readonly identity: string;
45
+ readonly displayName: string;
46
+ /** Empty when the queue is invalid (a diagnostic was reported). */
47
+ readonly queue: string;
48
+ /** `@Consumer` options without `queue` and `scope` (`undefined` when empty). */
49
+ readonly options: Readonly<Record<string, StaticValue>> | undefined;
50
+ /** Handlers of the class and its analysable base classes, in declaration order (bases first). */
51
+ readonly handlers: readonly HandlerData[];
52
+ readonly errorHandler: string | undefined;
53
+ /** Identities of the class and of its analysable base classes (nearest first). */
54
+ readonly chain: readonly string[];
55
+ /** Location of the `@Consumer` decorator. */
56
+ readonly location: Location;
57
+ }
58
+ /** A class carrying messaging member decorators without `@Consumer` (a base class, or a mistake). */
59
+ interface MemberClassData {
60
+ readonly kind: "members";
61
+ readonly identity: string;
62
+ readonly displayName: string;
63
+ readonly abstract: boolean;
64
+ /** Method decorators (`@OnMessage`, `@OnError`, `@Retry`), in source order. */
65
+ readonly decorators: readonly {
66
+ readonly name: string;
67
+ readonly location: Location;
68
+ }[];
69
+ }
70
+ /** Everything the extension stores. */
71
+ type MessagingData = ConsumerData | MemberClassData;
72
+ /** The `@znight/messaging` compiler extension. */
73
+ export declare const messagingCompilerExtension: CompilerExtension<MessagingData>;
74
+ export { type ConsumerData, type HandlerData, type HandlerParamData, type MemberClassData, type MessagingData, type RetryPolicyData, messagingCompilerExtension as default };
75
+ //# sourceMappingURL=compiler.d.mts.map