@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.
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,609 @@
1
+ /**
2
+ * `@znight/messaging/memory`: an in-process broker for tests and local development. Deterministic
3
+ * (FIFO queues, round-robin between competing subscribers, microtask dispatch, timers only for
4
+ * delays), full featured (delays, native delivery counts, dead-letter queues, competing consumers)
5
+ * and inspectable (published messages, settlements, dead letters, `idle()`, failure injection).
6
+ *
7
+ * ```ts
8
+ * const driver = memory()
9
+ * const app = await Application.create({ metadata, plugins: [MessagingPlugin.configure({ driver })] })
10
+ * await app.start()
11
+ * await app.get(MessageBroker).publish('orders', { id: 1 }, { type: 'order.created' })
12
+ * await driver.idle()
13
+ * expect(driver.settlements('orders')).toMatchObject([{ action: 'ack' }])
14
+ * ```
15
+ */
16
+ import type {
17
+ DeadLetterRequest,
18
+ DeliveryHandler,
19
+ DriverCapabilities,
20
+ DriverContext,
21
+ DriverDelivery,
22
+ MessagingDriver,
23
+ RetryRequest,
24
+ SubscribeOptions,
25
+ Subscription,
26
+ } from '../driver.ts'
27
+ import type { DeadLetterReason, OutboundMessage } from '../message.ts'
28
+
29
+ import { randomUUID } from 'node:crypto'
30
+
31
+ import { jsonCodec } from '../codec.ts'
32
+ import { brokerUnavailableError } from '../errors.ts'
33
+ import { headerRecord } from '../headers.ts'
34
+ import { MAX_TIMER_DELAY_MS } from '../internal/async.ts'
35
+ import { deadLetterHeaders, durationOption } from './shared.ts'
36
+
37
+ /** Options of {@link memory}. */
38
+ export interface MemoryDriverOptions {
39
+ /** Longest retry delay waited in-process. Default: 1000 ms. */
40
+ readonly maxInProcessDelayMs?: number
41
+ /** Whether longer delays are scheduled by the driver (timers). Default: `true`. */
42
+ readonly nativeDelay?: boolean
43
+ /** Longest native delay. Default: unlimited. */
44
+ readonly maxNativeDelayMs?: number
45
+ /** Longest `publish(..., { delay })`. Default: unlimited. */
46
+ readonly maxPublishDelayMs?: number
47
+ /** Dead-letter naming. Default: `${queue}.dlq`. */
48
+ readonly deadLetterName?: (queue: string) => string
49
+ }
50
+
51
+ /** A message stored by the memory broker. */
52
+ export interface MemoryMessage {
53
+ readonly queue: string
54
+ readonly id: string
55
+ readonly type: string | undefined
56
+ readonly key: string | undefined
57
+ readonly headers: Readonly<Record<string, string>>
58
+ readonly body: Uint8Array | string
59
+ readonly contentType: string | undefined
60
+ readonly timestamp: number
61
+ /** Deliveries made so far (the next delivery is attempt `deliveries + 1`). */
62
+ readonly deliveries: number
63
+ /** The body decoded with the JSON codec (`undefined` when it cannot be decoded). */
64
+ readonly payload: unknown
65
+ }
66
+
67
+ /** A settlement performed by a consumer, in order. */
68
+ export interface MemorySettlement {
69
+ readonly queue: string
70
+ readonly id: string
71
+ readonly attempt: number
72
+ readonly action: 'ack' | 'retry' | 'release' | 'dead-letter' | 'reject'
73
+ /** Retry delay requested (native delays only; in-process delays happen before a `retry` with delay 0). */
74
+ readonly delayMs?: number
75
+ readonly reason?: DeadLetterReason
76
+ readonly error?: unknown
77
+ }
78
+
79
+ /** A dead-lettered message. */
80
+ export interface MemoryDeadLetter {
81
+ /** Dead-letter queue (`undefined` when dead-lettering was disabled: the message was discarded). */
82
+ readonly queue: string | undefined
83
+ readonly originalQueue: string
84
+ readonly message: MemoryMessage
85
+ readonly reason: DeadLetterReason
86
+ readonly error: unknown
87
+ readonly attempt: number
88
+ }
89
+
90
+ /** A message injected with {@link MemoryDriver.send}, as an external producer would. */
91
+ export interface MemorySendOptions {
92
+ /** Payload encoded with the JSON codec (ignored when `body` is given). */
93
+ readonly payload?: unknown
94
+ /** Raw body (e.g. malformed JSON). */
95
+ readonly body?: Uint8Array | string
96
+ readonly contentType?: string
97
+ readonly type?: string
98
+ readonly key?: string
99
+ readonly id?: string
100
+ readonly headers?: Readonly<Record<string, string>>
101
+ /** Deliveries already made (simulates a redelivered message). Default: 0. */
102
+ readonly deliveries?: number
103
+ }
104
+
105
+ /** Operations whose failure can be injected with {@link MemoryDriver.fail}. */
106
+ export type MemoryOperation = 'connect' | 'subscribe' | 'publish' | 'ack' | 'retry' | 'dead-letter' | 'release'
107
+
108
+ /** Options of {@link MemoryDriver.fail}. */
109
+ export interface MemoryFailureOptions {
110
+ /** Only for this queue (the destination queue for `publish`, the consumed queue otherwise). */
111
+ readonly queue?: string
112
+ /** Number of failures before the operation works again. Default: unlimited. */
113
+ readonly times?: number
114
+ /** Thrown error. Default: an `Error` naming the operation. */
115
+ readonly error?: unknown
116
+ }
117
+
118
+ interface StoredMessage {
119
+ readonly queue: string
120
+ readonly id: string
121
+ readonly type: string | undefined
122
+ readonly key: string | undefined
123
+ readonly headers: Readonly<Record<string, string>>
124
+ readonly body: Uint8Array | string
125
+ readonly contentType: string | undefined
126
+ readonly timestamp: number
127
+ deliveries: number
128
+ }
129
+
130
+ interface Subscriber {
131
+ readonly queue: string
132
+ readonly handler: DeliveryHandler
133
+ readonly concurrency: number
134
+ readonly options: SubscribeOptions
135
+ inflight: number
136
+ active: boolean
137
+ }
138
+
139
+ interface QueueState {
140
+ readonly ready: StoredMessage[]
141
+ readonly subscribers: Subscriber[]
142
+ next: number
143
+ scheduled: boolean
144
+ }
145
+
146
+ interface Failure {
147
+ readonly operation: MemoryOperation
148
+ readonly queue: string | undefined
149
+ remaining: number
150
+ readonly error: unknown
151
+ }
152
+
153
+ const DEFAULT_IN_PROCESS_DELAY_MS = 1000
154
+ const DEFAULT_IDLE_TIMEOUT_MS = 5000
155
+
156
+ /** Creates a {@link MemoryDriver}. */
157
+ export function memory(options: MemoryDriverOptions = {}): MemoryDriver {
158
+ return new MemoryDriver(options)
159
+ }
160
+
161
+ /** In-memory {@link MessagingDriver}. See the module documentation. */
162
+ export class MemoryDriver implements MessagingDriver {
163
+ readonly name = 'memory'
164
+ readonly capabilities: DriverCapabilities
165
+ readonly #deadLetterName: (queue: string) => string
166
+ readonly #queues = new Map<string, QueueState>()
167
+ readonly #published: MemoryMessage[] = []
168
+ readonly #settlements: MemorySettlement[] = []
169
+ readonly #deadLetters: MemoryDeadLetter[] = []
170
+ readonly #timers = new Set<ReturnType<typeof setTimeout>>()
171
+ readonly #failures: Failure[] = []
172
+ readonly #idleWaiters = new Set<() => void>()
173
+ #inflight = 0
174
+ #connected = false
175
+ #closed = false
176
+ #idleCheckScheduled = false
177
+
178
+ constructor(options: MemoryDriverOptions = {}) {
179
+ const nativeDelay = options.nativeDelay ?? true
180
+ this.capabilities = Object.freeze({
181
+ nativeDelay,
182
+ maxNativeDelayMs: nativeDelay
183
+ ? durationOption(options.maxNativeDelayMs, Number.POSITIVE_INFINITY, 'maxNativeDelayMs')
184
+ : 0,
185
+ maxInProcessDelayMs: durationOption(
186
+ options.maxInProcessDelayMs,
187
+ DEFAULT_IN_PROCESS_DELAY_MS,
188
+ 'maxInProcessDelayMs',
189
+ ),
190
+ deliveryCount: 'native',
191
+ maxPublishDelayMs: durationOption(options.maxPublishDelayMs, Number.POSITIVE_INFINITY, 'maxPublishDelayMs'),
192
+ })
193
+ this.#deadLetterName = options.deadLetterName ?? (queue => `${queue}.dlq`)
194
+ }
195
+
196
+ /** Whether `connect()` was called and `close()` was not. */
197
+ get connected(): boolean {
198
+ return this.#connected && !this.#closed
199
+ }
200
+
201
+ // -----------------------------------------------------------------------------------------------
202
+ // MessagingDriver
203
+ // -----------------------------------------------------------------------------------------------
204
+
205
+ async connect(_context: DriverContext): Promise<void> {
206
+ await Promise.resolve()
207
+ this.#maybeFail('connect', undefined)
208
+ this.#connected = true
209
+ this.#closed = false
210
+ }
211
+
212
+ async close(): Promise<void> {
213
+ await Promise.resolve()
214
+ this.#closed = true
215
+ for (const timer of this.#timers) clearTimeout(timer)
216
+ this.#timers.clear()
217
+ for (const state of this.#queues.values()) {
218
+ for (const subscriber of state.subscribers) subscriber.active = false
219
+ state.subscribers.length = 0
220
+ }
221
+ this.#checkIdle()
222
+ }
223
+
224
+ deadLetterName(queue: string): string {
225
+ return this.#deadLetterName(queue)
226
+ }
227
+
228
+ async publish(queue: string, message: OutboundMessage): Promise<void> {
229
+ await Promise.resolve()
230
+ this.#assertOpen(`publish to "${queue}"`)
231
+ this.#maybeFail('publish', queue)
232
+ const stored: StoredMessage = {
233
+ queue,
234
+ id: message.id,
235
+ type: message.type,
236
+ key: message.key,
237
+ headers: { ...message.headers },
238
+ body: message.body,
239
+ contentType: message.contentType,
240
+ timestamp: message.timestamp,
241
+ deliveries: 0,
242
+ }
243
+ this.#published.push(snapshot(stored))
244
+ if (message.delayMs > 0) this.#schedule(() => this.#enqueue(stored, 'tail'), message.delayMs)
245
+ else this.#enqueue(stored, 'tail')
246
+ }
247
+
248
+ async subscribe(queue: string, handler: DeliveryHandler, options: SubscribeOptions): Promise<Subscription> {
249
+ await Promise.resolve()
250
+ this.#assertOpen(`subscribe to "${queue}"`)
251
+ this.#maybeFail('subscribe', queue)
252
+ const state = this.#queue(queue)
253
+ const subscriber: Subscriber = {
254
+ queue,
255
+ handler,
256
+ concurrency: options.concurrency,
257
+ options,
258
+ inflight: 0,
259
+ active: true,
260
+ }
261
+ state.subscribers.push(subscriber)
262
+ this.#pump(queue)
263
+ return {
264
+ cancel: async (): Promise<void> => {
265
+ await Promise.resolve()
266
+ subscriber.active = false
267
+ this.#checkIdle()
268
+ },
269
+ close: async (): Promise<void> => {
270
+ await Promise.resolve()
271
+ subscriber.active = false
272
+ const index = state.subscribers.indexOf(subscriber)
273
+ if (index !== -1) state.subscribers.splice(index, 1)
274
+ this.#checkIdle()
275
+ },
276
+ }
277
+ }
278
+
279
+ // -----------------------------------------------------------------------------------------------
280
+ // Test helpers
281
+ // -----------------------------------------------------------------------------------------------
282
+
283
+ /**
284
+ * Injects a message into `queue` as an external producer would (no codec involved when `body` is
285
+ * given). Returns the message id.
286
+ */
287
+ send(queue: string, options: MemorySendOptions = {}): string {
288
+ this.#assertOpen(`send to "${queue}"`)
289
+ const encoded =
290
+ options.body === undefined
291
+ ? jsonCodec.encode(options.payload, options.contentType)
292
+ : { body: options.body, contentType: options.contentType }
293
+ const stored: StoredMessage = {
294
+ queue,
295
+ id: options.id ?? randomUUID(),
296
+ type: options.type,
297
+ key: options.key,
298
+ headers: { ...(options.headers ?? {}) },
299
+ body: encoded.body,
300
+ contentType: encoded.contentType,
301
+ timestamp: Date.now(),
302
+ deliveries: options.deliveries ?? 0,
303
+ }
304
+ this.#enqueue(stored, 'tail')
305
+ return stored.id
306
+ }
307
+
308
+ /** Messages published through `publish()` (the `MessageBroker`), optionally for one queue. */
309
+ published(queue?: string): MemoryMessage[] {
310
+ return this.#published.filter(message => queue === undefined || message.queue === queue)
311
+ }
312
+
313
+ /** Messages waiting in `queue` (not delivered yet, delayed messages excluded). */
314
+ pending(queue: string): MemoryMessage[] {
315
+ return (this.#queues.get(queue)?.ready ?? []).map(snapshot)
316
+ }
317
+
318
+ /** Settlements performed by consumers, in order, optionally for one consumed queue. */
319
+ settlements(queue?: string): MemorySettlement[] {
320
+ return this.#settlements.filter(settlement => queue === undefined || settlement.queue === queue)
321
+ }
322
+
323
+ /** Dead-lettered (or discarded) messages, optionally for one consumed queue. */
324
+ deadLetters(queue?: string): MemoryDeadLetter[] {
325
+ return this.#deadLetters.filter(entry => queue === undefined || entry.originalQueue === queue)
326
+ }
327
+
328
+ /** Deliveries currently handed to consumers and not finished. */
329
+ get inFlight(): number {
330
+ return this.#inflight
331
+ }
332
+
333
+ /** Whether nothing is in flight, scheduled or deliverable. */
334
+ isIdle(): boolean {
335
+ if (this.#inflight > 0 || this.#timers.size > 0) return false
336
+ for (const state of this.#queues.values()) {
337
+ if (state.scheduled) return false
338
+ if (state.ready.length > 0 && state.subscribers.some(s => s.active && s.inflight < s.concurrency)) return false
339
+ }
340
+ return true
341
+ }
342
+
343
+ /**
344
+ * Resolves once nothing is in flight, scheduled (delayed messages, retries) or deliverable to an
345
+ * active subscriber. Rejects after `timeoutMs` (default 5 s).
346
+ */
347
+ idle(timeoutMs: number = DEFAULT_IDLE_TIMEOUT_MS): Promise<void> {
348
+ return new Promise((resolve, reject) => {
349
+ const timer = setTimeout(() => {
350
+ this.#idleWaiters.delete(done)
351
+ reject(
352
+ new Error(`[znight] MemoryDriver.idle(): still busy after ${timeoutMs} ms (${this.#inflight} in flight)`),
353
+ )
354
+ }, timeoutMs)
355
+ const done = (): void => {
356
+ clearTimeout(timer)
357
+ resolve()
358
+ }
359
+ this.#idleWaiters.add(done)
360
+ this.#checkIdle()
361
+ })
362
+ }
363
+
364
+ /** Polls `predicate` until it returns `true`; rejects after `timeoutMs` (default 5 s). */
365
+ async waitFor(predicate: () => boolean, timeoutMs: number = DEFAULT_IDLE_TIMEOUT_MS): Promise<void> {
366
+ const deadline = Date.now() + timeoutMs
367
+ while (!predicate()) {
368
+ if (Date.now() > deadline)
369
+ throw new Error(`[znight] MemoryDriver.waitFor(): condition not met after ${timeoutMs} ms`)
370
+ await new Promise(resolve => setTimeout(resolve, 1))
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Makes an operation fail (for tests): `publish` (also dead-letter publications, by destination
376
+ * queue), `ack`, `retry`, `dead-letter`, `release` (by consumed queue), `subscribe`, `connect`.
377
+ * Returns a function removing the failure.
378
+ */
379
+ fail(operation: MemoryOperation, options: MemoryFailureOptions = {}): () => void {
380
+ const failure: Failure = {
381
+ operation,
382
+ queue: options.queue,
383
+ remaining: options.times ?? Number.POSITIVE_INFINITY,
384
+ error: options.error ?? new Error(`[memory] injected ${operation} failure`),
385
+ }
386
+ this.#failures.push(failure)
387
+ return () => {
388
+ const index = this.#failures.indexOf(failure)
389
+ if (index !== -1) this.#failures.splice(index, 1)
390
+ }
391
+ }
392
+
393
+ /** Forgets every message, settlement and failure (subscriptions are kept). */
394
+ reset(): void {
395
+ for (const timer of this.#timers) clearTimeout(timer)
396
+ this.#timers.clear()
397
+ for (const state of this.#queues.values()) state.ready.length = 0
398
+ this.#published.length = 0
399
+ this.#settlements.length = 0
400
+ this.#deadLetters.length = 0
401
+ this.#failures.length = 0
402
+ this.#checkIdle()
403
+ }
404
+
405
+ // -----------------------------------------------------------------------------------------------
406
+ // Internals
407
+ // -----------------------------------------------------------------------------------------------
408
+
409
+ #queue(name: string): QueueState {
410
+ let state = this.#queues.get(name)
411
+ if (state === undefined) {
412
+ state = { ready: [], subscribers: [], next: 0, scheduled: false }
413
+ this.#queues.set(name, state)
414
+ }
415
+ return state
416
+ }
417
+
418
+ #enqueue(message: StoredMessage, position: 'head' | 'tail'): void {
419
+ const state = this.#queue(message.queue)
420
+ if (position === 'head') state.ready.unshift(message)
421
+ else state.ready.push(message)
422
+ this.#pump(message.queue)
423
+ }
424
+
425
+ /** Runs `fn` after `delayMs` (any duration: a timer waits at most 2^31 - 1 ms, longer delays re-arm). */
426
+ #schedule(fn: () => void, delayMs: number): void {
427
+ const wait = Math.min(delayMs, MAX_TIMER_DELAY_MS)
428
+ const timer = setTimeout(() => {
429
+ this.#timers.delete(timer)
430
+ if (delayMs > wait) return this.#schedule(fn, delayMs - wait)
431
+ fn()
432
+ this.#checkIdle()
433
+ }, wait)
434
+ this.#timers.add(timer)
435
+ }
436
+
437
+ #pump(queue: string): void {
438
+ const state = this.#queue(queue)
439
+ if (state.scheduled) return
440
+ state.scheduled = true
441
+ queueMicrotask(() => {
442
+ state.scheduled = false
443
+ this.#dispatch(state)
444
+ this.#checkIdle()
445
+ })
446
+ }
447
+
448
+ #dispatch(state: QueueState): void {
449
+ while (state.ready.length > 0) {
450
+ const subscriber = this.#nextSubscriber(state)
451
+ if (subscriber === undefined) return
452
+ const message = state.ready.shift() as StoredMessage
453
+ this.#deliver(subscriber, message)
454
+ }
455
+ }
456
+
457
+ #nextSubscriber(state: QueueState): Subscriber | undefined {
458
+ const count = state.subscribers.length
459
+ for (let offset = 0; offset < count; offset++) {
460
+ const index = (state.next + offset) % count
461
+ const subscriber = state.subscribers[index] as Subscriber
462
+ if (subscriber.active && subscriber.inflight < subscriber.concurrency) {
463
+ state.next = (index + 1) % count
464
+ return subscriber
465
+ }
466
+ }
467
+ return undefined
468
+ }
469
+
470
+ #deliver(subscriber: Subscriber, message: StoredMessage): void {
471
+ message.deliveries++
472
+ subscriber.inflight++
473
+ this.#inflight++
474
+ const attempt = message.deliveries
475
+ let settled = false
476
+ /** Validates a settlement before any side effect (already settled, closed driver, injected failure). */
477
+ const check = (action: MemoryOperation): void => {
478
+ if (settled) throw new Error(`[memory] message ${message.id} was already settled (${action})`)
479
+ this.#assertOpen(`${action} message ${message.id}`)
480
+ this.#maybeFail(action, message.queue)
481
+ }
482
+ const record = (settlement: Omit<MemorySettlement, 'queue' | 'id' | 'attempt'>): void => {
483
+ this.#settlements.push({ queue: message.queue, id: message.id, attempt, ...settlement })
484
+ }
485
+ const delivery: DriverDelivery = {
486
+ id: message.id,
487
+ body: message.body,
488
+ contentType: message.contentType,
489
+ headers: message.headers,
490
+ type: message.type,
491
+ key: message.key,
492
+ attempt,
493
+ redelivered: attempt > 1,
494
+ timestamp: message.timestamp,
495
+ raw: snapshot(message),
496
+ ack: async (): Promise<void> => {
497
+ await Promise.resolve()
498
+ check('ack')
499
+ settled = true
500
+ record({ action: 'ack' })
501
+ },
502
+ retry: async (request: RetryRequest): Promise<void> => {
503
+ await Promise.resolve()
504
+ check('retry')
505
+ settled = true
506
+ record({ action: 'retry', delayMs: request.delayMs, error: request.error })
507
+ if (request.delayMs > 0) this.#schedule(() => this.#enqueue(message, 'tail'), request.delayMs)
508
+ else this.#enqueue(message, 'tail')
509
+ },
510
+ deadLetter: async (request: DeadLetterRequest): Promise<void> => {
511
+ await Promise.resolve()
512
+ check('dead-letter')
513
+ const queue = request.queue
514
+ if (queue !== undefined) {
515
+ this.#maybeFail('publish', queue)
516
+ const copy: StoredMessage = {
517
+ ...message,
518
+ queue,
519
+ headers: deadLetterHeaders(message.headers, request, message.queue),
520
+ deliveries: 0,
521
+ }
522
+ this.#enqueue(copy, 'tail')
523
+ }
524
+ settled = true
525
+ record({ action: queue === undefined ? 'reject' : 'dead-letter', reason: request.reason, error: request.error })
526
+ this.#deadLetters.push({
527
+ queue,
528
+ originalQueue: message.queue,
529
+ message: snapshot(message),
530
+ reason: request.reason,
531
+ error: request.error,
532
+ attempt: request.attempt,
533
+ })
534
+ },
535
+ release: async (): Promise<void> => {
536
+ await Promise.resolve()
537
+ check('release')
538
+ settled = true
539
+ record({ action: 'release' })
540
+ this.#enqueue(message, 'head')
541
+ },
542
+ }
543
+ const finish = (): void => {
544
+ subscriber.inflight--
545
+ this.#inflight--
546
+ if (!settled && !this.#closed) {
547
+ // the consumer returned without settling: give the message back, like a broker would
548
+ settled = true
549
+ record({ action: 'release' })
550
+ this.#enqueue(message, 'head')
551
+ }
552
+ this.#pump(message.queue)
553
+ this.#checkIdle()
554
+ }
555
+ let result: Promise<void>
556
+ try {
557
+ result = subscriber.handler(delivery)
558
+ } catch (error) {
559
+ result = Promise.reject(error)
560
+ }
561
+ Promise.resolve(result).then(finish, finish)
562
+ }
563
+
564
+ #maybeFail(operation: MemoryOperation, queue: string | undefined): void {
565
+ for (const failure of this.#failures) {
566
+ if (failure.operation !== operation || failure.remaining <= 0) continue
567
+ if (failure.queue !== undefined && failure.queue !== queue) continue
568
+ failure.remaining--
569
+ throw failure.error
570
+ }
571
+ }
572
+
573
+ #assertOpen(action: string): void {
574
+ if (this.#closed) throw brokerUnavailableError(`[memory] Cannot ${action}: the driver is closed`)
575
+ }
576
+
577
+ #checkIdle(): void {
578
+ if (this.#idleWaiters.size === 0 || this.#idleCheckScheduled) return
579
+ this.#idleCheckScheduled = true
580
+ queueMicrotask(() => {
581
+ this.#idleCheckScheduled = false
582
+ if (!this.isIdle()) return
583
+ const waiters = [...this.#idleWaiters]
584
+ this.#idleWaiters.clear()
585
+ for (const waiter of waiters) waiter()
586
+ })
587
+ }
588
+ }
589
+
590
+ function snapshot(message: StoredMessage): MemoryMessage {
591
+ let payload: unknown
592
+ try {
593
+ payload = jsonCodec.decode(message.body, message.contentType)
594
+ } catch {
595
+ payload = undefined
596
+ }
597
+ return {
598
+ queue: message.queue,
599
+ id: message.id,
600
+ type: message.type,
601
+ key: message.key,
602
+ headers: Object.assign(headerRecord(), message.headers),
603
+ body: message.body,
604
+ contentType: message.contentType,
605
+ timestamp: message.timestamp,
606
+ deliveries: message.deliveries,
607
+ payload,
608
+ }
609
+ }