@znight/messaging 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +622 -2
- package/dist/async-CFh_bvex.mjs +216 -0
- package/dist/async-CFh_bvex.mjs.map +1 -0
- package/dist/codec-Dku-NXXU.mjs +83 -0
- package/dist/codec-Dku-NXXU.mjs.map +1 -0
- package/dist/compiler.d.mts +75 -0
- package/dist/compiler.mjs +1023 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/driver-ZxLqy_Ap.d.mts +216 -0
- package/dist/index.d.mts +459 -0
- package/dist/index.mjs +870 -0
- package/dist/index.mjs.map +1 -0
- package/dist/kafka.d.mts +51 -0
- package/dist/kafka.mjs +517 -0
- package/dist/kafka.mjs.map +1 -0
- package/dist/memory.d.mts +124 -0
- package/dist/memory.mjs +429 -0
- package/dist/memory.mjs.map +1 -0
- package/dist/rabbitmq.d.mts +65 -0
- package/dist/rabbitmq.mjs +686 -0
- package/dist/rabbitmq.mjs.map +1 -0
- package/dist/shared-BJBpLS8V.mjs +52 -0
- package/dist/shared-BJBpLS8V.mjs.map +1 -0
- package/dist/sqs.d.mts +38 -0
- package/dist/sqs.mjs +421 -0
- package/dist/sqs.mjs.map +1 -0
- package/package.json +123 -4
- package/src/broker.ts +156 -0
- package/src/codec.ts +98 -0
- package/src/compiler/analyze.ts +800 -0
- package/src/compiler/constants.ts +73 -0
- package/src/compiler/data.ts +75 -0
- package/src/compiler/generate.ts +131 -0
- package/src/compiler/index.ts +52 -0
- package/src/compiler/patterns.ts +54 -0
- package/src/compiler/retry.ts +136 -0
- package/src/compiler/validate.ts +57 -0
- package/src/compiler/values.ts +76 -0
- package/src/consumers.ts +260 -0
- package/src/decorators.ts +131 -0
- package/src/driver.ts +142 -0
- package/src/drivers/kafka.ts +765 -0
- package/src/drivers/memory.ts +609 -0
- package/src/drivers/rabbitmq.ts +944 -0
- package/src/drivers/shared.ts +81 -0
- package/src/drivers/sqs.ts +583 -0
- package/src/errors.ts +95 -0
- package/src/headers.ts +70 -0
- package/src/hooks.ts +2 -0
- package/src/index.ts +89 -0
- package/src/internal/async.ts +115 -0
- package/src/message.ts +96 -0
- package/src/metadata.ts +65 -0
- package/src/plugin.ts +236 -0
- package/src/retry.ts +129 -0
- package/src/runtime.ts +424 -0
- package/src/standard-schema.ts +68 -0
- package/src/tokens.ts +14 -0
- package/src/validation.ts +61 -0
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { Plugin, PluginContext, PluginOptions } from '@znight/core'
|
|
2
|
+
import type { MessageCodec } from './codec.ts'
|
|
3
|
+
import type { MessagingDriver } from './driver.ts'
|
|
4
|
+
import type { RetryOptions } from './retry.ts'
|
|
5
|
+
|
|
6
|
+
import { resolveOptions } from '@znight/core'
|
|
7
|
+
|
|
8
|
+
import { DriverMessageBroker, type MessageBroker } from './broker.ts'
|
|
9
|
+
import { jsonCodec } from './codec.ts'
|
|
10
|
+
import { type ConsumerDefaults, defaultRetryPolicy, prepareConsumers } from './consumers.ts'
|
|
11
|
+
import { consumerError } from './errors.ts'
|
|
12
|
+
import { isPositiveInteger } from './internal/async.ts'
|
|
13
|
+
import { ConsumerRuntime, type ConsumerState } from './runtime.ts'
|
|
14
|
+
import { MessagingTokens } from './tokens.ts'
|
|
15
|
+
|
|
16
|
+
/** Defaults applied to every consumer (each `@Consumer` / `@Retry` may override them). */
|
|
17
|
+
export interface MessagingDefaults {
|
|
18
|
+
/** Retry policy of handlers without `@Retry`. Default: 3 attempts, exponential from 1 s up to 30 s. `false`: no retry. */
|
|
19
|
+
readonly retry?: RetryOptions | false
|
|
20
|
+
/** Concurrency of consumers without `concurrency` option. Default: 1. */
|
|
21
|
+
readonly concurrency?: number
|
|
22
|
+
/**
|
|
23
|
+
* Dead-letter destination of a queue, or `false` to disable dead-lettering (messages are then
|
|
24
|
+
* rejected natively or discarded). Default: the driver naming (`orders.dlq`, SQS `orders-dlq`).
|
|
25
|
+
*/
|
|
26
|
+
readonly deadLetter?: ((queue: string) => string) | false
|
|
27
|
+
/**
|
|
28
|
+
* Pause, in milliseconds, before releasing a message whose settlement failed (dead-letter queue
|
|
29
|
+
* unavailable...), to avoid hot redelivery loops. Default: 1000.
|
|
30
|
+
*/
|
|
31
|
+
readonly requeueDelay?: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Options of {@link MessagingPlugin.configure}. */
|
|
35
|
+
export interface MessagingPluginOptions {
|
|
36
|
+
/** The broker driver: `rabbitmq()`, `sqs()`, `kafka()` or `memory()` (from the matching subpath). */
|
|
37
|
+
readonly driver: MessagingDriver
|
|
38
|
+
readonly defaults?: MessagingDefaults
|
|
39
|
+
/** Payload encoding. Default: {@link jsonCodec}. */
|
|
40
|
+
readonly codec?: MessageCodec
|
|
41
|
+
/** Subscribe the consumers on start. `false` for publish-only processes. Default: `true`. */
|
|
42
|
+
readonly consume?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Runtime status of a consumer. */
|
|
46
|
+
export interface ConsumerStatus {
|
|
47
|
+
/** Display name of the consumer class. */
|
|
48
|
+
readonly consumer: string
|
|
49
|
+
readonly queue: string
|
|
50
|
+
readonly state: ConsumerState
|
|
51
|
+
readonly concurrency: number
|
|
52
|
+
readonly inFlight: number
|
|
53
|
+
readonly deadLetterQueue: string | undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The plugin returned by {@link MessagingPlugin.configure}. */
|
|
57
|
+
export interface MessagingPluginInstance extends Plugin {
|
|
58
|
+
readonly name: 'messaging'
|
|
59
|
+
/** The broker (also injectable as `MessageBroker`). */
|
|
60
|
+
readonly broker: MessageBroker
|
|
61
|
+
/** The driver, once the plugin is bootstrapped. */
|
|
62
|
+
readonly driver: MessagingDriver | undefined
|
|
63
|
+
/** Status of every consumer, once the plugin is bootstrapped. */
|
|
64
|
+
consumers(): ConsumerStatus[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const DEFAULT_REQUEUE_DELAY_MS = 1000
|
|
68
|
+
/** Period of the keep-alive timer (it never does anything: it only keeps the event loop alive). */
|
|
69
|
+
const KEEP_ALIVE_INTERVAL_MS = 2 ** 30
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Message consumers and publishing.
|
|
73
|
+
*
|
|
74
|
+
* ```ts
|
|
75
|
+
* import { MessagingPlugin } from '@znight/messaging'
|
|
76
|
+
* import { rabbitmq } from '@znight/messaging/rabbitmq'
|
|
77
|
+
*
|
|
78
|
+
* const app = await Application.create({
|
|
79
|
+
* metadata: __ZNIGHT_METADATA__,
|
|
80
|
+
* plugins: [MessagingPlugin.configure({ driver: rabbitmq({ url: 'amqp://localhost' }) })],
|
|
81
|
+
* })
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* Options may be computed from the container (evaluated in `onBootstrap`):
|
|
85
|
+
* `MessagingPlugin.configure(c => ({ driver: rabbitmq({ url: c.get(AmqpConfig).url }) }))`.
|
|
86
|
+
*/
|
|
87
|
+
export const MessagingPlugin: {
|
|
88
|
+
configure(options: PluginOptions<MessagingPluginOptions>): MessagingPluginInstance
|
|
89
|
+
} = Object.freeze({
|
|
90
|
+
configure(options: PluginOptions<MessagingPluginOptions>): MessagingPluginInstance {
|
|
91
|
+
return createPlugin(options)
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
function createPlugin(rawOptions: PluginOptions<MessagingPluginOptions>): MessagingPluginInstance {
|
|
96
|
+
const broker = new DriverMessageBroker()
|
|
97
|
+
let driver: MessagingDriver | undefined
|
|
98
|
+
let connected = false
|
|
99
|
+
let consume = true
|
|
100
|
+
let runtimes: ConsumerRuntime[] = []
|
|
101
|
+
/**
|
|
102
|
+
* Keeps the process alive while consumers run: they are its ingress, and some drivers hold no
|
|
103
|
+
* handle of their own (the memory driver only uses microtasks and short timers), so a process
|
|
104
|
+
* without other handle would exit right after `app.start()`, without graceful shutdown.
|
|
105
|
+
*/
|
|
106
|
+
let keepAlive: ReturnType<typeof setInterval> | undefined
|
|
107
|
+
const stopKeepAlive = (): void => {
|
|
108
|
+
if (keepAlive !== undefined) clearInterval(keepAlive)
|
|
109
|
+
keepAlive = undefined
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
name: 'messaging',
|
|
114
|
+
broker,
|
|
115
|
+
get driver(): MessagingDriver | undefined {
|
|
116
|
+
return driver
|
|
117
|
+
},
|
|
118
|
+
consumers(): ConsumerStatus[] {
|
|
119
|
+
return runtimes.map(runtime => ({
|
|
120
|
+
consumer: runtime.consumer.name,
|
|
121
|
+
queue: runtime.consumer.queue,
|
|
122
|
+
state: runtime.state,
|
|
123
|
+
concurrency: runtime.consumer.concurrency,
|
|
124
|
+
inFlight: runtime.inFlight,
|
|
125
|
+
deadLetterQueue: runtime.consumer.deadLetterQueue,
|
|
126
|
+
}))
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
register(ctx: PluginContext): void {
|
|
130
|
+
ctx.container.registerExternal({
|
|
131
|
+
token: MessagingTokens.InboundMessage,
|
|
132
|
+
scope: 'request',
|
|
133
|
+
providedBy: 'MessagingPlugin',
|
|
134
|
+
})
|
|
135
|
+
ctx.container.registerValue(MessagingTokens.MessageBroker, broker)
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
async onBootstrap(ctx: PluginContext): Promise<void> {
|
|
139
|
+
const options = validateOptions(resolveOptions(rawOptions, ctx.container))
|
|
140
|
+
driver = options.driver
|
|
141
|
+
consume = options.consume ?? true
|
|
142
|
+
const defaults = options.defaults ?? {}
|
|
143
|
+
const consumerDefaults: ConsumerDefaults = {
|
|
144
|
+
retry: defaultRetryPolicy(defaults.retry),
|
|
145
|
+
concurrency: defaults.concurrency ?? 1,
|
|
146
|
+
deadLetter: defaults.deadLetter,
|
|
147
|
+
}
|
|
148
|
+
const prepared = prepareConsumers(ctx.metadata.extensions?.messaging, ctx.container, driver, consumerDefaults)
|
|
149
|
+
const codec = options.codec ?? jsonCodec
|
|
150
|
+
connected = true // close() must run even if connect() failed half-way
|
|
151
|
+
await driver.connect({ logger: ctx.logger })
|
|
152
|
+
broker.bind(driver, codec, ctx.hooks)
|
|
153
|
+
const services = {
|
|
154
|
+
container: ctx.container,
|
|
155
|
+
hooks: ctx.hooks,
|
|
156
|
+
logger: ctx.logger,
|
|
157
|
+
codec,
|
|
158
|
+
driver,
|
|
159
|
+
requeueDelayMs: defaults.requeueDelay ?? DEFAULT_REQUEUE_DELAY_MS,
|
|
160
|
+
}
|
|
161
|
+
runtimes = prepared.map(consumer => new ConsumerRuntime(consumer, services))
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
async onStart(ctx: PluginContext): Promise<void> {
|
|
165
|
+
if (!consume || runtimes.length === 0) return
|
|
166
|
+
const results = await Promise.allSettled(runtimes.map(runtime => runtime.start()))
|
|
167
|
+
const failure = results.find(result => result.status === 'rejected')
|
|
168
|
+
if (failure !== undefined) {
|
|
169
|
+
// the application only stops plugins whose onStart succeeded: undo the subscriptions made
|
|
170
|
+
await Promise.all(runtimes.map(runtime => runtime.stop()))
|
|
171
|
+
throw failure.reason
|
|
172
|
+
}
|
|
173
|
+
keepAlive ??= setInterval(() => undefined, KEEP_ALIVE_INTERVAL_MS)
|
|
174
|
+
ctx.logger.info(`Messaging: ${runtimes.length} consumer(s) subscribed with the ${driver?.name} driver`, {
|
|
175
|
+
queues: runtimes.map(runtime => runtime.consumer.queue),
|
|
176
|
+
})
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
async onStop(): Promise<void> {
|
|
180
|
+
try {
|
|
181
|
+
await Promise.all(runtimes.map(runtime => runtime.stop()))
|
|
182
|
+
} finally {
|
|
183
|
+
stopKeepAlive()
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
async onDispose(): Promise<void> {
|
|
188
|
+
stopKeepAlive()
|
|
189
|
+
broker.unbind()
|
|
190
|
+
if (driver !== undefined && connected) {
|
|
191
|
+
connected = false
|
|
192
|
+
await driver.close()
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function validateOptions(options: MessagingPluginOptions): MessagingPluginOptions {
|
|
199
|
+
if (options === null || typeof options !== 'object') {
|
|
200
|
+
throw consumerError('MessagingPlugin.configure(): options must be an object')
|
|
201
|
+
}
|
|
202
|
+
const driver = options.driver as Partial<MessagingDriver> | undefined
|
|
203
|
+
if (
|
|
204
|
+
driver === undefined ||
|
|
205
|
+
driver === null ||
|
|
206
|
+
typeof driver.connect !== 'function' ||
|
|
207
|
+
typeof driver.subscribe !== 'function' ||
|
|
208
|
+
typeof driver.publish !== 'function' ||
|
|
209
|
+
typeof driver.close !== 'function' ||
|
|
210
|
+
typeof driver.deadLetterName !== 'function' ||
|
|
211
|
+
typeof driver.capabilities !== 'object'
|
|
212
|
+
) {
|
|
213
|
+
throw consumerError('MessagingPlugin.configure(): "driver" must be a messaging driver', [
|
|
214
|
+
"Use rabbitmq() from '@znight/messaging/rabbitmq', sqs(), kafka() or memory() from their subpaths",
|
|
215
|
+
])
|
|
216
|
+
}
|
|
217
|
+
const defaults = options.defaults
|
|
218
|
+
if (defaults?.concurrency !== undefined && !isPositiveInteger(defaults.concurrency)) {
|
|
219
|
+
throw consumerError(
|
|
220
|
+
`MessagingPlugin defaults.concurrency must be a positive integer, got ${String(defaults.concurrency)}`,
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
if (defaults?.requeueDelay !== undefined && !(Number.isFinite(defaults.requeueDelay) && defaults.requeueDelay >= 0)) {
|
|
224
|
+
throw consumerError(
|
|
225
|
+
`MessagingPlugin defaults.requeueDelay must be a number of milliseconds >= 0, got ${String(defaults.requeueDelay)}`,
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
if (
|
|
229
|
+
defaults?.deadLetter !== undefined &&
|
|
230
|
+
defaults.deadLetter !== false &&
|
|
231
|
+
typeof defaults.deadLetter !== 'function'
|
|
232
|
+
) {
|
|
233
|
+
throw consumerError('MessagingPlugin defaults.deadLetter must be a function (queue => name) or false')
|
|
234
|
+
}
|
|
235
|
+
return options
|
|
236
|
+
}
|
package/src/retry.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { DriverCapabilities } from './driver.ts'
|
|
2
|
+
|
|
3
|
+
import { consumerError } from './errors.ts'
|
|
4
|
+
|
|
5
|
+
/** Backoff strategy between attempts. */
|
|
6
|
+
export type BackoffStrategy = 'fixed' | 'exponential'
|
|
7
|
+
|
|
8
|
+
/** Options of `@Retry()` and of the plugin's `defaults.retry`. */
|
|
9
|
+
export interface RetryOptions {
|
|
10
|
+
/** Total number of attempts, including the first delivery (`1`: no retry). */
|
|
11
|
+
readonly attempts: number
|
|
12
|
+
/** `fixed`: always `delay`; `exponential`: `delay * 2^(attempt - 1)`, capped by `maxDelay`. Default: `exponential`. */
|
|
13
|
+
readonly backoff?: BackoffStrategy
|
|
14
|
+
/** Base delay between attempts, in milliseconds. Default: 1000. */
|
|
15
|
+
readonly delay?: number
|
|
16
|
+
/** Upper bound of exponential delays, in milliseconds. */
|
|
17
|
+
readonly maxDelay?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A normalized retry policy, as stored in the metadata (`MessageHandlerDefinition.retry`). */
|
|
21
|
+
export interface RetryPolicy {
|
|
22
|
+
readonly attempts: number
|
|
23
|
+
readonly backoff: BackoffStrategy
|
|
24
|
+
readonly delay: number
|
|
25
|
+
readonly maxDelay?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Default base delay of a retry policy, in milliseconds. */
|
|
29
|
+
export const DEFAULT_RETRY_DELAY_MS = 1000
|
|
30
|
+
|
|
31
|
+
/** Default backoff strategy. */
|
|
32
|
+
export const DEFAULT_BACKOFF: BackoffStrategy = 'exponential'
|
|
33
|
+
|
|
34
|
+
/** Policy applied to handlers without `@Retry` when the plugin sets no `defaults.retry`. */
|
|
35
|
+
export const DEFAULT_RETRY_POLICY: RetryPolicy = Object.freeze({
|
|
36
|
+
attempts: 3,
|
|
37
|
+
backoff: 'exponential',
|
|
38
|
+
delay: DEFAULT_RETRY_DELAY_MS,
|
|
39
|
+
maxDelay: 30_000,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
/** The policy of `retry: false` / `{ attempts: 1 }`. */
|
|
43
|
+
export const NO_RETRY_POLICY: RetryPolicy = Object.freeze({ attempts: 1, backoff: 'fixed', delay: 0 })
|
|
44
|
+
|
|
45
|
+
const MAX_ATTEMPTS = 10_000
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validates retry options and fills defaults.
|
|
49
|
+
*
|
|
50
|
+
* @throws ZnightError ZNIGHT011 for invalid values (`where` names the handler in the message).
|
|
51
|
+
*/
|
|
52
|
+
export function normalizeRetry(options: RetryOptions | RetryPolicy, where = '@Retry()'): RetryPolicy {
|
|
53
|
+
if (options === null || typeof options !== 'object') {
|
|
54
|
+
throw consumerError(`${where}: retry options must be an object, got ${String(options)}`)
|
|
55
|
+
}
|
|
56
|
+
const { attempts, backoff = DEFAULT_BACKOFF, delay = DEFAULT_RETRY_DELAY_MS, maxDelay } = options
|
|
57
|
+
if (!Number.isInteger(attempts) || attempts < 1 || attempts > MAX_ATTEMPTS) {
|
|
58
|
+
throw consumerError(
|
|
59
|
+
`${where}: "attempts" must be an integer between 1 and ${MAX_ATTEMPTS}, got ${String(attempts)}`,
|
|
60
|
+
['attempts counts the first delivery: { attempts: 3 } means 1 delivery + 2 retries'],
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
if (backoff !== 'fixed' && backoff !== 'exponential') {
|
|
64
|
+
throw consumerError(`${where}: "backoff" must be 'fixed' or 'exponential', got ${String(backoff)}`)
|
|
65
|
+
}
|
|
66
|
+
if (!isDuration(delay)) {
|
|
67
|
+
throw consumerError(`${where}: "delay" must be a finite number of milliseconds >= 0, got ${String(delay)}`)
|
|
68
|
+
}
|
|
69
|
+
if (maxDelay !== undefined && (!isDuration(maxDelay) || maxDelay < delay)) {
|
|
70
|
+
throw consumerError(
|
|
71
|
+
`${where}: "maxDelay" must be a finite number of milliseconds >= delay (${delay}), got ${String(maxDelay)}`,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
return maxDelay === undefined ? { attempts, backoff, delay } : { attempts, backoff, delay, maxDelay }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isDuration(value: unknown): value is number {
|
|
78
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Delay before the next attempt once `attempt` (1-based) failed: `delay` for `fixed`,
|
|
83
|
+
* `delay * 2^(attempt - 1)` capped by `maxDelay` for `exponential`.
|
|
84
|
+
*/
|
|
85
|
+
export function computeRetryDelay(policy: RetryPolicy, attempt: number): number {
|
|
86
|
+
if (policy.backoff === 'fixed') return policy.delay
|
|
87
|
+
const exponent = Math.max(0, attempt - 1)
|
|
88
|
+
const raw = policy.delay * 2 ** exponent
|
|
89
|
+
return Math.min(raw, policy.maxDelay ?? Number.MAX_SAFE_INTEGER)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Every delay the policy may request (one per retry), in attempt order. */
|
|
93
|
+
export function retryDelays(policy: RetryPolicy): number[] {
|
|
94
|
+
const delays: number[] = []
|
|
95
|
+
for (let attempt = 1; attempt < policy.attempts; attempt++) delays.push(computeRetryDelay(policy, attempt))
|
|
96
|
+
return delays
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Whether a delay is waited in-process (`true`) or delegated to the broker (`false`). */
|
|
100
|
+
export function isInProcessDelay(delayMs: number, capabilities: DriverCapabilities): boolean {
|
|
101
|
+
return delayMs <= capabilities.maxInProcessDelayMs
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Checks that the driver can honour every delay of `policy`.
|
|
106
|
+
*
|
|
107
|
+
* @throws ZnightError ZNIGHT011 naming `where` and the offending delay.
|
|
108
|
+
*/
|
|
109
|
+
export function assertRetrySupported(
|
|
110
|
+
policy: RetryPolicy,
|
|
111
|
+
capabilities: DriverCapabilities,
|
|
112
|
+
driver: string,
|
|
113
|
+
where: string,
|
|
114
|
+
): void {
|
|
115
|
+
for (const delay of new Set(retryDelays(policy))) {
|
|
116
|
+
if (isInProcessDelay(delay, capabilities)) continue
|
|
117
|
+
if (capabilities.nativeDelay && delay <= capabilities.maxNativeDelayMs) continue
|
|
118
|
+
const limits = capabilities.nativeDelay
|
|
119
|
+
? `in-process delays up to ${capabilities.maxInProcessDelayMs} ms and native delays up to ${capabilities.maxNativeDelayMs} ms`
|
|
120
|
+
: `in-process delays up to ${capabilities.maxInProcessDelayMs} ms and no native delay`
|
|
121
|
+
throw consumerError(
|
|
122
|
+
`${where}: the ${driver} driver cannot wait ${delay} ms between attempts (it supports ${limits})`,
|
|
123
|
+
[
|
|
124
|
+
'Lower "delay"/"maxDelay" of the retry policy',
|
|
125
|
+
'Or raise the driver budget (e.g. maxInProcessDelayMs) if holding messages that long is acceptable',
|
|
126
|
+
],
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
}
|