@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,260 @@
1
+ /**
2
+ * Preparation and validation of the consumers declared in the metadata (boot time, ZNIGHT011).
3
+ */
4
+ import type { Container, Token } from '@znight/core'
5
+ import type { MessagingDriver } from './driver.ts'
6
+ import type { ConsumerDefinition, MessageHandlerDefinition, MessagingMetadata } from './metadata.ts'
7
+ import type { RetryOptions, RetryPolicy } from './retry.ts'
8
+
9
+ import { displayName } from '@znight/core'
10
+
11
+ import { consumerError } from './errors.ts'
12
+ import { isPositiveInteger } from './internal/async.ts'
13
+ import {
14
+ assertRetrySupported,
15
+ DEFAULT_RETRY_POLICY,
16
+ isInProcessDelay,
17
+ NO_RETRY_POLICY,
18
+ normalizeRetry,
19
+ retryDelays,
20
+ } from './retry.ts'
21
+
22
+ /** A handler ready to be dispatched to. */
23
+ export interface PreparedHandler {
24
+ readonly definition: MessageHandlerDefinition
25
+ readonly method: string
26
+ readonly policy: RetryPolicy
27
+ }
28
+
29
+ /** A validated consumer. */
30
+ export interface PreparedConsumer {
31
+ readonly definition: ConsumerDefinition
32
+ readonly token: Token
33
+ /** Display name of the consumer class. */
34
+ readonly name: string
35
+ readonly queue: string
36
+ readonly concurrency: number
37
+ readonly deadLetterQueue: string | undefined
38
+ readonly errorHandler: string | undefined
39
+ readonly maxAttempts: number
40
+ /** Native delays the runtime may request (sorted, unique). */
41
+ readonly retryDelays: readonly number[]
42
+ readonly driverOptions: Readonly<Record<string, unknown>>
43
+ /** Selects the handler of a message type (exact, then patterns, then the default handler). */
44
+ select(type: string | undefined): PreparedHandler | undefined
45
+ }
46
+
47
+ /** Plugin-level defaults applied to every consumer. */
48
+ export interface ConsumerDefaults {
49
+ readonly retry: RetryPolicy
50
+ readonly concurrency: number
51
+ readonly deadLetter: ((queue: string) => string) | false | undefined
52
+ }
53
+
54
+ /** Normalizes `defaults.retry` of the plugin. */
55
+ export function defaultRetryPolicy(retry: RetryOptions | false | undefined): RetryPolicy {
56
+ if (retry === undefined) return DEFAULT_RETRY_POLICY
57
+ if (retry === false) return NO_RETRY_POLICY
58
+ return normalizeRetry(retry, 'MessagingPlugin defaults.retry')
59
+ }
60
+
61
+ /**
62
+ * Validates `metadata.extensions.messaging` against the container and the driver capabilities.
63
+ *
64
+ * @throws ZnightError ZNIGHT011 describing the first invalid consumer.
65
+ */
66
+ export function prepareConsumers(
67
+ metadata: MessagingMetadata | undefined,
68
+ container: Container,
69
+ driver: MessagingDriver,
70
+ defaults: ConsumerDefaults,
71
+ ): PreparedConsumer[] {
72
+ if (metadata === undefined) return []
73
+ if (metadata === null || typeof metadata !== 'object' || !Array.isArray(metadata.consumers)) {
74
+ throw consumerError('Invalid messaging metadata: "consumers" must be an array', [
75
+ 'Regenerate the metadata with the @znight/messaging compiler extension',
76
+ ])
77
+ }
78
+ const byQueue = new Map<string, string>()
79
+ const prepared: PreparedConsumer[] = []
80
+ for (const definition of metadata.consumers) {
81
+ const consumer = prepareConsumer(definition, container, driver, defaults)
82
+ const previous = byQueue.get(consumer.queue)
83
+ if (previous !== undefined) {
84
+ throw consumerError(`${previous} and ${consumer.name} both consume "${consumer.queue}"`, [
85
+ 'A queue is consumed by a single @Consumer class: merge the handlers, or route by type with @OnMessage(type)',
86
+ ])
87
+ }
88
+ byQueue.set(consumer.queue, consumer.name)
89
+ prepared.push(consumer)
90
+ }
91
+ return prepared
92
+ }
93
+
94
+ function prepareConsumer(
95
+ definition: ConsumerDefinition,
96
+ container: Container,
97
+ driver: MessagingDriver,
98
+ defaults: ConsumerDefaults,
99
+ ): PreparedConsumer {
100
+ if (
101
+ definition === null ||
102
+ typeof definition !== 'object' ||
103
+ typeof definition.token !== 'string' ||
104
+ definition.token === ''
105
+ ) {
106
+ throw consumerError('Invalid consumer definition: missing provider token')
107
+ }
108
+ const token = definition.token
109
+ const name = displayName(token)
110
+ if (typeof definition.queue !== 'string' || definition.queue === '') {
111
+ throw consumerError(`@Consumer ${name}: the queue must be a non-empty string`)
112
+ }
113
+ if (!container.has(token)) {
114
+ throw consumerError(`@Consumer ${name}: no provider is registered for ${token}`, [
115
+ 'Regenerate the metadata: the consumer class must be a compiled provider',
116
+ ])
117
+ }
118
+ const options = definition.options ?? {}
119
+ const { concurrency: rawConcurrency, deadLetter, ...driverOptions } = options
120
+ const concurrency = rawConcurrency ?? defaults.concurrency
121
+ if (!isPositiveInteger(concurrency)) {
122
+ throw consumerError(`@Consumer ${name}: "concurrency" must be a positive integer, got ${String(concurrency)}`)
123
+ }
124
+ if (deadLetter !== undefined && deadLetter !== false && (typeof deadLetter !== 'string' || deadLetter === '')) {
125
+ throw consumerError(`@Consumer ${name}: "deadLetter" must be a queue name or false, got ${String(deadLetter)}`)
126
+ }
127
+ if (deadLetter === definition.queue) {
128
+ throw consumerError(`@Consumer ${name}: the dead-letter queue cannot be the consumed queue "${definition.queue}"`)
129
+ }
130
+
131
+ const handlers = definition.handlers
132
+ if (!Array.isArray(handlers) || handlers.length === 0) {
133
+ throw consumerError(`@Consumer ${name} has no @OnMessage handler`, [
134
+ 'Decorate at least one method with @OnMessage()',
135
+ ])
136
+ }
137
+ const exact = new Map<string, PreparedHandler>()
138
+ const patterns: { readonly regex: RegExp; readonly handler: PreparedHandler }[] = []
139
+ let fallback: PreparedHandler | undefined
140
+ let maxAttempts = 1
141
+ const nativeDelays = new Set<number>()
142
+ for (const handlerDefinition of handlers) {
143
+ const handler = prepareHandler(handlerDefinition, name, driver, defaults.retry)
144
+ maxAttempts = Math.max(maxAttempts, handler.policy.attempts)
145
+ for (const delay of retryDelays(handler.policy)) {
146
+ if (!isInProcessDelay(delay, driver.capabilities)) nativeDelays.add(delay)
147
+ }
148
+ const type = handlerDefinition.type
149
+ if (type === undefined) {
150
+ if (fallback !== undefined) {
151
+ throw consumerError(
152
+ `@Consumer ${name}: ${fallback.method}() and ${handler.method}() are both default @OnMessage() handlers`,
153
+ ['Give one of them a message type: @OnMessage("order.created")'],
154
+ )
155
+ }
156
+ fallback = handler
157
+ } else if (type.includes('*')) {
158
+ if (patterns.some(entry => entry.handler.definition.type === type)) {
159
+ throw duplicateType(name, type)
160
+ }
161
+ patterns.push({ regex: patternToRegExp(type), handler })
162
+ } else {
163
+ if (exact.has(type)) throw duplicateType(name, type)
164
+ exact.set(type, handler)
165
+ }
166
+ }
167
+
168
+ const errorHandler = definition.errorHandler
169
+ if (errorHandler !== undefined) {
170
+ if (typeof errorHandler !== 'string' || errorHandler === '') {
171
+ throw consumerError(`@Consumer ${name}: invalid @OnError method name ${String(errorHandler)}`)
172
+ }
173
+ const providers = container.metadata.providers
174
+ const provider = Object.hasOwn(providers, token) ? providers[token] : undefined
175
+ const prototype = provider?.class?.prototype as Record<string, unknown> | undefined
176
+ if (prototype !== undefined && typeof prototype[errorHandler] !== 'function') {
177
+ throw consumerError(`@Consumer ${name}: the @OnError method ${errorHandler}() does not exist`)
178
+ }
179
+ }
180
+
181
+ return {
182
+ definition,
183
+ token,
184
+ name,
185
+ queue: definition.queue,
186
+ concurrency,
187
+ deadLetterQueue: resolveDeadLetter(definition.queue, deadLetter, defaults.deadLetter, driver),
188
+ errorHandler,
189
+ maxAttempts,
190
+ retryDelays: [...nativeDelays].sort((a, b) => a - b),
191
+ driverOptions,
192
+ select(type: string | undefined): PreparedHandler | undefined {
193
+ if (type === undefined) return fallback
194
+ const handler = exact.get(type)
195
+ if (handler !== undefined) return handler
196
+ for (const entry of patterns) if (entry.regex.test(type)) return entry.handler
197
+ return fallback
198
+ },
199
+ }
200
+ }
201
+
202
+ function prepareHandler(
203
+ definition: MessageHandlerDefinition,
204
+ consumer: string,
205
+ driver: MessagingDriver,
206
+ defaultPolicy: RetryPolicy,
207
+ ): PreparedHandler {
208
+ if (
209
+ definition === null ||
210
+ typeof definition !== 'object' ||
211
+ typeof definition.method !== 'string' ||
212
+ definition.method === ''
213
+ ) {
214
+ throw consumerError(`@Consumer ${consumer}: invalid @OnMessage handler definition`)
215
+ }
216
+ const where = `${consumer}.${definition.method}()`
217
+ if (typeof definition.invoke !== 'function') {
218
+ throw consumerError(`${where}: the handler has no invoker`, ['Regenerate the metadata'])
219
+ }
220
+ if (definition.type !== undefined && (typeof definition.type !== 'string' || definition.type === '')) {
221
+ throw consumerError(`${where}: the @OnMessage type must be a non-empty string`)
222
+ }
223
+ const policy = definition.retry === undefined ? defaultPolicy : normalizeRetry(definition.retry, `@Retry on ${where}`)
224
+ assertRetrySupported(
225
+ policy,
226
+ driver.capabilities,
227
+ driver.name,
228
+ definition.retry === undefined ? `${where} (default retry policy)` : `@Retry on ${where}`,
229
+ )
230
+ return { definition, method: definition.method, policy }
231
+ }
232
+
233
+ function duplicateType(consumer: string, type: string): Error {
234
+ return consumerError(`@Consumer ${consumer}: several @OnMessage handlers for type "${type}"`)
235
+ }
236
+
237
+ /** `order.*` → `/^order\..*$/`: `*` matches any sequence of characters. */
238
+ function patternToRegExp(pattern: string): RegExp {
239
+ const source = pattern
240
+ .split('*')
241
+ .map(part => part.replace(/[.+?^${}()|[\]\\]/g, '\\$&'))
242
+ .join('.*')
243
+ return new RegExp(`^${source}$`, 's')
244
+ }
245
+
246
+ function resolveDeadLetter(
247
+ queue: string,
248
+ option: string | false | undefined,
249
+ defaults: ((queue: string) => string) | false | undefined,
250
+ driver: MessagingDriver,
251
+ ): string | undefined {
252
+ if (option === false) return undefined
253
+ if (typeof option === 'string') return option
254
+ if (defaults === false) return undefined
255
+ const name = defaults === undefined ? driver.deadLetterName(queue) : defaults(queue)
256
+ if (typeof name !== 'string' || name === '' || name === queue) {
257
+ throw consumerError(`Invalid dead-letter queue name for "${queue}": ${String(name)}`)
258
+ }
259
+ return name
260
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Marker decorators of `@znight/messaging`. Like every Znight decorator they do nothing at runtime:
3
+ * the compiler extension (`@znight/messaging/compiler`) reads them from the source code and generates
4
+ * `metadata.extensions.messaging`. Arguments marked _static_ must be statically evaluable (literals,
5
+ * `const` references); _value_ arguments (schemas) may also be references to exported bindings.
6
+ */
7
+ import type { ClassMarker, MethodMarker, ParameterMarker, Scope } from '@znight/core'
8
+ import type { InboundMessage } from './message.ts'
9
+ import type { RetryOptions } from './retry.ts'
10
+ import type { StandardSchemaV1 } from './standard-schema.ts'
11
+
12
+ /** Options of {@link Consumer} (static). */
13
+ export interface ConsumerOptions {
14
+ /** Queue (RabbitMQ, SQS) or topic (Kafka) to consume. */
15
+ readonly queue: string
16
+ /** Scope of the consumer provider. Default: `singleton`; `request` creates one instance per message. */
17
+ readonly scope?: Scope
18
+ /** Maximum number of messages handled concurrently. Default: plugin `defaults.concurrency` (1). */
19
+ readonly concurrency?: number
20
+ /** Dead-letter queue/topic, or `false` to disable dead-lettering. Default: driver naming (`<queue>.dlq`). */
21
+ readonly deadLetter?: string | false
22
+ /** Driver-specific options (e.g. Kafka `groupId`). */
23
+ readonly [driverOption: string]: unknown
24
+ }
25
+
26
+ /** Result of an `@OnError` method: `'ack'` acknowledges (swallows) the message, anything else dead-letters it. */
27
+ // biome-ignore lint/suspicious/noConfusingVoidType: lets error handlers without return statement type-check
28
+ export type ErrorHandlerResult = 'ack' | undefined | void
29
+
30
+ /**
31
+ * Signature of an `@OnError` method. Declared as a method signature so that parameters may be
32
+ * narrowed (`onError(error: Error, message: InboundMessage<Order>)`).
33
+ */
34
+ export type ErrorHandlerMethod = ErrorHandlerSignature['onError']
35
+
36
+ interface ErrorHandlerSignature {
37
+ onError(error: unknown, message: InboundMessage): ErrorHandlerResult | Promise<ErrorHandlerResult>
38
+ }
39
+
40
+ /** Marker usable on an `@OnError` method: checks its signature. */
41
+ export type ErrorHandlerMarker = <F extends ErrorHandlerMethod>(
42
+ target: object,
43
+ propertyKeyOrContext: string | symbol | ClassMethodDecoratorContext<unknown, F>,
44
+ descriptor?: TypedPropertyDescriptor<F>,
45
+ ) => void
46
+
47
+ function marker(): void {
48
+ // intentionally empty: decorators are compile-time markers
49
+ }
50
+
51
+ const classMarker: ClassMarker = marker
52
+ const methodMarker: MethodMarker = marker
53
+ const parameterMarker: ParameterMarker = marker
54
+ const errorHandlerMarker: ErrorHandlerMarker = marker
55
+
56
+ /**
57
+ * Declares a message consumer: a provider (singleton by default) whose `@OnMessage` methods handle
58
+ * the messages of `queue`. Arguments are static.
59
+ *
60
+ * ```ts
61
+ * @Consumer('orders')
62
+ * export class OrderConsumer {
63
+ * constructor(private readonly orders: OrderService) {}
64
+ *
65
+ * @OnMessage('order.created')
66
+ * @Retry({ attempts: 5, backoff: 'exponential', delay: 500 })
67
+ * async created(@Message(OrderCreated) event: OrderCreated) {
68
+ * await this.orders.process(event)
69
+ * }
70
+ * }
71
+ * ```
72
+ */
73
+ export function Consumer(queue: string): ClassMarker
74
+ export function Consumer(options: ConsumerOptions): ClassMarker
75
+ export function Consumer(_queueOrOptions: string | ConsumerOptions): ClassMarker {
76
+ return classMarker
77
+ }
78
+
79
+ /**
80
+ * Declares a message handler. With `type` (static), it handles the messages of that type (exact
81
+ * match, or a pattern where `*` matches any sequence: `'order.*'`); without type it is the consumer's
82
+ * default handler (messages without type, or whose type no other handler matches). A message no
83
+ * handler matches is dead-lettered (reason `no-handler`).
84
+ */
85
+ export function OnMessage(_type?: string): MethodMarker {
86
+ return methodMarker
87
+ }
88
+
89
+ /**
90
+ * Retry policy of a handler (static options). `attempts` counts the first delivery:
91
+ * `{ attempts: 3 }` means one delivery plus two retries. Without `@Retry`, the plugin's
92
+ * `defaults.retry` applies (3 attempts, exponential from 1 s).
93
+ */
94
+ export function Retry(_options: RetryOptions): MethodMarker {
95
+ return methodMarker
96
+ }
97
+
98
+ /**
99
+ * Declares the consumer's error handler, called once a message will not be retried anymore (last
100
+ * attempt failed, `NonRetryableError`, invalid payload, no matching handler). Returning `'ack'`
101
+ * acknowledges the message; returning anything else (or throwing) dead-letters it.
102
+ *
103
+ * ```ts
104
+ * @OnError()
105
+ * onError(error: unknown, message: InboundMessage) {
106
+ * if (message.type?.startsWith('legacy.')) return 'ack'
107
+ * }
108
+ * ```
109
+ */
110
+ export function OnError(): ErrorHandlerMarker {
111
+ return errorHandlerMarker
112
+ }
113
+
114
+ /**
115
+ * Injects the message payload. With a Standard Schema (Zod, Valibot...), the payload is validated and
116
+ * the parameter receives the schema output; an invalid payload is dead-lettered without retry
117
+ * (reason `validation`). The schema is a _value_ argument (inline, or an exported binding).
118
+ */
119
+ export function Message(_schema?: StandardSchemaV1): ParameterMarker {
120
+ return parameterMarker
121
+ }
122
+
123
+ /** Injects all the message headers, or the value of one header (`string | undefined`). `name` is static. */
124
+ export function Headers(_name?: string): ParameterMarker {
125
+ return parameterMarker
126
+ }
127
+
128
+ /** Injects the {@link InboundMessage} (id, type, attempt, headers, raw message, abort signal...). */
129
+ export function Ctx(): ParameterMarker {
130
+ return parameterMarker
131
+ }
package/src/driver.ts ADDED
@@ -0,0 +1,142 @@
1
+ import type { CoreLogger } from '@znight/core'
2
+ import type { DeadLetterReason, OutboundMessage } from './message.ts'
3
+
4
+ /**
5
+ * What a driver can do. `MessagingPlugin` validates every `@Retry` policy against it at boot: a
6
+ * delay is either waited in-process (`<= maxInProcessDelayMs`) or delegated to the broker
7
+ * (`nativeDelay`, `<= maxNativeDelayMs`); otherwise the application fails to start (ZNIGHT011).
8
+ */
9
+ export interface DriverCapabilities {
10
+ /** The broker can redeliver a message after a delay (SQS visibility, RabbitMQ TTL queue, Kafka retry topic). */
11
+ readonly nativeDelay: boolean
12
+ /** Longest native redelivery delay, in milliseconds (`0` without native delay). */
13
+ readonly maxNativeDelayMs: number
14
+ /**
15
+ * Longest delay the runtime may wait in-process while holding a delivery before asking the
16
+ * broker to redeliver it (the message stays locked: prefetch slot, SQS lease...).
17
+ */
18
+ readonly maxInProcessDelayMs: number
19
+ /**
20
+ * How attempts are counted: `native` (broker delivery count: SQS `ApproximateReceiveCount`,
21
+ * RabbitMQ quorum `x-delivery-count`) or `header` (`x-znight-attempt` on republished retries).
22
+ */
23
+ readonly deliveryCount: 'native' | 'header'
24
+ /** Longest `publish(..., { delay })`, in milliseconds (`0`: delayed publishing unsupported). */
25
+ readonly maxPublishDelayMs: number
26
+ }
27
+
28
+ /** Services a driver receives when it connects. */
29
+ export interface DriverContext {
30
+ /** Logger for background failures (polling errors, connection loss...). */
31
+ readonly logger: CoreLogger
32
+ }
33
+
34
+ /** Redelivery request of a failed delivery. */
35
+ export interface RetryRequest {
36
+ /** Attempt number of the redelivery (failed attempt + 1). */
37
+ readonly attempt: number
38
+ /** Delay before the redelivery, in milliseconds (`0`: as soon as possible). */
39
+ readonly delayMs: number
40
+ /** Error of the failed attempt. */
41
+ readonly error: unknown
42
+ }
43
+
44
+ /** Dead-letter request of a delivery. */
45
+ export interface DeadLetterRequest {
46
+ /**
47
+ * Destination queue/topic, `undefined` when dead-lettering is disabled for the consumer: the driver
48
+ * then rejects the message natively (RabbitMQ `nack` without requeue, so a queue DLX applies) or
49
+ * discards it.
50
+ */
51
+ readonly queue: string | undefined
52
+ readonly reason: DeadLetterReason
53
+ readonly error: unknown
54
+ /** Attempts made. */
55
+ readonly attempt: number
56
+ }
57
+
58
+ /**
59
+ * A message received by a driver, with the operations settling it. The runtime calls exactly one
60
+ * settlement operation (`ack`, `retry`, `deadLetter` or `release`), possibly followed by `release`
61
+ * when the first one failed. Each operation resolves once the broker acknowledged it.
62
+ */
63
+ export interface DriverDelivery {
64
+ readonly id: string
65
+ readonly body: Uint8Array | string
66
+ readonly contentType: string | undefined
67
+ /** Headers as strings (non-string values converted). */
68
+ readonly headers: Readonly<Record<string, string>>
69
+ readonly type: string | undefined
70
+ readonly key: string | undefined
71
+ /** 1-based attempt of this delivery. */
72
+ readonly attempt: number
73
+ readonly redelivered: boolean
74
+ /** Production time (epoch ms), when known. */
75
+ readonly timestamp: number | undefined
76
+ /** Driver-native message. */
77
+ readonly raw: unknown
78
+ /** The message was handled: remove it from the queue / commit it. */
79
+ ack(): Promise<void>
80
+ /**
81
+ * Redeliver the message as attempt `request.attempt` after `request.delayMs`. The redelivery must be
82
+ * durable (confirmed) before the original is acknowledged.
83
+ */
84
+ retry(request: RetryRequest): Promise<void>
85
+ /** Move the message to the dead-letter destination (publish with confirmation, then acknowledge). */
86
+ deadLetter(request: DeadLetterRequest): Promise<void>
87
+ /**
88
+ * Give the message back to the broker for a later redelivery without settling it (shutdown,
89
+ * failed dead-lettering). Must never lose the message.
90
+ */
91
+ release(): Promise<void>
92
+ }
93
+
94
+ /**
95
+ * Handles one delivery. The returned promise settles once the delivery was settled: drivers use it
96
+ * for back-pressure (never more than `concurrency` pending promises per subscription).
97
+ */
98
+ export type DeliveryHandler = (delivery: DriverDelivery) => Promise<void>
99
+
100
+ /** Options of {@link MessagingDriver.subscribe}. */
101
+ export interface SubscribeOptions {
102
+ /** Display name of the consumer class (logs, errors). */
103
+ readonly consumer: string
104
+ /** Maximum number of deliveries handled concurrently (prefetch, receive batch size...). */
105
+ readonly concurrency: number
106
+ /** Dead-letter destination, `undefined` when disabled. Drivers verify it exists at subscribe time. */
107
+ readonly deadLetterQueue: string | undefined
108
+ /** Largest number of attempts among the consumer's handlers (e.g. checked against SQS `maxReceiveCount`). */
109
+ readonly maxAttempts: number
110
+ /** Native retry delays (ms) the runtime may request: lets drivers prepare delay queues. Sorted, unique. */
111
+ readonly retryDelays: readonly number[]
112
+ /** Driver-specific options of `@Consumer({ queue, ...options })`. */
113
+ readonly options: Readonly<Record<string, unknown>>
114
+ }
115
+
116
+ /** An active subscription. */
117
+ export interface Subscription {
118
+ /** Stops receiving new deliveries. Deliveries already handed to the runtime can still be settled. */
119
+ cancel(): Promise<void>
120
+ /** Releases the subscription's resources, once in-flight deliveries settled. Idempotent. */
121
+ close(): Promise<void>
122
+ }
123
+
124
+ /**
125
+ * A broker integration. Drivers live in subpath exports (`@znight/messaging/rabbitmq`, `/sqs`,
126
+ * `/kafka`, `/memory`) so their client libraries stay optional peers.
127
+ */
128
+ export interface MessagingDriver {
129
+ /** Short name (`rabbitmq`, `sqs`, `kafka`, `memory`), exposed as `InboundMessage.driver`. */
130
+ readonly name: string
131
+ readonly capabilities: DriverCapabilities
132
+ /** Connects to the broker (called in `onBootstrap`). */
133
+ connect(context: DriverContext): Promise<void>
134
+ /** Starts consuming `queue`. Verifies the destination and its dead-letter configuration. */
135
+ subscribe(queue: string, handler: DeliveryHandler, options: SubscribeOptions): Promise<Subscription>
136
+ /** Publishes a message; resolves once the broker confirmed it. */
137
+ publish(queue: string, message: OutboundMessage): Promise<void>
138
+ /** Closes every subscription and the connection. Idempotent. */
139
+ close(): Promise<void>
140
+ /** Default dead-letter destination of `queue` (`orders.dlq`, `orders-dlq`...). */
141
+ deadLetterName(queue: string): string
142
+ }