@warlock.js/herald 4.15.0 → 5.0.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/CHANGELOG.md CHANGED
@@ -1,30 +1,48 @@
1
- # Changelog — @warlock.js/herald
2
-
3
- All notable changes to `@warlock.js/herald` are documented in this file.
4
-
1
+ # Changelog — @warlock.js/herald
2
+
3
+ All notable changes to `@warlock.js/herald` are documented in this file.
4
+
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
7
- ## 4.15.0
8
-
9
- ### Fixed
10
-
11
- - **The RabbitMQ driver's lazy `amqplib` loader was not idempotent under concurrent callers.** It cached the resolved module in a module-level binding but nothing guarded the load itself, so two loads could be in flight at the same time and the last one to settle won the binding. The eager, unawaited `loadAmqplibModule()` call at module scope was one of those callers by construction — it started a load nobody was waiting on, which then raced the awaited call from `connect()`. That eager call has been removed: `connect()` already awaits the loader, so it bought nothing but the race. The loader now memoizes the in-flight promise itself, so the first caller starts the `import()` and every later caller awaits that same one
12
- - **No user-visible misbehaviour is known in production** — both racing paths resolve the same real `amqplib`, so whichever won, callers got the module they expected. The observable damage was in test isolation: when a test was aborted mid-`await import(...)`, the racing loads could leave the binding holding the real `amqplib` while the test file's `vi.mock("amqplib")` was still active, so every later test in that file silently bypassed the mock and opened a real socket. Proven by instrumentation — the driver held a live `ChannelModel` on `::1:5672` while `import("amqplib")` inside the same test still returned the mock, which is how a green test could be green for the wrong reason
13
- - **Verified by a timeout sweep, not by a passing suite.** The full suite passed both before and after (13 files / 137 tests), because the fault only surfaces when the first test is starved of time. Running `tests/connect-to-broker.test.ts` at `--testTimeout=3000` and `4000` previously timed out the first test *and* took `wraps a connection failure with the driver name` down with it, failing in ~50 ms with `promise resolved "Broker{…}" instead of rejecting` — the mock was gone. With the loader fixed, that test passes at every timeout even while the first test still times out: starving one test can no longer poison the next
14
- - **The first test also paid a cold-transform cost inside its own timed body**, since `connectToBroker` dynamically imports the driver, which pulls in `@warlock.js/seal` and `@warlock.js/logger` as raw TS source. That work moved to a `beforeAll` warm-up. This is a test-timing change only and carries none of the correctness weight above — the loader fix stands on its own without it
15
-
16
- ## 4.12.0
7
+ ## 5.0.0 - 2026-08-25
17
8
 
18
9
  ### Changed
19
10
 
20
- - Declares its own test runner and pins it to an exact version (`vitest@4.1.10`). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
21
-
22
- ## 4.2.11
23
-
24
- ### Changed
25
-
26
- - Bumped `@mongez/reinforcements` to 3.3.0
27
-
28
- ## 4.1.15
29
-
30
- - Baseline — per-package changelog tracking starts at this version.
11
+ - This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
12
+
13
+ ## 4.16.0 - 2026-08-18
14
+
15
+ ### Security
16
+
17
+ - **Poison-message DoS: an `EventConsumer.handle()` that reliably throws was nack+requeued forever**, with no retry cap and no dead-letter escape hatch — a single bad message could pin a consumer in a hot ack/nack loop indefinitely, starving every other message behind it (worse with low prefetch). `prepareConsumerSubscription`'s catch now calls the channel's bounded `ctx.retry()` instead of an unconditional `ctx.nack(true)`, so redelivery is capped and the message is dead-lettered (if configured) or dropped with a loud `log.error` once the cap is hit — never silently, and never forever
18
+ - **Fixed the retry counter never advancing on the automatic (non-explicit) nack path** (`RabbitMQChannel.subscribe`'s catch, used by any direct `channel.subscribe(handler, { retry, deadLetter })` caller whose handler throws instead of calling `ctx.retry()` itself). It read `x-retry-count` from the *original* message's headers and then plain-`nack(msg, false, true)`'d — which redelivers that same original message, so the header a developer's `retry.maxRetries`/`deadLetter` depended on never changed and the configured cap was silently never reached. Both the automatic path and the explicit `ctx.retry()` path now share one bounded-retry routine that republishes with an incremented header, so `maxRetries`/`deadLetter` are honored regardless of which path a handler takes
19
+ - **Broker credentials no longer leak into thrown/logged connection errors.** `username`/`password` are now URI-encoded when building the `amqp://` URL (a reserved character like `@`/`:`/`/` in a generated secret previously produced a malformed URL whose parser error echoed the raw credential back), and any error surfaced from `connect()` — including one that embeds a caller-supplied `uri` with credentials — has `user:pass@` redacted before it's re-thrown, so a connection failure can no longer put a plaintext broker password in front of `console.error`/structured logging/an error tracker
20
+
21
+ ### Dependencies
22
+
23
+ - Bumped `@mongez/events` to `^2.2.7` (no breaking changes) and `@mongez/reinforcements` to `^4.0.1`. The reinforcements major makes `Random.string/nanoid/id/token/uuid` CSPRNG-backed (WebCrypto) and removes `Random.seed()` support — audited this package's source and tests for `Random.seed(` and for seeded/reproducible use of `Random.*`; none found, so no code changes were needed.
24
+
25
+ ## 4.15.0 - 2026-08-16
26
+
27
+ ### Fixed
28
+
29
+ - **The RabbitMQ driver's lazy `amqplib` loader was not idempotent under concurrent callers.** It cached the resolved module in a module-level binding but nothing guarded the load itself, so two loads could be in flight at the same time and the last one to settle won the binding. The eager, unawaited `loadAmqplibModule()` call at module scope was one of those callers by construction — it started a load nobody was waiting on, which then raced the awaited call from `connect()`. That eager call has been removed: `connect()` already awaits the loader, so it bought nothing but the race. The loader now memoizes the in-flight promise itself, so the first caller starts the `import()` and every later caller awaits that same one
30
+ - **No user-visible misbehaviour is known in production** — both racing paths resolve the same real `amqplib`, so whichever won, callers got the module they expected. The observable damage was in test isolation: when a test was aborted mid-`await import(...)`, the racing loads could leave the binding holding the real `amqplib` while the test file's `vi.mock("amqplib")` was still active, so every later test in that file silently bypassed the mock and opened a real socket. Proven by instrumentation — the driver held a live `ChannelModel` on `::1:5672` while `import("amqplib")` inside the same test still returned the mock, which is how a green test could be green for the wrong reason
31
+ - **Verified by a timeout sweep, not by a passing suite.** The full suite passed both before and after (13 files / 137 tests), because the fault only surfaces when the first test is starved of time. Running `tests/connect-to-broker.test.ts` at `--testTimeout=3000` and `4000` previously timed out the first test *and* took `wraps a connection failure with the driver name` down with it, failing in ~50 ms with `promise resolved "Broker{…}" instead of rejecting` — the mock was gone. With the loader fixed, that test passes at every timeout even while the first test still times out: starving one test can no longer poison the next
32
+ - **The first test also paid a cold-transform cost inside its own timed body**, since `connectToBroker` dynamically imports the driver, which pulls in `@warlock.js/seal` and `@warlock.js/logger` as raw TS source. That work moved to a `beforeAll` warm-up. This is a test-timing change only and carries none of the correctness weight above — the loader fix stands on its own without it
33
+
34
+ ## 4.12.0
35
+
36
+ ### Changed
37
+
38
+ - Declares its own test runner and pins it to an exact version (`vitest@4.1.10`). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
39
+
40
+ ## 4.2.11
41
+
42
+ ### Changed
43
+
44
+ - Bumped `@mongez/reinforcements` to 3.3.0
45
+
46
+ ## 4.1.15
47
+
48
+ - Baseline — per-package changelog tracking starts at this version.
package/cjs/index.cjs CHANGED
@@ -17,6 +17,7 @@ var __exportAll = (all, no_symbols) => {
17
17
 
18
18
  //#endregion
19
19
  let node_events = require("node:events");
20
+ let _warlock_js_logger = require("@warlock.js/logger");
20
21
  let _warlock_js_seal = require("@warlock.js/seal");
21
22
  let node_crypto = require("node:crypto");
22
23
  let crypto = require("crypto");
@@ -354,7 +355,7 @@ function prepareConsumerSubscription(Consumer, onError) {
354
355
  });
355
356
  ctx.ack();
356
357
  } catch (error) {
357
- ctx.nack(true);
358
+ await ctx.retry();
358
359
  if (onError) onError(error, Consumer.eventName);
359
360
  }
360
361
  };
@@ -465,6 +466,7 @@ var RabbitMQChannel = class {
465
466
  const { consumerTag } = await this.amqpChannel.consume(this.name, async (msg) => {
466
467
  if (!msg) return;
467
468
  let ackHandled = isFireAndForget;
469
+ let parsedMessage;
468
470
  try {
469
471
  const content = JSON.parse(msg.content.toString());
470
472
  let payload = content.payload;
@@ -495,6 +497,7 @@ var RabbitMQChannel = class {
495
497
  payload,
496
498
  raw: msg
497
499
  };
500
+ parsedMessage = message;
498
501
  await handler(message, {
499
502
  ack: async () => {
500
503
  if (!ackHandled) {
@@ -530,31 +533,16 @@ var RabbitMQChannel = class {
530
533
  retry: async (delay) => {
531
534
  if (ackHandled) return;
532
535
  ackHandled = true;
533
- const retryCount = (metadata.retryCount || 0) + 1;
534
- if (retryCount > (options?.retry?.maxRetries ?? 3)) {
535
- if (options?.deadLetter) await this.sendToDeadLetter(message, options.deadLetter.channel);
536
- this.amqpChannel.ack(msg);
537
- return;
538
- }
539
- const headers = {
540
- ...msg.properties.headers,
541
- "x-retry-count": retryCount
542
- };
543
- if (delay) headers["x-delay"] = delay;
544
- this.amqpChannel.sendToQueue(this.name, msg.content, {
545
- ...msg.properties,
546
- headers
547
- });
548
- this.amqpChannel.ack(msg);
536
+ await this.retryOrGiveUp(msg, metadata.retryCount || 0, options, message, delay);
549
537
  }
550
538
  });
551
539
  if (!ackHandled) this.amqpChannel.ack(msg);
552
540
  } catch (error) {
553
541
  if (ackHandled) return;
554
- if (options?.retry) if ((msg.properties.headers?.["x-retry-count"] || 0) < options.retry.maxRetries) this.amqpChannel.nack(msg, false, true);
555
- else if (options.deadLetter) this.amqpChannel.nack(msg, false, false);
556
- else this.amqpChannel.reject(msg, false);
557
- else this.amqpChannel.nack(msg, false, false);
542
+ if (options?.retry) {
543
+ const currentRetryCount = msg.properties.headers?.["x-retry-count"] || 0;
544
+ await this.retryOrGiveUp(msg, currentRetryCount, options, parsedMessage);
545
+ } else this.amqpChannel.nack(msg, false, false);
558
546
  }
559
547
  }, consumerOptions);
560
548
  const subscription = new RabbitMQSubscription(subscriptionId, this.name, consumerTag, this.amqpChannel);
@@ -593,6 +581,61 @@ var RabbitMQChannel = class {
593
581
  this.amqpChannel.sendToQueue(deadLetterChannel, Buffer.from(content), { persistent: true });
594
582
  }
595
583
  /**
584
+ * Dead-letter a message whose body couldn't be parsed into a {@link Message}
585
+ * (e.g. `JSON.parse` itself threw before an envelope existed). Forwards the
586
+ * raw AMQP bytes/properties as-is rather than re-deriving a payload, so a
587
+ * malformed message isn't lost.
588
+ */
589
+ sendToDeadLetterRaw(msg, deadLetterChannel) {
590
+ this.amqpChannel.sendToQueue(deadLetterChannel, msg.content, {
591
+ ...msg.properties,
592
+ persistent: true
593
+ });
594
+ }
595
+ /**
596
+ * Bounded retry shared by the explicit `ctx.retry()` call and the automatic
597
+ * catch when a handler throws without calling it itself — so both paths
598
+ * honor the same cap instead of the automatic path silently requeueing
599
+ * forever (see `subscribe()`'s catch block).
600
+ *
601
+ * Under the cap: republishes with an incremented `x-retry-count` header —
602
+ * NOT a plain `nack(msg, false, true)`, which redelivers the original
603
+ * message untouched and never advances the counter.
604
+ *
605
+ * At/over the cap: dead-letters if configured, otherwise drops the message
606
+ * with a loud `log.error` (never a silent drop) so an operator can see a
607
+ * poison message was discarded instead of it vanishing without a trace.
608
+ */
609
+ async retryOrGiveUp(msg, currentRetryCount, options, parsedMessage, delay) {
610
+ const retryCount = currentRetryCount + 1;
611
+ const maxRetries = options?.retry?.maxRetries ?? 3;
612
+ if (retryCount > maxRetries) {
613
+ if (options?.deadLetter) if (parsedMessage) await this.sendToDeadLetter(parsedMessage, options.deadLetter.channel);
614
+ else this.sendToDeadLetterRaw(msg, options.deadLetter.channel);
615
+ else {
616
+ _warlock_js_logger.log.error("herald", "poison-message", `Dropping message on channel "${this.name}" after ${retryCount - 1} failed ${retryCount - 1 === 1 ? "retry" : "retries"} (maxRetries: ${maxRetries}) with no dead-letter channel configured.`, {
617
+ channel: this.name,
618
+ retryCount: retryCount - 1,
619
+ maxRetries
620
+ });
621
+ this.amqpChannel.reject(msg, false);
622
+ return;
623
+ }
624
+ this.amqpChannel.ack(msg);
625
+ return;
626
+ }
627
+ const headers = {
628
+ ...msg.properties.headers,
629
+ "x-retry-count": retryCount
630
+ };
631
+ if (delay) headers["x-delay"] = delay;
632
+ this.amqpChannel.sendToQueue(this.name, msg.content, {
633
+ ...msg.properties,
634
+ headers
635
+ });
636
+ this.amqpChannel.ack(msg);
637
+ }
638
+ /**
596
639
  * Request-response pattern
597
640
  */
598
641
  async request(payload, options) {
@@ -738,6 +781,18 @@ Or manually:
738
781
  yarn add amqplib
739
782
  `.trim();
740
783
  /**
784
+ * Strip `user:password@` credentials from any `amqp(s)://` URL embedded in a
785
+ * string. The connection URL carries plaintext broker credentials, and
786
+ * amqplib/Node's URL parser commonly echoes the offending URL verbatim in a
787
+ * malformed-URL error (e.g. an unencoded `@`/`:` in the password) — applied
788
+ * to every error `connect()` surfaces so a credential never reaches whatever
789
+ * the host app does with a thrown connection error (console.error,
790
+ * structured logging, an error tracker).
791
+ */
792
+ function redactAmqpCredentials(message) {
793
+ return message.replace(/(amqps?:\/\/)[^/@\s]+@/gi, "$1****:****@");
794
+ }
795
+ /**
741
796
  * Load amqplib, reusing the single shared load for every caller.
742
797
  *
743
798
  * @returns The amqplib module, or `undefined` when it is not installed.
@@ -854,7 +909,8 @@ var RabbitMQDriver = class {
854
909
  });
855
910
  } catch (error) {
856
911
  this._isConnected = false;
857
- throw new Error(`Failed to connect to RabbitMQ: ${error instanceof Error ? error.message : String(error)}`);
912
+ const message = error instanceof Error ? error.message : String(error);
913
+ throw new Error(`Failed to connect to RabbitMQ: ${redactAmqpCredentials(message)}`);
858
914
  }
859
915
  }
860
916
  /**
@@ -866,7 +922,7 @@ var RabbitMQDriver = class {
866
922
  const host = this.options.host ?? "localhost";
867
923
  const port = this.options.port ?? 5672;
868
924
  const vhost = this.options.vhost ?? "/";
869
- return `${protocol}://${this.options.username ?? "guest"}:${this.options.password ?? "guest"}@${host}:${port}/${encodeURIComponent(vhost)}`;
925
+ return `${protocol}://${encodeURIComponent(this.options.username ?? "guest")}:${encodeURIComponent(this.options.password ?? "guest")}@${host}:${port}/${encodeURIComponent(vhost)}`;
870
926
  }
871
927
  /**
872
928
  * Handle reconnection
package/cjs/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["EventEmitter","v","EventEmitter","v"],"sources":["../../../../../../herald/src/communicators/broker.ts","../../../../../../herald/src/communicators/broker-registry.ts","../../../../../../herald/src/message-managers/prepare-consumer-subscription.ts","../../../../../../herald/src/drivers/rabbitmq/rabbitmq-channel.ts","../../../../../../herald/src/drivers/rabbitmq/rabbitmq-driver.ts","../../../../../../herald/src/utils/connect-to-broker.ts","../../../../../../herald/src/decorators/consumable.ts","../../../../../../herald/src/message-managers/event-consumer.ts","../../../../../../herald/src/message-managers/event-message.ts","../../../../../../herald/src/use-case-broadcast.ts"],"sourcesContent":["import type { BrokerDriverContract } from \"../contracts\";\r\nimport type { ChannelContract } from \"../contracts/channel.contract\";\r\nimport { EventMessage } from \"../message-managers/event-message\";\r\nimport { EventConsumerClass } from \"../message-managers/types\";\r\nimport type { ChannelOptions } from \"../types\";\r\n\r\n/**\r\n * Options for creating a Broker\r\n */\r\nexport interface BrokerOptions {\r\n /** Unique name for this broker */\r\n name: string;\r\n /** The underlying driver */\r\n driver: BrokerDriverContract;\r\n /** Whether this is the default broker */\r\n isDefault?: boolean;\r\n}\r\n\r\n/**\r\n * Broker - wrapper around a driver with metadata\r\n *\r\n * Similar to DataSource in @warlock.js/cascade\r\n *\r\n * @example\r\n * ```typescript\r\n * const broker = new Broker({\r\n * name: \"default\",\r\n * driver: rabbitMQDriver,\r\n * isDefault: true,\r\n * });\r\n *\r\n * // Get a channel\r\n * const channel = broker.channel(\"user.created\");\r\n * ```\r\n */\r\nexport class Broker {\r\n /** Unique name identifying this broker */\r\n public readonly name: string;\r\n\r\n /** The underlying driver */\r\n public readonly driver: BrokerDriverContract;\r\n\r\n /** Whether this is the default broker */\r\n public readonly isDefault: boolean;\r\n\r\n /**\r\n * Create a new Broker\r\n *\r\n * @param options - Broker configuration\r\n */\r\n public constructor(options: BrokerOptions) {\r\n this.name = options.name;\r\n this.driver = options.driver;\r\n this.isDefault = Boolean(options.isDefault);\r\n }\r\n\r\n /**\r\n * Subscribe the given consumer\r\n */\r\n public subscribe(consumer: EventConsumerClass<any>) {\r\n return this.driver.subscribe(consumer);\r\n }\r\n\r\n /**\r\n * Publish the given event message\r\n */\r\n public publish<TPayload = Record<string, any>>(event: EventMessage<TPayload>) {\r\n this.driver.publish(event);\r\n }\r\n\r\n /**\r\n * Get or create a channel\r\n *\r\n * @param name - Channel name\r\n * @param options - Channel options\r\n * @returns Channel instance\r\n *\r\n * @example\r\n * ```typescript\r\n * // Simple channel\r\n * const channel = broker.channel(\"notifications\");\r\n *\r\n * // Typed channel with schema\r\n * const orderChannel = broker.channel<OrderPayload>(\"orders\", {\r\n * schema: OrderSchema,\r\n * durable: true,\r\n * });\r\n * ```\r\n */\r\n public channel<TPayload = unknown>(\r\n name: string,\r\n options?: ChannelOptions<TPayload>,\r\n ): ChannelContract<TPayload> {\r\n return this.driver.channel<TPayload>(name, options);\r\n }\r\n\r\n /**\r\n * Check if the broker is connected\r\n */\r\n public get isConnected(): boolean {\r\n return this.driver.isConnected;\r\n }\r\n\r\n /**\r\n * Connect the underlying driver\r\n */\r\n public async connect(): Promise<void> {\r\n await this.driver.connect();\r\n }\r\n\r\n /**\r\n * Disconnect the underlying driver\r\n */\r\n public async disconnect(): Promise<void> {\r\n await this.driver.disconnect();\r\n }\r\n\r\n /**\r\n * Start consuming messages\r\n */\r\n public async startConsuming(): Promise<void> {\r\n await this.driver.startConsuming();\r\n }\r\n\r\n /**\r\n * Stop consuming messages\r\n */\r\n public async stopConsuming(): Promise<void> {\r\n await this.driver.stopConsuming();\r\n }\r\n\r\n /**\r\n * Health check\r\n */\r\n public async healthCheck() {\r\n return this.driver.healthCheck();\r\n }\r\n}\r\n","import { EventEmitter } from \"node:events\";\r\nimport type { BrokerRegistryEvent, BrokerRegistryListener } from \"../types\";\r\nimport { Broker, type BrokerOptions } from \"./broker\";\r\n\r\n/**\r\n * Error thrown when a broker is not found\r\n */\r\nexport class MissingBrokerError extends Error {\r\n public readonly brokerName?: string;\r\n\r\n public constructor(message: string, brokerName?: string) {\r\n super(message);\r\n this.name = \"MissingBrokerError\";\r\n this.brokerName = brokerName;\r\n }\r\n}\r\n\r\n/**\r\n * Broker Registry\r\n *\r\n * Maintains registry of named brokers.\r\n * Similar to DataSourceRegistry in @warlock.js/cascade\r\n *\r\n * @example\r\n * ```typescript\r\n * // Register a broker\r\n * brokerRegistry.register({\r\n * name: \"default\",\r\n * driver: rabbitMQDriver,\r\n * isDefault: true,\r\n * });\r\n *\r\n * // Get the default broker\r\n * const comm = brokerRegistry.get();\r\n *\r\n * // Get a specific broker by name\r\n * const analytics = brokerRegistry.get(\"analytics\");\r\n *\r\n * // Listen for events\r\n * brokerRegistry.on(\"connected\", (comm) => {\r\n * console.log(`${comm.name} connected`);\r\n * });\r\n * ```\r\n */\r\nclass BrokerRegistry {\r\n private readonly sources = new Map<string, Broker>();\r\n private defaultSource?: Broker;\r\n private readonly events = new EventEmitter();\r\n\r\n /**\r\n * Register a new broker\r\n *\r\n * Sets up event forwarding from the driver to the registry.\r\n *\r\n * @param options - Broker configuration\r\n * @returns The registered broker instance\r\n *\r\n * @example\r\n * ```typescript\r\n * const broker = brokerRegistry.register({\r\n * name: \"primary\",\r\n * driver: myDriver,\r\n * isDefault: true,\r\n * });\r\n * ```\r\n */\r\n public register(options: BrokerOptions): Broker {\r\n const broker = new Broker(options);\r\n this.sources.set(broker.name, broker);\r\n\r\n const isNewDefault = broker.isDefault || !this.defaultSource;\r\n\r\n if (isNewDefault) {\r\n this.defaultSource = broker;\r\n }\r\n\r\n // Emit registration events\r\n this.events.emit(\"registered\", broker);\r\n\r\n if (isNewDefault) {\r\n this.events.emit(\"default-registered\", broker);\r\n }\r\n\r\n // Forward driver events to registry\r\n broker.driver.on(\"connected\", () => {\r\n this.events.emit(\"connected\", broker);\r\n });\r\n\r\n broker.driver.on(\"disconnected\", () => {\r\n this.events.emit(\"disconnected\", broker);\r\n });\r\n\r\n return broker;\r\n }\r\n\r\n /**\r\n * Clear all registered brokers\r\n */\r\n public clear(): void {\r\n this.defaultSource = undefined;\r\n this.sources.clear();\r\n }\r\n\r\n /**\r\n * Listen for registry events\r\n *\r\n * @param event - Event to listen for\r\n * @param listener - Callback function\r\n *\r\n * @example\r\n * ```typescript\r\n * brokerRegistry.on(\"registered\", (comm) => {\r\n * console.log(`Broker \"${comm.name}\" registered`);\r\n * });\r\n *\r\n * brokerRegistry.on(\"connected\", (comm) => {\r\n * console.log(`Broker \"${comm.name}\" connected`);\r\n * });\r\n * ```\r\n */\r\n public on(event: BrokerRegistryEvent, listener: BrokerRegistryListener): void {\r\n this.events.on(event, listener);\r\n }\r\n\r\n /**\r\n * Listen for a registry event once\r\n *\r\n * @param event - Event to listen for\r\n * @param listener - Callback function\r\n */\r\n public once(event: BrokerRegistryEvent, listener: BrokerRegistryListener): void {\r\n this.events.once(event, listener);\r\n }\r\n\r\n /**\r\n * Remove an event listener\r\n *\r\n * @param event - Event to stop listening for\r\n * @param listener - Callback to remove\r\n */\r\n public off(event: BrokerRegistryEvent, listener: BrokerRegistryListener): void {\r\n this.events.off(event, listener);\r\n }\r\n\r\n /**\r\n * Get a broker by name or the default one\r\n *\r\n * @param name - Optional broker name\r\n * @returns Broker instance\r\n * @throws MissingBrokerError if not found\r\n *\r\n * @example\r\n * ```typescript\r\n * // Get default broker\r\n * const comm = brokerRegistry.get();\r\n *\r\n * // Get specific broker\r\n * const analytics = brokerRegistry.get(\"analytics\");\r\n * ```\r\n */\r\n public get(name?: string): Broker {\r\n if (name !== undefined) {\r\n const source = this.sources.get(name);\r\n if (!source) {\r\n throw new MissingBrokerError(`Broker \"${name}\" is not registered.`, name);\r\n }\r\n return source;\r\n }\r\n\r\n if (!this.defaultSource) {\r\n throw new MissingBrokerError(\"No default broker registered.\");\r\n }\r\n\r\n return this.defaultSource;\r\n }\r\n\r\n /**\r\n * Check if a broker exists\r\n *\r\n * @param name - Broker name to check\r\n * @returns True if exists\r\n */\r\n public has(name: string): boolean {\r\n return this.sources.has(name);\r\n }\r\n\r\n /**\r\n * Check if any brokers are registered\r\n */\r\n public hasAny(): boolean {\r\n return this.sources.size > 0;\r\n }\r\n\r\n /**\r\n * Get all registered brokers\r\n *\r\n * @returns Array of all brokers\r\n *\r\n * @example\r\n * ```typescript\r\n * // Disconnect all brokers\r\n * for (const comm of brokerRegistry.getAll()) {\r\n * await comm.disconnect();\r\n * }\r\n * ```\r\n */\r\n public getAll(): Broker[] {\r\n return Array.from(this.sources.values());\r\n }\r\n\r\n /**\r\n * Get all broker names\r\n *\r\n * @returns Array of broker names\r\n */\r\n public getNames(): string[] {\r\n return Array.from(this.sources.keys());\r\n }\r\n\r\n /**\r\n * Get the default broker (if any)\r\n *\r\n * @returns Default broker or undefined\r\n */\r\n public getDefault(): Broker | undefined {\r\n return this.defaultSource;\r\n }\r\n}\r\n\r\n/**\r\n * Global broker registry instance\r\n */\r\nexport const brokerRegistry = new BrokerRegistry();\r\n","import type { MessageHandler } from \"./../types\";\nimport { EventConsumerClass } from \"./types\";\n\nexport function prepareConsumerSubscription(\n Consumer: EventConsumerClass,\n onError?: (error: unknown, consumerName: string) => void,\n) {\n const callback: MessageHandler<any> = async (message, ctx) => {\n const envelope = message.payload;\n let eventPayload = envelope.payload;\n\n if (envelope.version) {\n if (!Consumer.isAcceptedVersion(Number(envelope.version))) {\n ctx.ack(); // Acknowledge but don't process\n return;\n }\n }\n\n const consumer = new Consumer();\n\n if (consumer.schema) {\n const result = await consumer.validate(eventPayload);\n if (!result || result.isValid === false) {\n ctx.nack();\n return;\n }\n\n eventPayload = result.data;\n }\n try {\n await consumer.handle(eventPayload, {\n payload: eventPayload,\n eventName: Consumer.eventName,\n messageId: message.metadata.messageId!,\n occurredAt: envelope.occurredAt ? new Date(envelope.occurredAt) : undefined,\n metadata: envelope.metadata,\n version: envelope.version,\n message,\n });\n ctx.ack(); // Auto-ack on success?\n } catch (error) {\n ctx.nack(true); // Requeue on failure\n if (onError) {\n onError(error, Consumer.eventName);\n }\n }\n };\n\n return callback;\n}\n","import { v } from \"@warlock.js/seal\";\r\nimport { randomUUID } from \"node:crypto\";\r\nimport type { ChannelContract } from \"../../contracts\";\r\nimport type {\r\n ChannelOptions,\r\n ChannelStats,\r\n Message,\r\n MessageContext,\r\n MessageHandler,\r\n MessageMetadata,\r\n PublishOptions,\r\n RequestOptions,\r\n ResponseHandler,\r\n SubscribeOptions,\r\n Subscription,\r\n} from \"../../types\";\r\n\r\n/**\r\n * RabbitMQ Channel Implementation\r\n *\r\n * Wraps a RabbitMQ queue/exchange with a unified API.\r\n *\r\n * @template TPayload - The typed payload\r\n */\r\nexport class RabbitMQChannel<TPayload = unknown> implements ChannelContract<TPayload> {\r\n public readonly name: string;\r\n public readonly options: ChannelOptions<TPayload>;\r\n\r\n private readonly amqpChannel: any;\r\n private readonly subscriptions = new Map<string, RabbitMQSubscription>();\r\n private asserted = false;\r\n\r\n /**\r\n * Create a new RabbitMQ channel\r\n */\r\n public constructor(name: string, amqpChannel: any, options?: ChannelOptions<TPayload>) {\r\n this.name = name;\r\n this.amqpChannel = amqpChannel;\r\n this.options = options ?? {};\r\n }\r\n\r\n /**\r\n * Assert the queue exists\r\n */\r\n public async assert(): Promise<void> {\r\n if (this.asserted) return;\r\n\r\n const queueOptions = {\r\n durable: this.options.durable ?? true,\r\n autoDelete: this.options.autoDelete ?? false,\r\n exclusive: this.options.exclusive ?? false,\r\n messageTtl: this.options.messageTtl,\r\n maxLength: this.options.maxLength,\r\n deadLetterExchange: this.options.deadLetter?.channel ? \"\" : undefined,\r\n deadLetterRoutingKey: this.options.deadLetter?.channel,\r\n };\r\n\r\n await this.amqpChannel.assertQueue(this.name, queueOptions);\r\n this.asserted = true;\r\n }\r\n\r\n /**\r\n * Publish a message\r\n */\r\n public async publish(payload: TPayload, options?: PublishOptions): Promise<void> {\r\n await this.assert();\r\n\r\n // Validate with schema if provided\r\n if (this.options.schema) {\r\n const context = {\r\n allValues: payload,\r\n value: payload,\r\n };\r\n const result = await v.validate(this.options.schema, payload, { context });\r\n if (!result.isValid) {\r\n throw new Error(`Message validation failed: ${JSON.stringify(result.errors)}`);\r\n }\r\n payload = result.data as TPayload;\r\n }\r\n\r\n const messageId = randomUUID();\r\n const timestamp = new Date();\r\n\r\n const messageContent = JSON.stringify({\r\n payload,\r\n metadata: {\r\n messageId,\r\n timestamp: timestamp.toISOString(),\r\n correlationId: options?.correlationId,\r\n headers: options?.headers,\r\n },\r\n });\r\n\r\n const publishOptions: any = {\r\n persistent: options?.persistent ?? true,\r\n messageId,\r\n timestamp: timestamp.getTime(),\r\n correlationId: options?.correlationId,\r\n expiration: options?.expiration?.toString(),\r\n priority: options?.priority,\r\n headers: options?.headers,\r\n };\r\n\r\n // Handle delayed messages (requires rabbitmq-delayed-message-exchange plugin)\r\n if (options?.delay) {\r\n publishOptions.headers = {\r\n ...publishOptions.headers,\r\n \"x-delay\": options.delay,\r\n };\r\n }\r\n\r\n this.amqpChannel.sendToQueue(this.name, Buffer.from(messageContent), publishOptions);\r\n }\r\n\r\n /**\r\n * Publish multiple messages\r\n */\r\n public async publishBatch(messages: TPayload[], options?: PublishOptions): Promise<void> {\r\n for (const payload of messages) {\r\n await this.publish(payload, options);\r\n }\r\n }\r\n\r\n /**\r\n * Subscribe to messages\r\n *\r\n * Smart auto-ack behavior (when autoAck is not true):\r\n * - If handler completes successfully without explicit ack/nack/reject → auto-ack\r\n * - If handler throws an error → auto-nack (with retry if configured)\r\n * - If handler explicitly calls ack/nack/reject → respects that call\r\n */\r\n public async subscribe(\r\n handler: MessageHandler<TPayload>,\r\n options?: SubscribeOptions,\r\n ): Promise<Subscription> {\r\n await this.assert();\r\n\r\n // Use consumerId from options if provided, otherwise generate a random one\r\n const subscriptionId = options?.consumerId ?? randomUUID();\r\n\r\n // Set prefetch if specified\r\n if (options?.prefetch) {\r\n await this.amqpChannel.prefetch(options.prefetch);\r\n }\r\n\r\n // If autoAck is true, RabbitMQ handles ack immediately (fire-and-forget)\r\n const isFireAndForget = options?.autoAck === true;\r\n\r\n const consumerOptions = {\r\n noAck: isFireAndForget,\r\n exclusive: options?.exclusive ?? false,\r\n consumerTag: options?.group ?? subscriptionId,\r\n };\r\n\r\n const { consumerTag } = await this.amqpChannel.consume(\r\n this.name,\r\n async (msg: any) => {\r\n if (!msg) return;\r\n\r\n // Track if acknowledgment was handled explicitly\r\n let ackHandled = isFireAndForget;\r\n\r\n try {\r\n const content = JSON.parse(msg.content.toString());\r\n let payload = content.payload as TPayload;\r\n\r\n // Validate with schema if provided\r\n if (this.options.schema) {\r\n const context = {\r\n allValues: payload,\r\n value: payload,\r\n };\r\n const result = await v.validate(this.options.schema, payload, { context });\r\n if (!result.isValid) {\r\n // Reject invalid messages\r\n this.amqpChannel.nack(msg, false, false);\r\n return;\r\n }\r\n payload = result.data as TPayload;\r\n }\r\n\r\n const metadata: MessageMetadata = {\r\n messageId: msg.properties.messageId || content.metadata?.messageId || randomUUID(),\r\n timestamp: new Date(msg.properties.timestamp || content.metadata?.timestamp),\r\n correlationId: msg.properties.correlationId || content.metadata?.correlationId,\r\n replyTo: msg.properties.replyTo,\r\n priority: msg.properties.priority,\r\n headers: msg.properties.headers,\r\n retryCount: msg.properties.headers?.[\"x-retry-count\"] || 0,\r\n originalChannel: this.name,\r\n };\r\n\r\n const message: Message<TPayload> = {\r\n metadata,\r\n payload,\r\n raw: msg,\r\n };\r\n\r\n const context: MessageContext = {\r\n ack: async () => {\r\n if (!ackHandled) {\r\n ackHandled = true;\r\n this.amqpChannel.ack(msg);\r\n }\r\n },\r\n nack: async (requeue = true) => {\r\n if (!ackHandled) {\r\n ackHandled = true;\r\n this.amqpChannel.nack(msg, false, requeue);\r\n }\r\n },\r\n reject: async () => {\r\n if (!ackHandled) {\r\n ackHandled = true;\r\n this.amqpChannel.reject(msg, false);\r\n }\r\n },\r\n reply: async <T>(replyPayload: T) => {\r\n if (msg.properties.replyTo) {\r\n const replyContent = JSON.stringify({\r\n payload: replyPayload,\r\n metadata: {\r\n messageId: randomUUID(),\r\n timestamp: new Date().toISOString(),\r\n correlationId: msg.properties.correlationId,\r\n },\r\n });\r\n\r\n this.amqpChannel.sendToQueue(msg.properties.replyTo, Buffer.from(replyContent), {\r\n correlationId: msg.properties.correlationId,\r\n });\r\n }\r\n },\r\n retry: async (delay?: number) => {\r\n if (ackHandled) return;\r\n ackHandled = true;\r\n\r\n const retryCount = (metadata.retryCount || 0) + 1;\r\n const maxRetries = options?.retry?.maxRetries ?? 3;\r\n\r\n if (retryCount > maxRetries) {\r\n // Send to dead-letter if configured\r\n if (options?.deadLetter) {\r\n await this.sendToDeadLetter(message, options.deadLetter.channel);\r\n }\r\n this.amqpChannel.ack(msg);\r\n return;\r\n }\r\n\r\n // Republish with retry count\r\n const headers = {\r\n ...msg.properties.headers,\r\n \"x-retry-count\": retryCount,\r\n };\r\n\r\n if (delay) {\r\n headers[\"x-delay\"] = delay;\r\n }\r\n\r\n this.amqpChannel.sendToQueue(this.name, msg.content, { ...msg.properties, headers });\r\n\r\n this.amqpChannel.ack(msg);\r\n },\r\n };\r\n\r\n // Execute handler\r\n await handler(message, context);\r\n\r\n // Smart auto-ack: if handler succeeded and didn't explicitly handle ack\r\n if (!ackHandled) {\r\n this.amqpChannel.ack(msg);\r\n }\r\n } catch (error) {\r\n // Smart auto-nack: if handler threw and didn't explicitly handle ack\r\n if (ackHandled) return;\r\n\r\n // Handle errors - nack and potentially retry\r\n if (options?.retry) {\r\n const retryCount = msg.properties.headers?.[\"x-retry-count\"] || 0;\r\n if (retryCount < options.retry.maxRetries) {\r\n // Requeue for retry\r\n this.amqpChannel.nack(msg, false, true);\r\n } else if (options.deadLetter) {\r\n // Send to dead-letter\r\n this.amqpChannel.nack(msg, false, false);\r\n } else {\r\n this.amqpChannel.reject(msg, false);\r\n }\r\n } else {\r\n // No retry configured - reject without requeue\r\n this.amqpChannel.nack(msg, false, false);\r\n }\r\n }\r\n },\r\n consumerOptions,\r\n );\r\n\r\n const subscription = new RabbitMQSubscription(\r\n subscriptionId,\r\n this.name,\r\n consumerTag,\r\n this.amqpChannel,\r\n );\r\n\r\n this.subscriptions.set(subscriptionId, subscription);\r\n\r\n return subscription;\r\n }\r\n\r\n /**\r\n * Unsubscribe by consumer ID\r\n */\r\n public async unsubscribeById(consumerId: string): Promise<void> {\r\n const subscription = this.subscriptions.get(consumerId);\r\n if (subscription) {\r\n await subscription.unsubscribe();\r\n this.subscriptions.delete(consumerId);\r\n }\r\n }\r\n\r\n /**\r\n * Stop consuming messages on this channel.\r\n * Cancels all active subscriptions gracefully.\r\n */\r\n public async stopConsuming(): Promise<void> {\r\n const cancellations = Array.from(this.subscriptions.values()).map(sub =>\r\n sub.unsubscribe(),\r\n );\r\n await Promise.all(cancellations);\r\n }\r\n\r\n /**\r\n * Send message to dead-letter queue\r\n */\r\n private async sendToDeadLetter(\r\n message: Message<TPayload>,\r\n deadLetterChannel: string,\r\n ): Promise<void> {\r\n const content = JSON.stringify({\r\n payload: message.payload,\r\n metadata: {\r\n ...message.metadata,\r\n originalChannel: this.name,\r\n },\r\n });\r\n\r\n this.amqpChannel.sendToQueue(deadLetterChannel, Buffer.from(content), { persistent: true });\r\n }\r\n\r\n /**\r\n * Request-response pattern\r\n */\r\n public async request<TResponse = unknown>(\r\n payload: TPayload,\r\n options?: RequestOptions,\r\n ): Promise<TResponse> {\r\n await this.assert();\r\n\r\n const correlationId = randomUUID();\r\n const timeout = options?.timeout ?? 30000;\r\n\r\n // Create exclusive reply queue\r\n const { queue: replyQueue } = await this.amqpChannel.assertQueue(\"\", {\r\n exclusive: true,\r\n autoDelete: true,\r\n });\r\n\r\n return new Promise<TResponse>((resolve, reject) => {\r\n const timeoutId = setTimeout(() => {\r\n reject(new Error(`Request timeout after ${timeout}ms`));\r\n }, timeout);\r\n\r\n // Consume reply\r\n this.amqpChannel.consume(\r\n replyQueue,\r\n (msg: any) => {\r\n if (msg?.properties.correlationId === correlationId) {\r\n clearTimeout(timeoutId);\r\n const content = JSON.parse(msg.content.toString());\r\n resolve(content.payload as TResponse);\r\n }\r\n },\r\n { noAck: true },\r\n );\r\n\r\n // Send request\r\n const messageContent = JSON.stringify({\r\n payload,\r\n metadata: {\r\n messageId: randomUUID(),\r\n timestamp: new Date().toISOString(),\r\n correlationId,\r\n },\r\n });\r\n\r\n this.amqpChannel.sendToQueue(this.name, Buffer.from(messageContent), {\r\n correlationId,\r\n replyTo: replyQueue,\r\n expiration: timeout.toString(),\r\n ...options,\r\n });\r\n });\r\n }\r\n\r\n /**\r\n * Register response handler for RPC\r\n */\r\n public async respond<TResponse = unknown>(\r\n handler: ResponseHandler<TPayload, TResponse>,\r\n ): Promise<Subscription> {\r\n return this.subscribe(async (message, ctx) => {\r\n const response = await handler(message, ctx);\r\n await ctx.reply(response);\r\n await ctx.ack();\r\n });\r\n }\r\n\r\n /**\r\n * Get queue statistics\r\n */\r\n public async stats(): Promise<ChannelStats> {\r\n await this.assert();\r\n\r\n const queueInfo = await this.amqpChannel.checkQueue(this.name);\r\n\r\n return {\r\n name: this.name,\r\n messageCount: queueInfo.messageCount,\r\n consumerCount: queueInfo.consumerCount,\r\n };\r\n }\r\n\r\n /**\r\n * Purge all messages\r\n */\r\n public async purge(): Promise<number> {\r\n await this.assert();\r\n\r\n const result = await this.amqpChannel.purgeQueue(this.name);\r\n return result.messageCount;\r\n }\r\n\r\n /**\r\n * Check if queue exists\r\n */\r\n public async exists(): Promise<boolean> {\r\n try {\r\n await this.amqpChannel.checkQueue(this.name);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Delete the queue\r\n */\r\n public async delete(): Promise<void> {\r\n // Cancel all subscriptions\r\n for (const subscription of this.subscriptions.values()) {\r\n await subscription.unsubscribe();\r\n }\r\n this.subscriptions.clear();\r\n\r\n try {\r\n await this.amqpChannel.deleteQueue(this.name);\r\n } catch {\r\n // Ignore if queue doesn't exist\r\n }\r\n\r\n this.asserted = false;\r\n }\r\n}\r\n\r\n/**\r\n * RabbitMQ Subscription Implementation\r\n */\r\nclass RabbitMQSubscription implements Subscription {\r\n public readonly id: string;\r\n public readonly channel: string;\r\n public readonly consumerTag: string;\r\n\r\n private readonly amqpChannel: any;\r\n private _isActive = true;\r\n\r\n public constructor(id: string, channel: string, consumerTag: string, amqpChannel: any) {\r\n this.id = id;\r\n this.channel = channel;\r\n this.consumerTag = consumerTag;\r\n this.amqpChannel = amqpChannel;\r\n }\r\n\r\n public async unsubscribe(): Promise<void> {\r\n if (!this._isActive) return;\r\n\r\n await this.amqpChannel.cancel(this.consumerTag);\r\n this._isActive = false;\r\n }\r\n\r\n public async pause(): Promise<void> {\r\n // RabbitMQ doesn't have native pause, cancel consumer\r\n await this.amqpChannel.cancel(this.consumerTag);\r\n }\r\n\r\n public async resume(): Promise<void> {\r\n // Would need to re-subscribe - not directly supported\r\n throw new Error(\"Resume is not supported for RabbitMQ. Please create a new subscription.\");\r\n }\r\n\r\n public isActive(): boolean {\r\n return this._isActive;\r\n }\r\n}\r\n","import { EventEmitter } from \"node:events\";\r\nimport type { BrokerDriverContract, ChannelContract } from \"../../contracts\";\r\nimport { EventMessage } from \"../../message-managers/event-message\";\r\nimport { EventConsumerClass } from \"../../message-managers/types\";\r\nimport type {\r\n BrokerDriverType,\r\n BrokerEvent,\r\n BrokerEventListener,\r\n ChannelOptions,\r\n HealthCheckResult,\r\n RabbitMQConnectionOptions,\r\n} from \"../../types\";\r\nimport { prepareConsumerSubscription } from \"./../../message-managers/prepare-consumer-subscription\";\r\nimport { RabbitMQChannel } from \"./rabbitmq-channel\";\r\n\r\n// ============================================================\r\n// Lazy-loaded amqplib Module\r\n// ============================================================\r\n\r\n/**\r\n * Shape of the lazily-imported amqplib module\r\n */\r\ntype AmqplibModule = typeof import(\"amqplib\");\r\n\r\n/**\r\n * The single amqplib load, shared by every caller.\r\n *\r\n * Memoized as a promise rather than as a resolved value so the loader stays\r\n * idempotent: without it, two callers arriving before the first `import()`\r\n * settles would each start their own load and the last writer would win, so a\r\n * caller could end up observing a module instance it never awaited.\r\n *\r\n * Resolves to `undefined` when amqplib is not installed.\r\n */\r\nlet amqplibModulePromise: Promise<AmqplibModule | undefined> | undefined;\r\n\r\n/**\r\n * Installation instructions for amqplib\r\n */\r\nconst AMQPLIB_INSTALL_INSTRUCTIONS = `\r\nRabbitMQ driver requires the amqplib package.\r\nInstall it with:\r\n\r\n npx warlock add herald --driver=rabbitmq\r\n\r\nOr manually:\r\n\r\n npm install amqplib\r\n pnpm add amqplib\r\n yarn add amqplib\r\n`.trim();\r\n\r\n/**\r\n * Load amqplib, reusing the single shared load for every caller.\r\n *\r\n * @returns The amqplib module, or `undefined` when it is not installed.\r\n */\r\nfunction loadAmqplibModule(): Promise<AmqplibModule | undefined> {\r\n if (!amqplibModulePromise) {\r\n amqplibModulePromise = import(\"amqplib\").catch(() => undefined);\r\n }\r\n\r\n return amqplibModulePromise;\r\n}\r\n\r\n// ============================================================\r\n// RabbitMQ Driver\r\n// ============================================================\r\n\r\n/**\r\n * RabbitMQ Driver\r\n *\r\n * Implementation of BrokerDriverContract for RabbitMQ/AMQP.\r\n *\r\n * **Important:** This driver requires the `amqplib` package to be installed.\r\n * Install it with: `npx warlock add herald --driver=rabbitmq` or `npm install amqplib`\r\n *\r\n * @example\r\n * ```typescript\r\n * const driver = new RabbitMQDriver({\r\n * driver: \"rabbitmq\",\r\n * host: \"localhost\",\r\n * port: 5672,\r\n * username: \"guest\",\r\n * password: \"guest\",\r\n * });\r\n *\r\n * await driver.connect();\r\n * const channel = driver.channel(\"user.created\");\r\n * ```\r\n */\r\nexport class RabbitMQDriver implements BrokerDriverContract {\r\n public readonly name = \"rabbitmq\" as const;\r\n\r\n public readonly consumers: EventConsumerClass[] = [];\r\n\r\n private readonly options: RabbitMQConnectionOptions;\r\n private readonly events = new EventEmitter();\r\n private readonly channels = new Map<string, ChannelContract<any>>();\r\n\r\n private connection: any = null;\r\n private amqpChannel: any = null;\r\n private _isConnected = false;\r\n\r\n /**\r\n * Create a new RabbitMQ driver\r\n *\r\n * @param options - RabbitMQ connection options\r\n */\r\n public constructor(options: RabbitMQConnectionOptions) {\r\n this.options = options;\r\n }\r\n\r\n /**\r\n * Whether connected to RabbitMQ\r\n */\r\n public get isConnected(): boolean {\r\n return this._isConnected;\r\n }\r\n\r\n /**\r\n * Subscribe the given consumer class to the driver\r\n *\r\n * @param consumer - Consumer class to subscribe\r\n *\r\n * @example\r\n * ```typescript\r\n * driver.subscribe(UserUpdatedConsumer);\r\n * ```\r\n */\r\n public subscribe(Consumer: EventConsumerClass) {\r\n if (this.isConnected) {\r\n this.channel(Consumer.eventName).subscribe(\r\n prepareConsumerSubscription(Consumer, (error, eventName) => {\r\n this.events.emit(\"error\", error, eventName);\r\n }),\r\n {\r\n consumerId: Consumer.consumerId,\r\n },\r\n );\r\n } else {\r\n this.consumers.push(Consumer);\r\n }\r\n\r\n return () => {\r\n this.unsubscribe(Consumer);\r\n };\r\n }\r\n\r\n /**\r\n * Unsubscribe the given consumer\r\n */\r\n public unsubscribe(Consumer: EventConsumerClass): void {\r\n if (this.isConnected) {\r\n this.channel(Consumer.eventName).unsubscribeById(Consumer.consumerId);\r\n }\r\n const index = this.consumers.indexOf(Consumer);\r\n if (index > -1) {\r\n this.consumers.splice(index, 1);\r\n }\r\n }\r\n\r\n /**\r\n * Publish the given event message.\r\n * Auto-creates the channel if it hasn't been accessed before.\r\n */\r\n public publish<TPayload = Record<string, any>>(event: EventMessage<TPayload>): void {\r\n this.channel(event.eventName).publish(event.serialize());\r\n }\r\n\r\n /**\r\n * Connect to RabbitMQ\r\n */\r\n public async connect(): Promise<void> {\r\n const amqplib = await loadAmqplibModule();\r\n\r\n if (!amqplib) {\r\n throw new Error(`amqplib is not installed.\\n\\n${AMQPLIB_INSTALL_INSTRUCTIONS}`);\r\n }\r\n\r\n try {\r\n // Build connection URL\r\n const url = this.buildConnectionUrl();\r\n\r\n // Build connection options merging our options with native client options\r\n const connectOptions = {\r\n heartbeat: this.options.heartbeat ?? 60,\r\n timeout: this.options.connectionTimeout,\r\n // Merge native amqplib client options\r\n ...this.options.clientOptions,\r\n };\r\n\r\n // Connect using cached module\r\n this.connection = await amqplib.connect(url, connectOptions);\r\n\r\n // Create channel\r\n this.amqpChannel = await this.connection.createChannel();\r\n\r\n // Set prefetch if specified\r\n if (this.options.prefetch) {\r\n await this.amqpChannel.prefetch(this.options.prefetch);\r\n }\r\n\r\n this._isConnected = true;\r\n this.events.emit(\"connected\");\r\n\r\n for (const consumer of this.consumers) {\r\n this.subscribe(consumer);\r\n }\r\n\r\n this.consumers.length = 0;\r\n\r\n // Handle connection close\r\n this.connection.on(\"close\", () => {\r\n this._isConnected = false;\r\n this.events.emit(\"disconnected\");\r\n\r\n if (this.options.reconnect !== false) {\r\n this.handleReconnect();\r\n }\r\n });\r\n\r\n // Handle errors\r\n this.connection.on(\"error\", (error: Error) => {\r\n this.events.emit(\"error\", error);\r\n });\r\n } catch (error) {\r\n this._isConnected = false;\r\n throw new Error(\r\n `Failed to connect to RabbitMQ: ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n }\r\n }\r\n\r\n /**\r\n * Build connection URL from options\r\n */\r\n private buildConnectionUrl(): string {\r\n if (this.options.uri) {\r\n return this.options.uri;\r\n }\r\n\r\n const protocol = \"amqp\";\r\n const host = this.options.host ?? \"localhost\";\r\n const port = this.options.port ?? 5672;\r\n const vhost = this.options.vhost ?? \"/\";\r\n const username = this.options.username ?? \"guest\";\r\n const password = this.options.password ?? \"guest\";\r\n\r\n const encodedVhost = encodeURIComponent(vhost);\r\n\r\n return `${protocol}://${username}:${password}@${host}:${port}/${encodedVhost}`;\r\n }\r\n\r\n /**\r\n * Handle reconnection\r\n */\r\n private async handleReconnect(): Promise<void> {\r\n const delay = this.options.reconnectDelay ?? 5000;\r\n let attempt = 0;\r\n\r\n const tryReconnect = async () => {\r\n attempt++;\r\n this.events.emit(\"reconnecting\", attempt);\r\n\r\n try {\r\n await this.connect();\r\n } catch {\r\n setTimeout(tryReconnect, delay);\r\n }\r\n };\r\n\r\n setTimeout(tryReconnect, delay);\r\n }\r\n\r\n /**\r\n * Disconnect from RabbitMQ\r\n */\r\n public async disconnect(): Promise<void> {\r\n if (this.amqpChannel) {\r\n try {\r\n await this.amqpChannel.close();\r\n } catch {\r\n // Ignore close errors\r\n }\r\n this.amqpChannel = null;\r\n }\r\n\r\n if (this.connection) {\r\n try {\r\n await this.connection.close();\r\n } catch {\r\n // Ignore close errors\r\n }\r\n this.connection = null;\r\n }\r\n\r\n this._isConnected = false;\r\n this.events.emit(\"disconnected\");\r\n }\r\n\r\n /**\r\n * Register event listener\r\n */\r\n public on(event: BrokerEvent, listener: BrokerEventListener): void {\r\n this.events.on(event, listener as any);\r\n }\r\n\r\n /**\r\n * Remove event listener\r\n */\r\n public off(event: BrokerEvent, listener: BrokerEventListener): void {\r\n this.events.off(event, listener as any);\r\n }\r\n\r\n /**\r\n * Get or create a channel\r\n */\r\n public channel<TPayload = unknown>(\r\n name: string,\r\n options?: ChannelOptions<TPayload>,\r\n ): ChannelContract<TPayload> {\r\n // Check cache\r\n const existing = this.channels.get(name);\r\n if (existing) {\r\n return existing as ChannelContract<TPayload>;\r\n }\r\n\r\n // Create new channel\r\n const channel = new RabbitMQChannel<TPayload>(name, this.amqpChannel, options);\r\n\r\n this.channels.set(name, channel);\r\n return channel;\r\n }\r\n\r\n /**\r\n * Start consuming messages\r\n */\r\n public async startConsuming(): Promise<void> {\r\n // Channels start consuming when subscribe() is called\r\n // This method is for batch start if needed\r\n }\r\n\r\n /**\r\n * Stop consuming messages from all subscribed channels.\r\n * Gracefully cancels all active consumers.\r\n */\r\n public async stopConsuming(): Promise<void> {\r\n const stops = Array.from(this.channels.values()).map(channel =>\r\n (channel as RabbitMQChannel<any>).stopConsuming(),\r\n );\r\n await Promise.all(stops);\r\n }\r\n\r\n /**\r\n * Health check\r\n */\r\n public async healthCheck(): Promise<HealthCheckResult> {\r\n if (!this._isConnected || !this.connection) {\r\n return {\r\n healthy: false,\r\n error: \"Not connected to RabbitMQ\",\r\n };\r\n }\r\n\r\n const start = Date.now();\r\n\r\n try {\r\n // Simple check - verify channel is open\r\n await this.amqpChannel.checkQueue(\"amq.rabbitmq.reply-to\").catch(() => {\r\n // Queue might not exist, but if we get here, connection is alive\r\n });\r\n\r\n return {\r\n healthy: true,\r\n latency: Date.now() - start,\r\n };\r\n } catch (error) {\r\n return {\r\n healthy: false,\r\n error: error instanceof Error ? error.message : String(error),\r\n latency: Date.now() - start,\r\n };\r\n }\r\n }\r\n\r\n /**\r\n * Get all channel names\r\n */\r\n public getChannelNames(): string[] {\r\n return Array.from(this.channels.keys());\r\n }\r\n\r\n /**\r\n * Close a specific channel\r\n */\r\n public async closeChannel(name: string): Promise<void> {\r\n const channel = this.channels.get(name);\r\n if (channel) {\r\n await channel.delete();\r\n this.channels.delete(name);\r\n }\r\n }\r\n\r\n /**\r\n * Get the raw AMQP channel (for advanced use)\r\n */\r\n public getRawChannel(): any {\r\n return this.amqpChannel;\r\n }\r\n\r\n /**\r\n * Get the raw connection (for advanced use)\r\n */\r\n public getRawConnection(): any {\r\n return this.connection;\r\n }\r\n}\r\n","import { Broker, brokerRegistry } from \"../communicators\";\r\nimport type { BrokerDriverContract, ChannelContract } from \"../contracts\";\r\nimport { EventConsumerClass, EventMessage } from \"../message-managers\";\r\nimport type { ChannelOptions, ConnectionOptions, RabbitMQConnectionOptions } from \"../types\";\r\n\r\n/**\r\n * Connect to a message broker and register it.\r\n *\r\n * This is a high-level utility function that simplifies connection setup\r\n * for most projects. It handles driver instantiation, connection,\r\n * broker creation, and automatic registration.\r\n *\r\n * **Supported Drivers:**\r\n * - `rabbitmq` (default) - RabbitMQ/AMQP driver\r\n * - `kafka` - Apache Kafka driver (coming soon)\r\n *\r\n * @param options - Connection configuration options\r\n * @returns A connected and registered Broker instance\r\n * @throws {Error} If connection fails or driver is not implemented\r\n *\r\n * @example\r\n * ```typescript\r\n * // RabbitMQ connection\r\n * const broker = await connectToBroker({\r\n * driver: \"rabbitmq\",\r\n * host: \"localhost\",\r\n * port: 5672,\r\n * username: \"guest\",\r\n * password: \"guest\",\r\n * });\r\n *\r\n * // Use the broker\r\n * await broker.channel(\"user.created\").publish({ userId: 1 });\r\n * ```\r\n *\r\n * @example\r\n * ```typescript\r\n * // Multiple brokers\r\n * await connectToBroker({\r\n * driver: \"rabbitmq\",\r\n * name: \"notifications\",\r\n * isDefault: true,\r\n * host: process.env.RABBITMQ_HOST,\r\n * });\r\n *\r\n * await connectToBroker({\r\n * driver: \"rabbitmq\",\r\n * name: \"analytics\",\r\n * host: process.env.ANALYTICS_RABBITMQ_HOST,\r\n * });\r\n *\r\n * // Use default broker\r\n * herald().channel(\"notifications\").publish({ ... });\r\n *\r\n * // Use specific broker\r\n * herald(\"analytics\").channel(\"events\").publish({ ... });\r\n * ```\r\n */\r\nexport async function connectToBroker(options: ConnectionOptions): Promise<Broker> {\r\n // Default values\r\n const driverType = options.driver ?? \"rabbitmq\";\r\n const brokerName = options.name ?? \"default\";\r\n const isDefault = options.isDefault ?? true;\r\n\r\n // Create driver based on type\r\n let driver: BrokerDriverContract;\r\n\r\n switch (driverType) {\r\n case \"rabbitmq\": {\r\n const rabbitOptions = options as RabbitMQConnectionOptions;\r\n // Dynamic import to avoid requiring amqplib if not used\r\n const { RabbitMQDriver } = await import(\"../drivers/rabbitmq/rabbitmq-driver\");\r\n driver = new RabbitMQDriver(rabbitOptions);\r\n break;\r\n }\r\n\r\n case \"kafka\": {\r\n // const kafkaOptions = options as KafkaConnectionOptions;\r\n // Dynamic import to avoid requiring kafkajs if not used\r\n throw new Error(\r\n \"Kafka driver is not yet implemented. Coming soon! For now, please use RabbitMQ.\",\r\n );\r\n }\r\n\r\n default:\r\n throw new Error(`Unknown driver: \"${driverType}\". Supported drivers: rabbitmq, kafka`);\r\n }\r\n\r\n // Create broker\r\n const broker = brokerRegistry.register({\r\n name: brokerName,\r\n driver,\r\n isDefault,\r\n });\r\n\r\n // Connect to the message broker\r\n try {\r\n await driver.connect();\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to connect to ${driverType}: ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n }\r\n\r\n return broker;\r\n}\r\n\r\n/**\r\n * Get a broker by name or the default one.\r\n *\r\n * This is the main entry point for using brokers in your application.\r\n * Named after the package — `herald()` carries your messages!\r\n *\r\n * @param name - Optional broker name (uses default if not provided)\r\n * @returns Broker instance\r\n * @throws MissingBrokerError if broker not found\r\n *\r\n * @example\r\n * // Get default broker\r\n * const channel = herald().channel(\"user.created\");\r\n * await channel.publish({ userId: 1 });\r\n *\r\n * // Get specific broker\r\n * const analyticsChannel = herald(\"analytics\").channel(\"events\");\r\n * await analyticsChannel.publish({ event: \"page_view\" });\r\n *\r\n * // Subscribe to messages\r\n * herald()\r\n * .channel<UserPayload>(\"user.created\")\r\n * .subscribe(async (message, ctx) => {\r\n * console.log(\"User created:\", message.payload);\r\n * await ctx.ack();\r\n * });\r\n * ```\r\n */\r\nexport function herald(name?: string): Broker {\r\n return brokerRegistry.get(name);\r\n}\r\n\r\n/**\r\n * Get channel instance for the given name from default broker.\r\n *\r\n * Shorthand for `herald().channel(name, options)`.\r\n *\r\n * @param name - Channel name\r\n * @param options - Optional channel options\r\n * @returns Channel instance\r\n * @throws MissingBrokerError if broker not found\r\n *\r\n * @example\r\n * ```typescript\r\n * const channel = heraldChannel(\"user.created\");\r\n * await channel.publish({ userId: 1 });\r\n * ```\r\n */\r\nexport function heraldChannel<TPayload = unknown>(\r\n name: string,\r\n options?: ChannelOptions<TPayload>,\r\n): ChannelContract<TPayload> {\r\n return herald().channel<TPayload>(name, options);\r\n}\r\n\r\n/**\r\n * Publish an EventMessage to the default broker.\r\n *\r\n * @param event - Event message to publish\r\n * @returns Promise that resolves when the event is published\r\n * @throws Error if the broker is not connected\r\n *\r\n * @example\r\n * ```typescript\r\n * await publishEvent(new UserUpdatedEvent({ id: 1, name: \"John Doe\" }));\r\n * ```\r\n */\r\nexport async function publishEvent<TPayload = Record<string, any>>(event: EventMessage<TPayload>) {\r\n return herald().publish(event);\r\n}\r\n\r\n/**\r\n * Subscribe an EventConsumer class to the default broker.\r\n *\r\n * @param Consumer - Event consumer class\r\n * @returns Unsubscribe function\r\n * @throws MissingBrokerError if broker not found\r\n *\r\n * @example\r\n * ```typescript\r\n * await subscribeConsumer(UserUpdatedConsumer);\r\n * ```\r\n */\r\nexport async function subscribeConsumer<TPayload = Record<string, any>>(\r\n Consumer: EventConsumerClass<TPayload>,\r\n) {\r\n return herald().subscribe(Consumer);\r\n}\r\n","import { brokerRegistry } from \"../communicators\";\nimport { type EventConsumerClass } from \"../message-managers/types\";\n\nexport type ConsumableOptions = {\n broker?: string;\n};\n\nexport const pendingSubscribers = new Set<{\n Consumer: EventConsumerClass;\n options?: ConsumableOptions;\n}>();\n\n/**\n * Register the consumer to the broker\n */\nexport function Consumable(options?: ConsumableOptions) {\n return function (target: EventConsumerClass) {\n const brokerName = options?.broker;\n\n try {\n const currentBroker = brokerRegistry.get(brokerName);\n\n // if broker is connected, subscribe the consumer\n if (currentBroker?.isConnected) {\n currentBroker.subscribe(target);\n } else {\n pendingSubscribers.add({ Consumer: target, options });\n }\n } catch {\n // mostly it will be an error that broker is not registered yet\n // then add it to the pending subscribers\n pendingSubscribers.add({ Consumer: target, options });\n }\n };\n}\n\n// Register pending consumers on broker's connection is done\nbrokerRegistry.on(\"connected\", (broker) => {\n for (const { Consumer, options } of pendingSubscribers) {\n if (options?.broker && broker.name !== options.broker) {\n continue;\n }\n\n broker.subscribe(Consumer);\n }\n});\n","/**\n * This class is used to be part of the Herald Event Consumer Manager.\n * It should be used to consume events from Either RabbitMQ or Kafka through Herald\n *\n * It's highly recommended using it instead of declaring manual channel namd and subscribing to event\n */\nimport { v, ValidationResult, type ObjectValidator } from \"@warlock.js/seal\";\nimport { randomUUID } from \"crypto\";\nimport { Consumable } from \"../decorators\";\nimport { ConsumedEventMessage, EventConsumerClass } from \"./types\";\n\nexport abstract class EventConsumer<Payload = Record<string, any>> {\n /**\n * Event name\n */\n public static eventName: string;\n\n private static _consumerId?: string;\n\n public static get consumerId(): string {\n if (!this._consumerId) {\n this._consumerId = randomUUID();\n }\n return this._consumerId;\n }\n\n public get eventName() {\n return (this.constructor as typeof EventConsumer).eventName;\n }\n\n /**\n * Min version accepted to be consumed by this class\n */\n public static minVersion?: number;\n\n /**\n * Max version accepted to be consumed by this class\n */\n public static maxVersion?: number;\n\n /**\n * Payload validation to auto reject the received event before accessing it in the handle method\n */\n public schema?: ObjectValidator;\n\n /**\n * The method that will be called when the event is received\n */\n public abstract handle(payload: Payload, event: ConsumedEventMessage): Promise<void>;\n\n /**\n * Determine whether this is accepted version to be used by this consumer\n */\n public static isAcceptedVersion(version: number): boolean {\n if (this.minVersion && version < this.minVersion) return false;\n if (this.maxVersion && version > this.maxVersion) return false;\n\n return true;\n }\n\n /**\n * Validate the given data\n */\n public async validate(data: Payload): Promise<ValidationResult | void> {\n if (!this.schema) return;\n\n return await v.validate(this.schema, data);\n }\n}\n\n/**\n * Define Consumer options\n */\ntype ConsumerOptions<Payload> = {\n /**\n * Payload validation to auto reject the received event before accessing it in the handle method\n */\n schema?: ObjectValidator;\n /**\n * Handle data\n */\n handle: (payload: Payload, event: ConsumedEventMessage) => Promise<void>;\n /**\n * Validate the payload before executing `handle`\n */\n validate?: (payload: Payload) => Promise<ValidationResult | boolean>;\n};\n\n/**\n * A shorthand to define an event consumer without declaring an entire class\n */\nexport function defineConsumer<Payload = Record<string, any>>(\n eventName: string,\n options: ConsumerOptions<Payload>,\n): EventConsumerClass {\n const Class = class AnnouncedConsumer extends EventConsumer<Payload> {\n public static eventName = eventName;\n public schema = options.schema;\n\n public async handle(payload: Payload, event: ConsumedEventMessage) {\n if (options.validate) {\n const result = await options.validate(payload);\n if (!result || !(result as ValidationResult).isValid) return;\n }\n\n return options.handle(payload, event);\n }\n };\n\n Consumable()(Class as EventConsumerClass);\n\n return Class as EventConsumerClass;\n}\n","/**\n * This class is used to be part of the Herald Event Message Manager.\n * It should be used to trigger events to Either RabbitMQ or Kafka through Herald\n *\n * It's highly recommended using it instead of declaring manual channel namd and publishing data\n */\nimport { GenericObject } from \"@mongez/reinforcements\";\nimport { type ObjectValidator } from \"@warlock.js/seal\";\nimport { randomUUID } from \"crypto\";\n\nexport abstract class EventMessage<TPayload = Record<string, any>> {\n /**\n * Event Name\n */\n public abstract eventName: string;\n\n /**\n * Event version\n */\n public version?: number;\n\n /**\n * Additional metadata (if any)\n */\n public metadata?: Record<string, any>;\n\n /**\n * Event Message id\n */\n public messageId?: string;\n\n /**\n * Schema of payload that will be used to determine whether this event should be published\n */\n public schema?: ObjectValidator;\n\n /**\n * Data that will be sent with the event (Payload)\n */\n public toJSON(): TPayload {\n if (!this.data) {\n throw new Error(`no Data is defined for Event: ${this.eventName}`);\n }\n\n return this.data as TPayload;\n }\n\n public constructor(protected data?: TPayload) {}\n\n /**\n * Serialize the event to be ready for publishing.\n * Delegates payload resolution to toJSON() — override toJSON() to customize.\n *\n * @throws Error if toJSON() throws (e.g. no data provided)\n */\n public serialize() {\n const payload = this.toJSON();\n\n return {\n payload,\n metadata: this.metadata,\n messageId: this.messageId ?? randomUUID(),\n eventName: this.eventName,\n version: this.version,\n occurredAt: new Date(),\n __through: \"EventMessage\",\n };\n }\n}\n\ntype EventOptions<T> = {\n /**\n * Shapen the data that will be used\n */\n toJSON?: (data: T) => GenericObject;\n /**\n * Validation schema\n */\n schema?: ObjectValidator;\n};\n\n/**\n * Represents an EventMessage class constructor.\n *\n * @template TIncoming - The type of data accepted by the constructor\n * @template TOutgoing - The type of data returned by toJSON() (defaults to TIncoming)\n */\ntype EventMessageClass<TIncoming = Record<string, any>, TOutgoing = TIncoming> = new (\n data?: TIncoming,\n) => EventMessage<TOutgoing>;\n\n/**\n * A shorthand to define an event without declaring an entire class.\n *\n * This factory function creates an EventMessage subclass that transforms\n * input data (IncomingData) into a different output format (OutgoingData).\n *\n * @template IncomingData - The type of data passed to the constructor\n * @template OutgoingData - The type of data returned by toJSON()\n *\n * @example\n * ```typescript\n * const UserCreatedEvent = defineEvent<User, { id: number; name: string }>(\n * \"user.created\",\n * { toJSON: (user) => user.only([\"id\", \"name\"]) }\n * );\n *\n * publishEvent(new UserCreatedEvent(user));\n * ```\n */\nexport function defineEvent<IncomingData = unknown, OutgoingData = unknown>(\n eventName: string,\n options: EventOptions<IncomingData> = {},\n): EventMessageClass<IncomingData, OutgoingData> {\n // We need to use `any` here to bridge the IncomingData -> OutgoingData transformation\n // The class accepts IncomingData in constructor but outputs OutgoingData via toJSON()\n return class AnnouncedEvent extends EventMessage<OutgoingData> {\n public eventName = eventName;\n public schema = options.schema;\n\n public constructor(data?: IncomingData) {\n super(data as any);\n }\n\n public toJSON(): OutgoingData {\n if (!options.toJSON) return this.data as OutgoingData;\n\n return options.toJSON(this.data as IncomingData) as OutgoingData;\n }\n };\n}\n","import { herald } from \"./utils/connect-to-broker\";\n\n/**\n * Minimal mirror of `@warlock.js/core`'s `UseCaseBroadcastEvent`.\n *\n * Kept local on purpose: this adapter is **structurally typed** so `@warlock.js/herald`\n * takes no dependency on `@warlock.js/core`. The shape only needs the fields the\n * adapter reads (`event` for the channel, `payload` for the body).\n */\nexport type UseCaseBroadcastEvent = {\n useCase: string;\n event: string;\n id: string;\n at: Date;\n payload: unknown;\n};\n\n/**\n * Channel adapter that publishes use-case broadcast events onto a herald broker.\n *\n * Register it in the use-cases config so successful use cases fan out to the bus:\n *\n * @example\n * // src/config/use-cases.ts\n * import { heraldBroadcast } from \"@warlock.js/herald\";\n *\n * export default {\n * broadcast: {\n * enabled: true,\n * channels: [heraldBroadcast({ broker: \"default\" })],\n * },\n * } satisfies UseCaseConfigurations;\n *\n * @param options - Optional broker name (defaults to the default broker)\n */\nexport function heraldBroadcast(options?: { broker?: string }) {\n return {\n async broadcast(event: UseCaseBroadcastEvent): Promise<void> {\n await herald(options?.broker).channel(event.event).publish(event.payload);\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["EventEmitter","v","EventEmitter","v"],"sources":["../../../../../../herald/src/communicators/broker.ts","../../../../../../herald/src/communicators/broker-registry.ts","../../../../../../herald/src/message-managers/prepare-consumer-subscription.ts","../../../../../../herald/src/drivers/rabbitmq/rabbitmq-channel.ts","../../../../../../herald/src/drivers/rabbitmq/rabbitmq-driver.ts","../../../../../../herald/src/utils/connect-to-broker.ts","../../../../../../herald/src/decorators/consumable.ts","../../../../../../herald/src/message-managers/event-consumer.ts","../../../../../../herald/src/message-managers/event-message.ts","../../../../../../herald/src/use-case-broadcast.ts"],"sourcesContent":["import type { BrokerDriverContract } from \"../contracts\";\r\nimport type { ChannelContract } from \"../contracts/channel.contract\";\r\nimport { EventMessage } from \"../message-managers/event-message\";\r\nimport { EventConsumerClass } from \"../message-managers/types\";\r\nimport type { ChannelOptions } from \"../types\";\r\n\r\n/**\r\n * Options for creating a Broker\r\n */\r\nexport interface BrokerOptions {\r\n /** Unique name for this broker */\r\n name: string;\r\n /** The underlying driver */\r\n driver: BrokerDriverContract;\r\n /** Whether this is the default broker */\r\n isDefault?: boolean;\r\n}\r\n\r\n/**\r\n * Broker - wrapper around a driver with metadata\r\n *\r\n * Similar to DataSource in @warlock.js/cascade\r\n *\r\n * @example\r\n * ```typescript\r\n * const broker = new Broker({\r\n * name: \"default\",\r\n * driver: rabbitMQDriver,\r\n * isDefault: true,\r\n * });\r\n *\r\n * // Get a channel\r\n * const channel = broker.channel(\"user.created\");\r\n * ```\r\n */\r\nexport class Broker {\r\n /** Unique name identifying this broker */\r\n public readonly name: string;\r\n\r\n /** The underlying driver */\r\n public readonly driver: BrokerDriverContract;\r\n\r\n /** Whether this is the default broker */\r\n public readonly isDefault: boolean;\r\n\r\n /**\r\n * Create a new Broker\r\n *\r\n * @param options - Broker configuration\r\n */\r\n public constructor(options: BrokerOptions) {\r\n this.name = options.name;\r\n this.driver = options.driver;\r\n this.isDefault = Boolean(options.isDefault);\r\n }\r\n\r\n /**\r\n * Subscribe the given consumer\r\n */\r\n public subscribe(consumer: EventConsumerClass<any>) {\r\n return this.driver.subscribe(consumer);\r\n }\r\n\r\n /**\r\n * Publish the given event message\r\n */\r\n public publish<TPayload = Record<string, any>>(event: EventMessage<TPayload>) {\r\n this.driver.publish(event);\r\n }\r\n\r\n /**\r\n * Get or create a channel\r\n *\r\n * @param name - Channel name\r\n * @param options - Channel options\r\n * @returns Channel instance\r\n *\r\n * @example\r\n * ```typescript\r\n * // Simple channel\r\n * const channel = broker.channel(\"notifications\");\r\n *\r\n * // Typed channel with schema\r\n * const orderChannel = broker.channel<OrderPayload>(\"orders\", {\r\n * schema: OrderSchema,\r\n * durable: true,\r\n * });\r\n * ```\r\n */\r\n public channel<TPayload = unknown>(\r\n name: string,\r\n options?: ChannelOptions<TPayload>,\r\n ): ChannelContract<TPayload> {\r\n return this.driver.channel<TPayload>(name, options);\r\n }\r\n\r\n /**\r\n * Check if the broker is connected\r\n */\r\n public get isConnected(): boolean {\r\n return this.driver.isConnected;\r\n }\r\n\r\n /**\r\n * Connect the underlying driver\r\n */\r\n public async connect(): Promise<void> {\r\n await this.driver.connect();\r\n }\r\n\r\n /**\r\n * Disconnect the underlying driver\r\n */\r\n public async disconnect(): Promise<void> {\r\n await this.driver.disconnect();\r\n }\r\n\r\n /**\r\n * Start consuming messages\r\n */\r\n public async startConsuming(): Promise<void> {\r\n await this.driver.startConsuming();\r\n }\r\n\r\n /**\r\n * Stop consuming messages\r\n */\r\n public async stopConsuming(): Promise<void> {\r\n await this.driver.stopConsuming();\r\n }\r\n\r\n /**\r\n * Health check\r\n */\r\n public async healthCheck() {\r\n return this.driver.healthCheck();\r\n }\r\n}\r\n","import { EventEmitter } from \"node:events\";\r\nimport type { BrokerRegistryEvent, BrokerRegistryListener } from \"../types\";\r\nimport { Broker, type BrokerOptions } from \"./broker\";\r\n\r\n/**\r\n * Error thrown when a broker is not found\r\n */\r\nexport class MissingBrokerError extends Error {\r\n public readonly brokerName?: string;\r\n\r\n public constructor(message: string, brokerName?: string) {\r\n super(message);\r\n this.name = \"MissingBrokerError\";\r\n this.brokerName = brokerName;\r\n }\r\n}\r\n\r\n/**\r\n * Broker Registry\r\n *\r\n * Maintains registry of named brokers.\r\n * Similar to DataSourceRegistry in @warlock.js/cascade\r\n *\r\n * @example\r\n * ```typescript\r\n * // Register a broker\r\n * brokerRegistry.register({\r\n * name: \"default\",\r\n * driver: rabbitMQDriver,\r\n * isDefault: true,\r\n * });\r\n *\r\n * // Get the default broker\r\n * const comm = brokerRegistry.get();\r\n *\r\n * // Get a specific broker by name\r\n * const analytics = brokerRegistry.get(\"analytics\");\r\n *\r\n * // Listen for events\r\n * brokerRegistry.on(\"connected\", (comm) => {\r\n * console.log(`${comm.name} connected`);\r\n * });\r\n * ```\r\n */\r\nclass BrokerRegistry {\r\n private readonly sources = new Map<string, Broker>();\r\n private defaultSource?: Broker;\r\n private readonly events = new EventEmitter();\r\n\r\n /**\r\n * Register a new broker\r\n *\r\n * Sets up event forwarding from the driver to the registry.\r\n *\r\n * @param options - Broker configuration\r\n * @returns The registered broker instance\r\n *\r\n * @example\r\n * ```typescript\r\n * const broker = brokerRegistry.register({\r\n * name: \"primary\",\r\n * driver: myDriver,\r\n * isDefault: true,\r\n * });\r\n * ```\r\n */\r\n public register(options: BrokerOptions): Broker {\r\n const broker = new Broker(options);\r\n this.sources.set(broker.name, broker);\r\n\r\n const isNewDefault = broker.isDefault || !this.defaultSource;\r\n\r\n if (isNewDefault) {\r\n this.defaultSource = broker;\r\n }\r\n\r\n // Emit registration events\r\n this.events.emit(\"registered\", broker);\r\n\r\n if (isNewDefault) {\r\n this.events.emit(\"default-registered\", broker);\r\n }\r\n\r\n // Forward driver events to registry\r\n broker.driver.on(\"connected\", () => {\r\n this.events.emit(\"connected\", broker);\r\n });\r\n\r\n broker.driver.on(\"disconnected\", () => {\r\n this.events.emit(\"disconnected\", broker);\r\n });\r\n\r\n return broker;\r\n }\r\n\r\n /**\r\n * Clear all registered brokers\r\n */\r\n public clear(): void {\r\n this.defaultSource = undefined;\r\n this.sources.clear();\r\n }\r\n\r\n /**\r\n * Listen for registry events\r\n *\r\n * @param event - Event to listen for\r\n * @param listener - Callback function\r\n *\r\n * @example\r\n * ```typescript\r\n * brokerRegistry.on(\"registered\", (comm) => {\r\n * console.log(`Broker \"${comm.name}\" registered`);\r\n * });\r\n *\r\n * brokerRegistry.on(\"connected\", (comm) => {\r\n * console.log(`Broker \"${comm.name}\" connected`);\r\n * });\r\n * ```\r\n */\r\n public on(event: BrokerRegistryEvent, listener: BrokerRegistryListener): void {\r\n this.events.on(event, listener);\r\n }\r\n\r\n /**\r\n * Listen for a registry event once\r\n *\r\n * @param event - Event to listen for\r\n * @param listener - Callback function\r\n */\r\n public once(event: BrokerRegistryEvent, listener: BrokerRegistryListener): void {\r\n this.events.once(event, listener);\r\n }\r\n\r\n /**\r\n * Remove an event listener\r\n *\r\n * @param event - Event to stop listening for\r\n * @param listener - Callback to remove\r\n */\r\n public off(event: BrokerRegistryEvent, listener: BrokerRegistryListener): void {\r\n this.events.off(event, listener);\r\n }\r\n\r\n /**\r\n * Get a broker by name or the default one\r\n *\r\n * @param name - Optional broker name\r\n * @returns Broker instance\r\n * @throws MissingBrokerError if not found\r\n *\r\n * @example\r\n * ```typescript\r\n * // Get default broker\r\n * const comm = brokerRegistry.get();\r\n *\r\n * // Get specific broker\r\n * const analytics = brokerRegistry.get(\"analytics\");\r\n * ```\r\n */\r\n public get(name?: string): Broker {\r\n if (name !== undefined) {\r\n const source = this.sources.get(name);\r\n if (!source) {\r\n throw new MissingBrokerError(`Broker \"${name}\" is not registered.`, name);\r\n }\r\n return source;\r\n }\r\n\r\n if (!this.defaultSource) {\r\n throw new MissingBrokerError(\"No default broker registered.\");\r\n }\r\n\r\n return this.defaultSource;\r\n }\r\n\r\n /**\r\n * Check if a broker exists\r\n *\r\n * @param name - Broker name to check\r\n * @returns True if exists\r\n */\r\n public has(name: string): boolean {\r\n return this.sources.has(name);\r\n }\r\n\r\n /**\r\n * Check if any brokers are registered\r\n */\r\n public hasAny(): boolean {\r\n return this.sources.size > 0;\r\n }\r\n\r\n /**\r\n * Get all registered brokers\r\n *\r\n * @returns Array of all brokers\r\n *\r\n * @example\r\n * ```typescript\r\n * // Disconnect all brokers\r\n * for (const comm of brokerRegistry.getAll()) {\r\n * await comm.disconnect();\r\n * }\r\n * ```\r\n */\r\n public getAll(): Broker[] {\r\n return Array.from(this.sources.values());\r\n }\r\n\r\n /**\r\n * Get all broker names\r\n *\r\n * @returns Array of broker names\r\n */\r\n public getNames(): string[] {\r\n return Array.from(this.sources.keys());\r\n }\r\n\r\n /**\r\n * Get the default broker (if any)\r\n *\r\n * @returns Default broker or undefined\r\n */\r\n public getDefault(): Broker | undefined {\r\n return this.defaultSource;\r\n }\r\n}\r\n\r\n/**\r\n * Global broker registry instance\r\n */\r\nexport const brokerRegistry = new BrokerRegistry();\r\n","import type { MessageHandler } from \"./../types\";\r\nimport { EventConsumerClass } from \"./types\";\r\n\r\nexport function prepareConsumerSubscription(\r\n Consumer: EventConsumerClass,\r\n onError?: (error: unknown, consumerName: string) => void,\r\n) {\r\n const callback: MessageHandler<any> = async (message, ctx) => {\r\n const envelope = message.payload;\r\n let eventPayload = envelope.payload;\r\n\r\n if (envelope.version) {\r\n if (!Consumer.isAcceptedVersion(Number(envelope.version))) {\r\n ctx.ack(); // Acknowledge but don't process\r\n return;\r\n }\r\n }\r\n\r\n const consumer = new Consumer();\r\n\r\n if (consumer.schema) {\r\n const result = await consumer.validate(eventPayload);\r\n if (!result || result.isValid === false) {\r\n ctx.nack();\r\n return;\r\n }\r\n\r\n eventPayload = result.data;\r\n }\r\n try {\r\n await consumer.handle(eventPayload, {\r\n payload: eventPayload,\r\n eventName: Consumer.eventName,\r\n messageId: message.metadata.messageId!,\r\n occurredAt: envelope.occurredAt ? new Date(envelope.occurredAt) : undefined,\r\n metadata: envelope.metadata,\r\n version: envelope.version,\r\n message,\r\n });\r\n ctx.ack(); // Auto-ack on success?\r\n } catch (error) {\r\n // Bounded retry instead of an unconditional requeue: a message that\r\n // reliably throws (bad payload, a handler bug, a downstream outage)\r\n // would otherwise be nack+requeued forever, pinning the consumer in a\r\n // hot loop. `ctx.retry()` caps redelivery and dead-letters/drops (with\r\n // a loud log) once the cap is hit.\r\n await ctx.retry();\r\n if (onError) {\r\n onError(error, Consumer.eventName);\r\n }\r\n }\r\n };\r\n\r\n return callback;\r\n}\r\n","import { log } from \"@warlock.js/logger\";\r\nimport { v } from \"@warlock.js/seal\";\r\nimport { randomUUID } from \"node:crypto\";\r\nimport type { ChannelContract } from \"../../contracts\";\r\nimport type {\r\n ChannelOptions,\r\n ChannelStats,\r\n Message,\r\n MessageContext,\r\n MessageHandler,\r\n MessageMetadata,\r\n PublishOptions,\r\n RequestOptions,\r\n ResponseHandler,\r\n SubscribeOptions,\r\n Subscription,\r\n} from \"../../types\";\r\n\r\n/**\r\n * RabbitMQ Channel Implementation\r\n *\r\n * Wraps a RabbitMQ queue/exchange with a unified API.\r\n *\r\n * @template TPayload - The typed payload\r\n */\r\nexport class RabbitMQChannel<TPayload = unknown> implements ChannelContract<TPayload> {\r\n public readonly name: string;\r\n public readonly options: ChannelOptions<TPayload>;\r\n\r\n private readonly amqpChannel: any;\r\n private readonly subscriptions = new Map<string, RabbitMQSubscription>();\r\n private asserted = false;\r\n\r\n /**\r\n * Create a new RabbitMQ channel\r\n */\r\n public constructor(name: string, amqpChannel: any, options?: ChannelOptions<TPayload>) {\r\n this.name = name;\r\n this.amqpChannel = amqpChannel;\r\n this.options = options ?? {};\r\n }\r\n\r\n /**\r\n * Assert the queue exists\r\n */\r\n public async assert(): Promise<void> {\r\n if (this.asserted) return;\r\n\r\n const queueOptions = {\r\n durable: this.options.durable ?? true,\r\n autoDelete: this.options.autoDelete ?? false,\r\n exclusive: this.options.exclusive ?? false,\r\n messageTtl: this.options.messageTtl,\r\n maxLength: this.options.maxLength,\r\n deadLetterExchange: this.options.deadLetter?.channel ? \"\" : undefined,\r\n deadLetterRoutingKey: this.options.deadLetter?.channel,\r\n };\r\n\r\n await this.amqpChannel.assertQueue(this.name, queueOptions);\r\n this.asserted = true;\r\n }\r\n\r\n /**\r\n * Publish a message\r\n */\r\n public async publish(payload: TPayload, options?: PublishOptions): Promise<void> {\r\n await this.assert();\r\n\r\n // Validate with schema if provided\r\n if (this.options.schema) {\r\n const context = {\r\n allValues: payload,\r\n value: payload,\r\n };\r\n const result = await v.validate(this.options.schema, payload, { context });\r\n if (!result.isValid) {\r\n throw new Error(`Message validation failed: ${JSON.stringify(result.errors)}`);\r\n }\r\n payload = result.data as TPayload;\r\n }\r\n\r\n const messageId = randomUUID();\r\n const timestamp = new Date();\r\n\r\n const messageContent = JSON.stringify({\r\n payload,\r\n metadata: {\r\n messageId,\r\n timestamp: timestamp.toISOString(),\r\n correlationId: options?.correlationId,\r\n headers: options?.headers,\r\n },\r\n });\r\n\r\n const publishOptions: any = {\r\n persistent: options?.persistent ?? true,\r\n messageId,\r\n timestamp: timestamp.getTime(),\r\n correlationId: options?.correlationId,\r\n expiration: options?.expiration?.toString(),\r\n priority: options?.priority,\r\n headers: options?.headers,\r\n };\r\n\r\n // Handle delayed messages (requires rabbitmq-delayed-message-exchange plugin)\r\n if (options?.delay) {\r\n publishOptions.headers = {\r\n ...publishOptions.headers,\r\n \"x-delay\": options.delay,\r\n };\r\n }\r\n\r\n this.amqpChannel.sendToQueue(this.name, Buffer.from(messageContent), publishOptions);\r\n }\r\n\r\n /**\r\n * Publish multiple messages\r\n */\r\n public async publishBatch(messages: TPayload[], options?: PublishOptions): Promise<void> {\r\n for (const payload of messages) {\r\n await this.publish(payload, options);\r\n }\r\n }\r\n\r\n /**\r\n * Subscribe to messages\r\n *\r\n * Smart auto-ack behavior (when autoAck is not true):\r\n * - If handler completes successfully without explicit ack/nack/reject → auto-ack\r\n * - If handler throws an error → auto-nack (with retry if configured)\r\n * - If handler explicitly calls ack/nack/reject → respects that call\r\n */\r\n public async subscribe(\r\n handler: MessageHandler<TPayload>,\r\n options?: SubscribeOptions,\r\n ): Promise<Subscription> {\r\n await this.assert();\r\n\r\n // Use consumerId from options if provided, otherwise generate a random one\r\n const subscriptionId = options?.consumerId ?? randomUUID();\r\n\r\n // Set prefetch if specified\r\n if (options?.prefetch) {\r\n await this.amqpChannel.prefetch(options.prefetch);\r\n }\r\n\r\n // If autoAck is true, RabbitMQ handles ack immediately (fire-and-forget)\r\n const isFireAndForget = options?.autoAck === true;\r\n\r\n const consumerOptions = {\r\n noAck: isFireAndForget,\r\n exclusive: options?.exclusive ?? false,\r\n consumerTag: options?.group ?? subscriptionId,\r\n };\r\n\r\n const { consumerTag } = await this.amqpChannel.consume(\r\n this.name,\r\n async (msg: any) => {\r\n if (!msg) return;\r\n\r\n // Track if acknowledgment was handled explicitly\r\n let ackHandled = isFireAndForget;\r\n\r\n // Populated once the message is successfully parsed, so the catch\r\n // block below can dead-letter with the full envelope. Stays\r\n // `undefined` when `JSON.parse` itself is what threw.\r\n let parsedMessage: Message<TPayload> | undefined;\r\n\r\n try {\r\n const content = JSON.parse(msg.content.toString());\r\n let payload = content.payload as TPayload;\r\n\r\n // Validate with schema if provided\r\n if (this.options.schema) {\r\n const context = {\r\n allValues: payload,\r\n value: payload,\r\n };\r\n const result = await v.validate(this.options.schema, payload, { context });\r\n if (!result.isValid) {\r\n // Reject invalid messages\r\n this.amqpChannel.nack(msg, false, false);\r\n return;\r\n }\r\n payload = result.data as TPayload;\r\n }\r\n\r\n const metadata: MessageMetadata = {\r\n messageId: msg.properties.messageId || content.metadata?.messageId || randomUUID(),\r\n timestamp: new Date(msg.properties.timestamp || content.metadata?.timestamp),\r\n correlationId: msg.properties.correlationId || content.metadata?.correlationId,\r\n replyTo: msg.properties.replyTo,\r\n priority: msg.properties.priority,\r\n headers: msg.properties.headers,\r\n retryCount: msg.properties.headers?.[\"x-retry-count\"] || 0,\r\n originalChannel: this.name,\r\n };\r\n\r\n const message: Message<TPayload> = {\r\n metadata,\r\n payload,\r\n raw: msg,\r\n };\r\n\r\n parsedMessage = message;\r\n\r\n const context: MessageContext = {\r\n ack: async () => {\r\n if (!ackHandled) {\r\n ackHandled = true;\r\n this.amqpChannel.ack(msg);\r\n }\r\n },\r\n nack: async (requeue = true) => {\r\n if (!ackHandled) {\r\n ackHandled = true;\r\n this.amqpChannel.nack(msg, false, requeue);\r\n }\r\n },\r\n reject: async () => {\r\n if (!ackHandled) {\r\n ackHandled = true;\r\n this.amqpChannel.reject(msg, false);\r\n }\r\n },\r\n reply: async <T>(replyPayload: T) => {\r\n if (msg.properties.replyTo) {\r\n const replyContent = JSON.stringify({\r\n payload: replyPayload,\r\n metadata: {\r\n messageId: randomUUID(),\r\n timestamp: new Date().toISOString(),\r\n correlationId: msg.properties.correlationId,\r\n },\r\n });\r\n\r\n this.amqpChannel.sendToQueue(msg.properties.replyTo, Buffer.from(replyContent), {\r\n correlationId: msg.properties.correlationId,\r\n });\r\n }\r\n },\r\n retry: async (delay?: number) => {\r\n if (ackHandled) return;\r\n ackHandled = true;\r\n\r\n await this.retryOrGiveUp(msg, metadata.retryCount || 0, options, message, delay);\r\n },\r\n };\r\n\r\n // Execute handler\r\n await handler(message, context);\r\n\r\n // Smart auto-ack: if handler succeeded and didn't explicitly handle ack\r\n if (!ackHandled) {\r\n this.amqpChannel.ack(msg);\r\n }\r\n } catch (error) {\r\n // Smart auto-nack: if handler threw and didn't explicitly handle ack\r\n if (ackHandled) return;\r\n\r\n if (options?.retry) {\r\n // A bare `nack(msg, false, true)` redelivers the ORIGINAL message\r\n // untouched — amqplib/RabbitMQ do not add an `x-retry-count`\r\n // header on requeue, so a plain requeue here never advances the\r\n // counter and `maxRetries`/`deadLetter` are silently never\r\n // reached. Route through the same bounded-retry path `ctx.retry()`\r\n // uses, so the counter increments (and the cap/dead-letter fires)\r\n // on this automatic path too.\r\n const currentRetryCount = msg.properties.headers?.[\"x-retry-count\"] || 0;\r\n await this.retryOrGiveUp(msg, currentRetryCount, options, parsedMessage);\r\n } else {\r\n // No retry configured - reject without requeue (already bounded:\r\n // a single attempt, no requeue loop possible).\r\n this.amqpChannel.nack(msg, false, false);\r\n }\r\n }\r\n },\r\n consumerOptions,\r\n );\r\n\r\n const subscription = new RabbitMQSubscription(\r\n subscriptionId,\r\n this.name,\r\n consumerTag,\r\n this.amqpChannel,\r\n );\r\n\r\n this.subscriptions.set(subscriptionId, subscription);\r\n\r\n return subscription;\r\n }\r\n\r\n /**\r\n * Unsubscribe by consumer ID\r\n */\r\n public async unsubscribeById(consumerId: string): Promise<void> {\r\n const subscription = this.subscriptions.get(consumerId);\r\n if (subscription) {\r\n await subscription.unsubscribe();\r\n this.subscriptions.delete(consumerId);\r\n }\r\n }\r\n\r\n /**\r\n * Stop consuming messages on this channel.\r\n * Cancels all active subscriptions gracefully.\r\n */\r\n public async stopConsuming(): Promise<void> {\r\n const cancellations = Array.from(this.subscriptions.values()).map(sub =>\r\n sub.unsubscribe(),\r\n );\r\n await Promise.all(cancellations);\r\n }\r\n\r\n /**\r\n * Send message to dead-letter queue\r\n */\r\n private async sendToDeadLetter(\r\n message: Message<TPayload>,\r\n deadLetterChannel: string,\r\n ): Promise<void> {\r\n const content = JSON.stringify({\r\n payload: message.payload,\r\n metadata: {\r\n ...message.metadata,\r\n originalChannel: this.name,\r\n },\r\n });\r\n\r\n this.amqpChannel.sendToQueue(deadLetterChannel, Buffer.from(content), { persistent: true });\r\n }\r\n\r\n /**\r\n * Dead-letter a message whose body couldn't be parsed into a {@link Message}\r\n * (e.g. `JSON.parse` itself threw before an envelope existed). Forwards the\r\n * raw AMQP bytes/properties as-is rather than re-deriving a payload, so a\r\n * malformed message isn't lost.\r\n */\r\n private sendToDeadLetterRaw(msg: any, deadLetterChannel: string): void {\r\n this.amqpChannel.sendToQueue(deadLetterChannel, msg.content, {\r\n ...msg.properties,\r\n persistent: true,\r\n });\r\n }\r\n\r\n /**\r\n * Bounded retry shared by the explicit `ctx.retry()` call and the automatic\r\n * catch when a handler throws without calling it itself — so both paths\r\n * honor the same cap instead of the automatic path silently requeueing\r\n * forever (see `subscribe()`'s catch block).\r\n *\r\n * Under the cap: republishes with an incremented `x-retry-count` header —\r\n * NOT a plain `nack(msg, false, true)`, which redelivers the original\r\n * message untouched and never advances the counter.\r\n *\r\n * At/over the cap: dead-letters if configured, otherwise drops the message\r\n * with a loud `log.error` (never a silent drop) so an operator can see a\r\n * poison message was discarded instead of it vanishing without a trace.\r\n */\r\n private async retryOrGiveUp(\r\n msg: any,\r\n currentRetryCount: number,\r\n options: SubscribeOptions | undefined,\r\n parsedMessage: Message<TPayload> | undefined,\r\n delay?: number,\r\n ): Promise<void> {\r\n const retryCount = currentRetryCount + 1;\r\n const maxRetries = options?.retry?.maxRetries ?? 3;\r\n\r\n if (retryCount > maxRetries) {\r\n if (options?.deadLetter) {\r\n if (parsedMessage) {\r\n await this.sendToDeadLetter(parsedMessage, options.deadLetter.channel);\r\n } else {\r\n this.sendToDeadLetterRaw(msg, options.deadLetter.channel);\r\n }\r\n } else {\r\n log.error(\r\n \"herald\",\r\n \"poison-message\",\r\n `Dropping message on channel \"${this.name}\" after ${retryCount - 1} failed ` +\r\n `${retryCount - 1 === 1 ? \"retry\" : \"retries\"} (maxRetries: ${maxRetries}) with no ` +\r\n `dead-letter channel configured.`,\r\n { channel: this.name, retryCount: retryCount - 1, maxRetries },\r\n );\r\n this.amqpChannel.reject(msg, false);\r\n return;\r\n }\r\n\r\n this.amqpChannel.ack(msg);\r\n return;\r\n }\r\n\r\n const headers: Record<string, unknown> = {\r\n ...msg.properties.headers,\r\n \"x-retry-count\": retryCount,\r\n };\r\n\r\n if (delay) {\r\n headers[\"x-delay\"] = delay;\r\n }\r\n\r\n this.amqpChannel.sendToQueue(this.name, msg.content, { ...msg.properties, headers });\r\n\r\n this.amqpChannel.ack(msg);\r\n }\r\n\r\n /**\r\n * Request-response pattern\r\n */\r\n public async request<TResponse = unknown>(\r\n payload: TPayload,\r\n options?: RequestOptions,\r\n ): Promise<TResponse> {\r\n await this.assert();\r\n\r\n const correlationId = randomUUID();\r\n const timeout = options?.timeout ?? 30000;\r\n\r\n // Create exclusive reply queue\r\n const { queue: replyQueue } = await this.amqpChannel.assertQueue(\"\", {\r\n exclusive: true,\r\n autoDelete: true,\r\n });\r\n\r\n return new Promise<TResponse>((resolve, reject) => {\r\n const timeoutId = setTimeout(() => {\r\n reject(new Error(`Request timeout after ${timeout}ms`));\r\n }, timeout);\r\n\r\n // Consume reply\r\n this.amqpChannel.consume(\r\n replyQueue,\r\n (msg: any) => {\r\n if (msg?.properties.correlationId === correlationId) {\r\n clearTimeout(timeoutId);\r\n const content = JSON.parse(msg.content.toString());\r\n resolve(content.payload as TResponse);\r\n }\r\n },\r\n { noAck: true },\r\n );\r\n\r\n // Send request\r\n const messageContent = JSON.stringify({\r\n payload,\r\n metadata: {\r\n messageId: randomUUID(),\r\n timestamp: new Date().toISOString(),\r\n correlationId,\r\n },\r\n });\r\n\r\n this.amqpChannel.sendToQueue(this.name, Buffer.from(messageContent), {\r\n correlationId,\r\n replyTo: replyQueue,\r\n expiration: timeout.toString(),\r\n ...options,\r\n });\r\n });\r\n }\r\n\r\n /**\r\n * Register response handler for RPC\r\n */\r\n public async respond<TResponse = unknown>(\r\n handler: ResponseHandler<TPayload, TResponse>,\r\n ): Promise<Subscription> {\r\n return this.subscribe(async (message, ctx) => {\r\n const response = await handler(message, ctx);\r\n await ctx.reply(response);\r\n await ctx.ack();\r\n });\r\n }\r\n\r\n /**\r\n * Get queue statistics\r\n */\r\n public async stats(): Promise<ChannelStats> {\r\n await this.assert();\r\n\r\n const queueInfo = await this.amqpChannel.checkQueue(this.name);\r\n\r\n return {\r\n name: this.name,\r\n messageCount: queueInfo.messageCount,\r\n consumerCount: queueInfo.consumerCount,\r\n };\r\n }\r\n\r\n /**\r\n * Purge all messages\r\n */\r\n public async purge(): Promise<number> {\r\n await this.assert();\r\n\r\n const result = await this.amqpChannel.purgeQueue(this.name);\r\n return result.messageCount;\r\n }\r\n\r\n /**\r\n * Check if queue exists\r\n */\r\n public async exists(): Promise<boolean> {\r\n try {\r\n await this.amqpChannel.checkQueue(this.name);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Delete the queue\r\n */\r\n public async delete(): Promise<void> {\r\n // Cancel all subscriptions\r\n for (const subscription of this.subscriptions.values()) {\r\n await subscription.unsubscribe();\r\n }\r\n this.subscriptions.clear();\r\n\r\n try {\r\n await this.amqpChannel.deleteQueue(this.name);\r\n } catch {\r\n // Ignore if queue doesn't exist\r\n }\r\n\r\n this.asserted = false;\r\n }\r\n}\r\n\r\n/**\r\n * RabbitMQ Subscription Implementation\r\n */\r\nclass RabbitMQSubscription implements Subscription {\r\n public readonly id: string;\r\n public readonly channel: string;\r\n public readonly consumerTag: string;\r\n\r\n private readonly amqpChannel: any;\r\n private _isActive = true;\r\n\r\n public constructor(id: string, channel: string, consumerTag: string, amqpChannel: any) {\r\n this.id = id;\r\n this.channel = channel;\r\n this.consumerTag = consumerTag;\r\n this.amqpChannel = amqpChannel;\r\n }\r\n\r\n public async unsubscribe(): Promise<void> {\r\n if (!this._isActive) return;\r\n\r\n await this.amqpChannel.cancel(this.consumerTag);\r\n this._isActive = false;\r\n }\r\n\r\n public async pause(): Promise<void> {\r\n // RabbitMQ doesn't have native pause, cancel consumer\r\n await this.amqpChannel.cancel(this.consumerTag);\r\n }\r\n\r\n public async resume(): Promise<void> {\r\n // Would need to re-subscribe - not directly supported\r\n throw new Error(\"Resume is not supported for RabbitMQ. Please create a new subscription.\");\r\n }\r\n\r\n public isActive(): boolean {\r\n return this._isActive;\r\n }\r\n}\r\n","import { EventEmitter } from \"node:events\";\r\nimport type { BrokerDriverContract, ChannelContract } from \"../../contracts\";\r\nimport { EventMessage } from \"../../message-managers/event-message\";\r\nimport { EventConsumerClass } from \"../../message-managers/types\";\r\nimport type {\r\n BrokerDriverType,\r\n BrokerEvent,\r\n BrokerEventListener,\r\n ChannelOptions,\r\n HealthCheckResult,\r\n RabbitMQConnectionOptions,\r\n} from \"../../types\";\r\nimport { prepareConsumerSubscription } from \"./../../message-managers/prepare-consumer-subscription\";\r\nimport { RabbitMQChannel } from \"./rabbitmq-channel\";\r\n\r\n// ============================================================\r\n// Lazy-loaded amqplib Module\r\n// ============================================================\r\n\r\n/**\r\n * Shape of the lazily-imported amqplib module\r\n */\r\ntype AmqplibModule = typeof import(\"amqplib\");\r\n\r\n/**\r\n * The single amqplib load, shared by every caller.\r\n *\r\n * Memoized as a promise rather than as a resolved value so the loader stays\r\n * idempotent: without it, two callers arriving before the first `import()`\r\n * settles would each start their own load and the last writer would win, so a\r\n * caller could end up observing a module instance it never awaited.\r\n *\r\n * Resolves to `undefined` when amqplib is not installed.\r\n */\r\nlet amqplibModulePromise: Promise<AmqplibModule | undefined> | undefined;\r\n\r\n/**\r\n * Installation instructions for amqplib\r\n */\r\nconst AMQPLIB_INSTALL_INSTRUCTIONS = `\r\nRabbitMQ driver requires the amqplib package.\r\nInstall it with:\r\n\r\n npx warlock add herald --driver=rabbitmq\r\n\r\nOr manually:\r\n\r\n npm install amqplib\r\n pnpm add amqplib\r\n yarn add amqplib\r\n`.trim();\r\n\r\n/**\r\n * Strip `user:password@` credentials from any `amqp(s)://` URL embedded in a\r\n * string. The connection URL carries plaintext broker credentials, and\r\n * amqplib/Node's URL parser commonly echoes the offending URL verbatim in a\r\n * malformed-URL error (e.g. an unencoded `@`/`:` in the password) — applied\r\n * to every error `connect()` surfaces so a credential never reaches whatever\r\n * the host app does with a thrown connection error (console.error,\r\n * structured logging, an error tracker).\r\n */\r\nfunction redactAmqpCredentials(message: string): string {\r\n return message.replace(/(amqps?:\\/\\/)[^/@\\s]+@/gi, \"$1****:****@\");\r\n}\r\n\r\n/**\r\n * Load amqplib, reusing the single shared load for every caller.\r\n *\r\n * @returns The amqplib module, or `undefined` when it is not installed.\r\n */\r\nfunction loadAmqplibModule(): Promise<AmqplibModule | undefined> {\r\n if (!amqplibModulePromise) {\r\n amqplibModulePromise = import(\"amqplib\").catch(() => undefined);\r\n }\r\n\r\n return amqplibModulePromise;\r\n}\r\n\r\n// ============================================================\r\n// RabbitMQ Driver\r\n// ============================================================\r\n\r\n/**\r\n * RabbitMQ Driver\r\n *\r\n * Implementation of BrokerDriverContract for RabbitMQ/AMQP.\r\n *\r\n * **Important:** This driver requires the `amqplib` package to be installed.\r\n * Install it with: `npx warlock add herald --driver=rabbitmq` or `npm install amqplib`\r\n *\r\n * @example\r\n * ```typescript\r\n * const driver = new RabbitMQDriver({\r\n * driver: \"rabbitmq\",\r\n * host: \"localhost\",\r\n * port: 5672,\r\n * username: \"guest\",\r\n * password: \"guest\",\r\n * });\r\n *\r\n * await driver.connect();\r\n * const channel = driver.channel(\"user.created\");\r\n * ```\r\n */\r\nexport class RabbitMQDriver implements BrokerDriverContract {\r\n public readonly name = \"rabbitmq\" as const;\r\n\r\n public readonly consumers: EventConsumerClass[] = [];\r\n\r\n private readonly options: RabbitMQConnectionOptions;\r\n private readonly events = new EventEmitter();\r\n private readonly channels = new Map<string, ChannelContract<any>>();\r\n\r\n private connection: any = null;\r\n private amqpChannel: any = null;\r\n private _isConnected = false;\r\n\r\n /**\r\n * Create a new RabbitMQ driver\r\n *\r\n * @param options - RabbitMQ connection options\r\n */\r\n public constructor(options: RabbitMQConnectionOptions) {\r\n this.options = options;\r\n }\r\n\r\n /**\r\n * Whether connected to RabbitMQ\r\n */\r\n public get isConnected(): boolean {\r\n return this._isConnected;\r\n }\r\n\r\n /**\r\n * Subscribe the given consumer class to the driver\r\n *\r\n * @param consumer - Consumer class to subscribe\r\n *\r\n * @example\r\n * ```typescript\r\n * driver.subscribe(UserUpdatedConsumer);\r\n * ```\r\n */\r\n public subscribe(Consumer: EventConsumerClass) {\r\n if (this.isConnected) {\r\n this.channel(Consumer.eventName).subscribe(\r\n prepareConsumerSubscription(Consumer, (error, eventName) => {\r\n this.events.emit(\"error\", error, eventName);\r\n }),\r\n {\r\n consumerId: Consumer.consumerId,\r\n },\r\n );\r\n } else {\r\n this.consumers.push(Consumer);\r\n }\r\n\r\n return () => {\r\n this.unsubscribe(Consumer);\r\n };\r\n }\r\n\r\n /**\r\n * Unsubscribe the given consumer\r\n */\r\n public unsubscribe(Consumer: EventConsumerClass): void {\r\n if (this.isConnected) {\r\n this.channel(Consumer.eventName).unsubscribeById(Consumer.consumerId);\r\n }\r\n const index = this.consumers.indexOf(Consumer);\r\n if (index > -1) {\r\n this.consumers.splice(index, 1);\r\n }\r\n }\r\n\r\n /**\r\n * Publish the given event message.\r\n * Auto-creates the channel if it hasn't been accessed before.\r\n */\r\n public publish<TPayload = Record<string, any>>(event: EventMessage<TPayload>): void {\r\n this.channel(event.eventName).publish(event.serialize());\r\n }\r\n\r\n /**\r\n * Connect to RabbitMQ\r\n */\r\n public async connect(): Promise<void> {\r\n const amqplib = await loadAmqplibModule();\r\n\r\n if (!amqplib) {\r\n throw new Error(`amqplib is not installed.\\n\\n${AMQPLIB_INSTALL_INSTRUCTIONS}`);\r\n }\r\n\r\n try {\r\n // Build connection URL\r\n const url = this.buildConnectionUrl();\r\n\r\n // Build connection options merging our options with native client options\r\n const connectOptions = {\r\n heartbeat: this.options.heartbeat ?? 60,\r\n timeout: this.options.connectionTimeout,\r\n // Merge native amqplib client options\r\n ...this.options.clientOptions,\r\n };\r\n\r\n // Connect using cached module\r\n this.connection = await amqplib.connect(url, connectOptions);\r\n\r\n // Create channel\r\n this.amqpChannel = await this.connection.createChannel();\r\n\r\n // Set prefetch if specified\r\n if (this.options.prefetch) {\r\n await this.amqpChannel.prefetch(this.options.prefetch);\r\n }\r\n\r\n this._isConnected = true;\r\n this.events.emit(\"connected\");\r\n\r\n for (const consumer of this.consumers) {\r\n this.subscribe(consumer);\r\n }\r\n\r\n this.consumers.length = 0;\r\n\r\n // Handle connection close\r\n this.connection.on(\"close\", () => {\r\n this._isConnected = false;\r\n this.events.emit(\"disconnected\");\r\n\r\n if (this.options.reconnect !== false) {\r\n this.handleReconnect();\r\n }\r\n });\r\n\r\n // Handle errors\r\n this.connection.on(\"error\", (error: Error) => {\r\n this.events.emit(\"error\", error);\r\n });\r\n } catch (error) {\r\n this._isConnected = false;\r\n const message = error instanceof Error ? error.message : String(error);\r\n throw new Error(`Failed to connect to RabbitMQ: ${redactAmqpCredentials(message)}`);\r\n }\r\n }\r\n\r\n /**\r\n * Build connection URL from options\r\n */\r\n private buildConnectionUrl(): string {\r\n if (this.options.uri) {\r\n return this.options.uri;\r\n }\r\n\r\n const protocol = \"amqp\";\r\n const host = this.options.host ?? \"localhost\";\r\n const port = this.options.port ?? 5672;\r\n const vhost = this.options.vhost ?? \"/\";\r\n // URI-encoded so a credential containing a reserved URL character\r\n // (`@`, `:`, `/`, whitespace — common in generated secrets) can't produce\r\n // a malformed URL whose parser error echoes the raw credential back.\r\n const username = encodeURIComponent(this.options.username ?? \"guest\");\r\n const password = encodeURIComponent(this.options.password ?? \"guest\");\r\n\r\n const encodedVhost = encodeURIComponent(vhost);\r\n\r\n return `${protocol}://${username}:${password}@${host}:${port}/${encodedVhost}`;\r\n }\r\n\r\n /**\r\n * Handle reconnection\r\n */\r\n private async handleReconnect(): Promise<void> {\r\n const delay = this.options.reconnectDelay ?? 5000;\r\n let attempt = 0;\r\n\r\n const tryReconnect = async () => {\r\n attempt++;\r\n this.events.emit(\"reconnecting\", attempt);\r\n\r\n try {\r\n await this.connect();\r\n } catch {\r\n setTimeout(tryReconnect, delay);\r\n }\r\n };\r\n\r\n setTimeout(tryReconnect, delay);\r\n }\r\n\r\n /**\r\n * Disconnect from RabbitMQ\r\n */\r\n public async disconnect(): Promise<void> {\r\n if (this.amqpChannel) {\r\n try {\r\n await this.amqpChannel.close();\r\n } catch {\r\n // Ignore close errors\r\n }\r\n this.amqpChannel = null;\r\n }\r\n\r\n if (this.connection) {\r\n try {\r\n await this.connection.close();\r\n } catch {\r\n // Ignore close errors\r\n }\r\n this.connection = null;\r\n }\r\n\r\n this._isConnected = false;\r\n this.events.emit(\"disconnected\");\r\n }\r\n\r\n /**\r\n * Register event listener\r\n */\r\n public on(event: BrokerEvent, listener: BrokerEventListener): void {\r\n this.events.on(event, listener as any);\r\n }\r\n\r\n /**\r\n * Remove event listener\r\n */\r\n public off(event: BrokerEvent, listener: BrokerEventListener): void {\r\n this.events.off(event, listener as any);\r\n }\r\n\r\n /**\r\n * Get or create a channel\r\n */\r\n public channel<TPayload = unknown>(\r\n name: string,\r\n options?: ChannelOptions<TPayload>,\r\n ): ChannelContract<TPayload> {\r\n // Check cache\r\n const existing = this.channels.get(name);\r\n if (existing) {\r\n return existing as ChannelContract<TPayload>;\r\n }\r\n\r\n // Create new channel\r\n const channel = new RabbitMQChannel<TPayload>(name, this.amqpChannel, options);\r\n\r\n this.channels.set(name, channel);\r\n return channel;\r\n }\r\n\r\n /**\r\n * Start consuming messages\r\n */\r\n public async startConsuming(): Promise<void> {\r\n // Channels start consuming when subscribe() is called\r\n // This method is for batch start if needed\r\n }\r\n\r\n /**\r\n * Stop consuming messages from all subscribed channels.\r\n * Gracefully cancels all active consumers.\r\n */\r\n public async stopConsuming(): Promise<void> {\r\n const stops = Array.from(this.channels.values()).map(channel =>\r\n (channel as RabbitMQChannel<any>).stopConsuming(),\r\n );\r\n await Promise.all(stops);\r\n }\r\n\r\n /**\r\n * Health check\r\n */\r\n public async healthCheck(): Promise<HealthCheckResult> {\r\n if (!this._isConnected || !this.connection) {\r\n return {\r\n healthy: false,\r\n error: \"Not connected to RabbitMQ\",\r\n };\r\n }\r\n\r\n const start = Date.now();\r\n\r\n try {\r\n // Simple check - verify channel is open\r\n await this.amqpChannel.checkQueue(\"amq.rabbitmq.reply-to\").catch(() => {\r\n // Queue might not exist, but if we get here, connection is alive\r\n });\r\n\r\n return {\r\n healthy: true,\r\n latency: Date.now() - start,\r\n };\r\n } catch (error) {\r\n return {\r\n healthy: false,\r\n error: error instanceof Error ? error.message : String(error),\r\n latency: Date.now() - start,\r\n };\r\n }\r\n }\r\n\r\n /**\r\n * Get all channel names\r\n */\r\n public getChannelNames(): string[] {\r\n return Array.from(this.channels.keys());\r\n }\r\n\r\n /**\r\n * Close a specific channel\r\n */\r\n public async closeChannel(name: string): Promise<void> {\r\n const channel = this.channels.get(name);\r\n if (channel) {\r\n await channel.delete();\r\n this.channels.delete(name);\r\n }\r\n }\r\n\r\n /**\r\n * Get the raw AMQP channel (for advanced use)\r\n */\r\n public getRawChannel(): any {\r\n return this.amqpChannel;\r\n }\r\n\r\n /**\r\n * Get the raw connection (for advanced use)\r\n */\r\n public getRawConnection(): any {\r\n return this.connection;\r\n }\r\n}\r\n","import { Broker, brokerRegistry } from \"../communicators\";\r\nimport type { BrokerDriverContract, ChannelContract } from \"../contracts\";\r\nimport { EventConsumerClass, EventMessage } from \"../message-managers\";\r\nimport type { ChannelOptions, ConnectionOptions, RabbitMQConnectionOptions } from \"../types\";\r\n\r\n/**\r\n * Connect to a message broker and register it.\r\n *\r\n * This is a high-level utility function that simplifies connection setup\r\n * for most projects. It handles driver instantiation, connection,\r\n * broker creation, and automatic registration.\r\n *\r\n * **Supported Drivers:**\r\n * - `rabbitmq` (default) - RabbitMQ/AMQP driver\r\n * - `kafka` - Apache Kafka driver (coming soon)\r\n *\r\n * @param options - Connection configuration options\r\n * @returns A connected and registered Broker instance\r\n * @throws {Error} If connection fails or driver is not implemented\r\n *\r\n * @example\r\n * ```typescript\r\n * // RabbitMQ connection\r\n * const broker = await connectToBroker({\r\n * driver: \"rabbitmq\",\r\n * host: \"localhost\",\r\n * port: 5672,\r\n * username: \"guest\",\r\n * password: \"guest\",\r\n * });\r\n *\r\n * // Use the broker\r\n * await broker.channel(\"user.created\").publish({ userId: 1 });\r\n * ```\r\n *\r\n * @example\r\n * ```typescript\r\n * // Multiple brokers\r\n * await connectToBroker({\r\n * driver: \"rabbitmq\",\r\n * name: \"notifications\",\r\n * isDefault: true,\r\n * host: process.env.RABBITMQ_HOST,\r\n * });\r\n *\r\n * await connectToBroker({\r\n * driver: \"rabbitmq\",\r\n * name: \"analytics\",\r\n * host: process.env.ANALYTICS_RABBITMQ_HOST,\r\n * });\r\n *\r\n * // Use default broker\r\n * herald().channel(\"notifications\").publish({ ... });\r\n *\r\n * // Use specific broker\r\n * herald(\"analytics\").channel(\"events\").publish({ ... });\r\n * ```\r\n */\r\nexport async function connectToBroker(options: ConnectionOptions): Promise<Broker> {\r\n // Default values\r\n const driverType = options.driver ?? \"rabbitmq\";\r\n const brokerName = options.name ?? \"default\";\r\n const isDefault = options.isDefault ?? true;\r\n\r\n // Create driver based on type\r\n let driver: BrokerDriverContract;\r\n\r\n switch (driverType) {\r\n case \"rabbitmq\": {\r\n const rabbitOptions = options as RabbitMQConnectionOptions;\r\n // Dynamic import to avoid requiring amqplib if not used\r\n const { RabbitMQDriver } = await import(\"../drivers/rabbitmq/rabbitmq-driver\");\r\n driver = new RabbitMQDriver(rabbitOptions);\r\n break;\r\n }\r\n\r\n case \"kafka\": {\r\n // const kafkaOptions = options as KafkaConnectionOptions;\r\n // Dynamic import to avoid requiring kafkajs if not used\r\n throw new Error(\r\n \"Kafka driver is not yet implemented. Coming soon! For now, please use RabbitMQ.\",\r\n );\r\n }\r\n\r\n default:\r\n throw new Error(`Unknown driver: \"${driverType}\". Supported drivers: rabbitmq, kafka`);\r\n }\r\n\r\n // Create broker\r\n const broker = brokerRegistry.register({\r\n name: brokerName,\r\n driver,\r\n isDefault,\r\n });\r\n\r\n // Connect to the message broker\r\n try {\r\n await driver.connect();\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to connect to ${driverType}: ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n }\r\n\r\n return broker;\r\n}\r\n\r\n/**\r\n * Get a broker by name or the default one.\r\n *\r\n * This is the main entry point for using brokers in your application.\r\n * Named after the package — `herald()` carries your messages!\r\n *\r\n * @param name - Optional broker name (uses default if not provided)\r\n * @returns Broker instance\r\n * @throws MissingBrokerError if broker not found\r\n *\r\n * @example\r\n * // Get default broker\r\n * const channel = herald().channel(\"user.created\");\r\n * await channel.publish({ userId: 1 });\r\n *\r\n * // Get specific broker\r\n * const analyticsChannel = herald(\"analytics\").channel(\"events\");\r\n * await analyticsChannel.publish({ event: \"page_view\" });\r\n *\r\n * // Subscribe to messages\r\n * herald()\r\n * .channel<UserPayload>(\"user.created\")\r\n * .subscribe(async (message, ctx) => {\r\n * console.log(\"User created:\", message.payload);\r\n * await ctx.ack();\r\n * });\r\n * ```\r\n */\r\nexport function herald(name?: string): Broker {\r\n return brokerRegistry.get(name);\r\n}\r\n\r\n/**\r\n * Get channel instance for the given name from default broker.\r\n *\r\n * Shorthand for `herald().channel(name, options)`.\r\n *\r\n * @param name - Channel name\r\n * @param options - Optional channel options\r\n * @returns Channel instance\r\n * @throws MissingBrokerError if broker not found\r\n *\r\n * @example\r\n * ```typescript\r\n * const channel = heraldChannel(\"user.created\");\r\n * await channel.publish({ userId: 1 });\r\n * ```\r\n */\r\nexport function heraldChannel<TPayload = unknown>(\r\n name: string,\r\n options?: ChannelOptions<TPayload>,\r\n): ChannelContract<TPayload> {\r\n return herald().channel<TPayload>(name, options);\r\n}\r\n\r\n/**\r\n * Publish an EventMessage to the default broker.\r\n *\r\n * @param event - Event message to publish\r\n * @returns Promise that resolves when the event is published\r\n * @throws Error if the broker is not connected\r\n *\r\n * @example\r\n * ```typescript\r\n * await publishEvent(new UserUpdatedEvent({ id: 1, name: \"John Doe\" }));\r\n * ```\r\n */\r\nexport async function publishEvent<TPayload = Record<string, any>>(event: EventMessage<TPayload>) {\r\n return herald().publish(event);\r\n}\r\n\r\n/**\r\n * Subscribe an EventConsumer class to the default broker.\r\n *\r\n * @param Consumer - Event consumer class\r\n * @returns Unsubscribe function\r\n * @throws MissingBrokerError if broker not found\r\n *\r\n * @example\r\n * ```typescript\r\n * await subscribeConsumer(UserUpdatedConsumer);\r\n * ```\r\n */\r\nexport async function subscribeConsumer<TPayload = Record<string, any>>(\r\n Consumer: EventConsumerClass<TPayload>,\r\n) {\r\n return herald().subscribe(Consumer);\r\n}\r\n","import { brokerRegistry } from \"../communicators\";\nimport { type EventConsumerClass } from \"../message-managers/types\";\n\nexport type ConsumableOptions = {\n broker?: string;\n};\n\nexport const pendingSubscribers = new Set<{\n Consumer: EventConsumerClass;\n options?: ConsumableOptions;\n}>();\n\n/**\n * Register the consumer to the broker\n */\nexport function Consumable(options?: ConsumableOptions) {\n return function (target: EventConsumerClass) {\n const brokerName = options?.broker;\n\n try {\n const currentBroker = brokerRegistry.get(brokerName);\n\n // if broker is connected, subscribe the consumer\n if (currentBroker?.isConnected) {\n currentBroker.subscribe(target);\n } else {\n pendingSubscribers.add({ Consumer: target, options });\n }\n } catch {\n // mostly it will be an error that broker is not registered yet\n // then add it to the pending subscribers\n pendingSubscribers.add({ Consumer: target, options });\n }\n };\n}\n\n// Register pending consumers on broker's connection is done\nbrokerRegistry.on(\"connected\", (broker) => {\n for (const { Consumer, options } of pendingSubscribers) {\n if (options?.broker && broker.name !== options.broker) {\n continue;\n }\n\n broker.subscribe(Consumer);\n }\n});\n","/**\n * This class is used to be part of the Herald Event Consumer Manager.\n * It should be used to consume events from Either RabbitMQ or Kafka through Herald\n *\n * It's highly recommended using it instead of declaring manual channel namd and subscribing to event\n */\nimport { v, ValidationResult, type ObjectValidator } from \"@warlock.js/seal\";\nimport { randomUUID } from \"crypto\";\nimport { Consumable } from \"../decorators\";\nimport { ConsumedEventMessage, EventConsumerClass } from \"./types\";\n\nexport abstract class EventConsumer<Payload = Record<string, any>> {\n /**\n * Event name\n */\n public static eventName: string;\n\n private static _consumerId?: string;\n\n public static get consumerId(): string {\n if (!this._consumerId) {\n this._consumerId = randomUUID();\n }\n return this._consumerId;\n }\n\n public get eventName() {\n return (this.constructor as typeof EventConsumer).eventName;\n }\n\n /**\n * Min version accepted to be consumed by this class\n */\n public static minVersion?: number;\n\n /**\n * Max version accepted to be consumed by this class\n */\n public static maxVersion?: number;\n\n /**\n * Payload validation to auto reject the received event before accessing it in the handle method\n */\n public schema?: ObjectValidator;\n\n /**\n * The method that will be called when the event is received\n */\n public abstract handle(payload: Payload, event: ConsumedEventMessage): Promise<void>;\n\n /**\n * Determine whether this is accepted version to be used by this consumer\n */\n public static isAcceptedVersion(version: number): boolean {\n if (this.minVersion && version < this.minVersion) return false;\n if (this.maxVersion && version > this.maxVersion) return false;\n\n return true;\n }\n\n /**\n * Validate the given data\n */\n public async validate(data: Payload): Promise<ValidationResult | void> {\n if (!this.schema) return;\n\n return await v.validate(this.schema, data);\n }\n}\n\n/**\n * Define Consumer options\n */\ntype ConsumerOptions<Payload> = {\n /**\n * Payload validation to auto reject the received event before accessing it in the handle method\n */\n schema?: ObjectValidator;\n /**\n * Handle data\n */\n handle: (payload: Payload, event: ConsumedEventMessage) => Promise<void>;\n /**\n * Validate the payload before executing `handle`\n */\n validate?: (payload: Payload) => Promise<ValidationResult | boolean>;\n};\n\n/**\n * A shorthand to define an event consumer without declaring an entire class\n */\nexport function defineConsumer<Payload = Record<string, any>>(\n eventName: string,\n options: ConsumerOptions<Payload>,\n): EventConsumerClass {\n const Class = class AnnouncedConsumer extends EventConsumer<Payload> {\n public static eventName = eventName;\n public schema = options.schema;\n\n public async handle(payload: Payload, event: ConsumedEventMessage) {\n if (options.validate) {\n const result = await options.validate(payload);\n if (!result || !(result as ValidationResult).isValid) return;\n }\n\n return options.handle(payload, event);\n }\n };\n\n Consumable()(Class as EventConsumerClass);\n\n return Class as EventConsumerClass;\n}\n","/**\n * This class is used to be part of the Herald Event Message Manager.\n * It should be used to trigger events to Either RabbitMQ or Kafka through Herald\n *\n * It's highly recommended using it instead of declaring manual channel namd and publishing data\n */\nimport { GenericObject } from \"@mongez/reinforcements\";\nimport { type ObjectValidator } from \"@warlock.js/seal\";\nimport { randomUUID } from \"crypto\";\n\nexport abstract class EventMessage<TPayload = Record<string, any>> {\n /**\n * Event Name\n */\n public abstract eventName: string;\n\n /**\n * Event version\n */\n public version?: number;\n\n /**\n * Additional metadata (if any)\n */\n public metadata?: Record<string, any>;\n\n /**\n * Event Message id\n */\n public messageId?: string;\n\n /**\n * Schema of payload that will be used to determine whether this event should be published\n */\n public schema?: ObjectValidator;\n\n /**\n * Data that will be sent with the event (Payload)\n */\n public toJSON(): TPayload {\n if (!this.data) {\n throw new Error(`no Data is defined for Event: ${this.eventName}`);\n }\n\n return this.data as TPayload;\n }\n\n public constructor(protected data?: TPayload) {}\n\n /**\n * Serialize the event to be ready for publishing.\n * Delegates payload resolution to toJSON() — override toJSON() to customize.\n *\n * @throws Error if toJSON() throws (e.g. no data provided)\n */\n public serialize() {\n const payload = this.toJSON();\n\n return {\n payload,\n metadata: this.metadata,\n messageId: this.messageId ?? randomUUID(),\n eventName: this.eventName,\n version: this.version,\n occurredAt: new Date(),\n __through: \"EventMessage\",\n };\n }\n}\n\ntype EventOptions<T> = {\n /**\n * Shapen the data that will be used\n */\n toJSON?: (data: T) => GenericObject;\n /**\n * Validation schema\n */\n schema?: ObjectValidator;\n};\n\n/**\n * Represents an EventMessage class constructor.\n *\n * @template TIncoming - The type of data accepted by the constructor\n * @template TOutgoing - The type of data returned by toJSON() (defaults to TIncoming)\n */\ntype EventMessageClass<TIncoming = Record<string, any>, TOutgoing = TIncoming> = new (\n data?: TIncoming,\n) => EventMessage<TOutgoing>;\n\n/**\n * A shorthand to define an event without declaring an entire class.\n *\n * This factory function creates an EventMessage subclass that transforms\n * input data (IncomingData) into a different output format (OutgoingData).\n *\n * @template IncomingData - The type of data passed to the constructor\n * @template OutgoingData - The type of data returned by toJSON()\n *\n * @example\n * ```typescript\n * const UserCreatedEvent = defineEvent<User, { id: number; name: string }>(\n * \"user.created\",\n * { toJSON: (user) => user.only([\"id\", \"name\"]) }\n * );\n *\n * publishEvent(new UserCreatedEvent(user));\n * ```\n */\nexport function defineEvent<IncomingData = unknown, OutgoingData = unknown>(\n eventName: string,\n options: EventOptions<IncomingData> = {},\n): EventMessageClass<IncomingData, OutgoingData> {\n // We need to use `any` here to bridge the IncomingData -> OutgoingData transformation\n // The class accepts IncomingData in constructor but outputs OutgoingData via toJSON()\n return class AnnouncedEvent extends EventMessage<OutgoingData> {\n public eventName = eventName;\n public schema = options.schema;\n\n public constructor(data?: IncomingData) {\n super(data as any);\n }\n\n public toJSON(): OutgoingData {\n if (!options.toJSON) return this.data as OutgoingData;\n\n return options.toJSON(this.data as IncomingData) as OutgoingData;\n }\n };\n}\n","import { herald } from \"./utils/connect-to-broker\";\n\n/**\n * Minimal mirror of `@warlock.js/core`'s `UseCaseBroadcastEvent`.\n *\n * Kept local on purpose: this adapter is **structurally typed** so `@warlock.js/herald`\n * takes no dependency on `@warlock.js/core`. The shape only needs the fields the\n * adapter reads (`event` for the channel, `payload` for the body).\n */\nexport type UseCaseBroadcastEvent = {\n useCase: string;\n event: string;\n id: string;\n at: Date;\n payload: unknown;\n};\n\n/**\n * Channel adapter that publishes use-case broadcast events onto a herald broker.\n *\n * Register it in the use-cases config so successful use cases fan out to the bus:\n *\n * @example\n * // src/config/use-cases.ts\n * import { heraldBroadcast } from \"@warlock.js/herald\";\n *\n * export default {\n * broadcast: {\n * enabled: true,\n * channels: [heraldBroadcast({ broker: \"default\" })],\n * },\n * } satisfies UseCaseConfigurations;\n *\n * @param options - Optional broker name (defaults to the default broker)\n */\nexport function heraldBroadcast(options?: { broker?: string }) {\n return {\n async broadcast(event: UseCaseBroadcastEvent): Promise<void> {\n await herald(options?.broker).channel(event.event).publish(event.payload);\n },\n };\n}\n"],"mappings":"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"}
@@ -55,6 +55,28 @@ declare class RabbitMQChannel<TPayload = unknown> implements ChannelContract<TPa
55
55
  * Send message to dead-letter queue
56
56
  */
57
57
  private sendToDeadLetter;
58
+ /**
59
+ * Dead-letter a message whose body couldn't be parsed into a {@link Message}
60
+ * (e.g. `JSON.parse` itself threw before an envelope existed). Forwards the
61
+ * raw AMQP bytes/properties as-is rather than re-deriving a payload, so a
62
+ * malformed message isn't lost.
63
+ */
64
+ private sendToDeadLetterRaw;
65
+ /**
66
+ * Bounded retry shared by the explicit `ctx.retry()` call and the automatic
67
+ * catch when a handler throws without calling it itself — so both paths
68
+ * honor the same cap instead of the automatic path silently requeueing
69
+ * forever (see `subscribe()`'s catch block).
70
+ *
71
+ * Under the cap: republishes with an incremented `x-retry-count` header —
72
+ * NOT a plain `nack(msg, false, true)`, which redelivers the original
73
+ * message untouched and never advances the counter.
74
+ *
75
+ * At/over the cap: dead-letters if configured, otherwise drops the message
76
+ * with a loud `log.error` (never a silent drop) so an operator can see a
77
+ * poison message was discarded instead of it vanishing without a trace.
78
+ */
79
+ private retryOrGiveUp;
58
80
  /**
59
81
  * Request-response pattern
60
82
  */
@@ -1 +1 @@
1
- {"version":3,"file":"rabbitmq-channel.d.mts","names":[],"sources":["../../../../../../../../herald/src/drivers/rabbitmq/rabbitmq-channel.ts"],"mappings":";;;;;;;;;;;;;cAwBa,eAAA,gCAA+C,eAAA,CAAgB,QAAA;EAAA,SAC1D,IAAA;EAAA,SACA,OAAA,EAAS,cAAA,CAAe,QAAA;EAAA,iBAEvB,WAAA;EAAA,iBACA,aAAA;EAAA,QACT,QAAA;EAKoE;;;cAAzD,IAAA,UAAc,WAAA,OAAkB,OAAA,GAAU,cAAA,CAAe,QAAA;EA6B1B;;;EApBrC,MAAA,IAAU,OAAA;EAyEoD;;;EArD9D,OAAA,CAAQ,OAAA,EAAS,QAAA,EAAU,OAAA,GAAU,cAAA,GAAiB,OAAA;EAsExD;;;EAjBE,YAAA,CAAa,QAAA,EAAU,QAAA,IAAY,OAAA,GAAU,cAAA,GAAiB,OAAA;EA4OhE;;;;;;;;EA9NE,SAAA,CACX,OAAA,EAAS,cAAA,CAAe,QAAA,GACxB,OAAA,GAAU,gBAAA,GACT,OAAA,CAAQ,YAAA;EA8RmB;;;EA5GjB,eAAA,CAAgB,UAAA,WAAqB,OAAA;EAiJ3B;;;;EArIV,aAAA,IAAiB,OAAA;EA5S4B;;;EAAA,QAsT5C,gBAAA;EApTW;;;EAsUZ,OAAA,sBACX,OAAA,EAAS,QAAA,EACT,OAAA,GAAU,cAAA,GACT,OAAA,CAAQ,SAAA;EArUH;;;EAyXK,OAAA,sBACX,OAAA,EAAS,eAAA,CAAgB,QAAA,EAAU,SAAA,IAClC,OAAA,CAAQ,YAAA;EAtXkD;;;EAiYhD,KAAA,IAAS,OAAA,CAAQ,YAAA;EAxXP;;;EAuYV,KAAA,IAAS,OAAA;EAnX4B;;;EA6XrC,MAAA,IAAU,OAAA;EAxUa;;;EAoVvB,MAAA,IAAU,OAAA;AAAA"}
1
+ {"version":3,"file":"rabbitmq-channel.d.mts","names":[],"sources":["../../../../../../../../herald/src/drivers/rabbitmq/rabbitmq-channel.ts"],"mappings":";;;;;;;;;;;;;cAyBa,eAAA,gCAA+C,eAAA,CAAgB,QAAA;EAAA,SAC1D,IAAA;EAAA,SACA,OAAA,EAAS,cAAA,CAAe,QAAA;EAAA,iBAEvB,WAAA;EAAA,iBACA,aAAA;EAAA,QACT,QAAA;EAKoE;;;cAAzD,IAAA,UAAc,WAAA,OAAkB,OAAA,GAAU,cAAA,CAAe,QAAA;EA6B1B;;;EApBrC,MAAA,IAAU,OAAA;EAyEoD;;;EArD9D,OAAA,CAAQ,OAAA,EAAS,QAAA,EAAU,OAAA,GAAU,cAAA,GAAiB,OAAA;EAsExD;;;EAjBE,YAAA,CAAa,QAAA,EAAU,QAAA,IAAY,OAAA,GAAU,cAAA,GAAiB,OAAA;EAqShE;;;;;;;;EAvRE,SAAA,CACX,OAAA,EAAS,cAAA,CAAe,QAAA,GACxB,OAAA,GAAU,gBAAA,GACT,OAAA,CAAQ,YAAA;EAuVmB;;;EAvLjB,eAAA,CAAgB,UAAA,WAAqB,OAAA;EA4N3B;;;;EAhNV,aAAA,IAAiB,OAAA;EA1R4B;;;EAAA,QAoS5C,gBAAA;EAlSW;;;;;;EAAA,QAuTjB,mBAAA;EA9SyB;;;;;;;;;;;;;;EAAA,QAmUnB,aAAA;EAjP4C;;;EAoS7C,OAAA,sBACX,OAAA,EAAS,QAAA,EACT,OAAA,GAAU,cAAA,GACT,OAAA,CAAQ,SAAA;EAxRA;;;EA4UE,OAAA,sBACX,OAAA,EAAS,eAAA,CAAgB,QAAA,EAAU,SAAA,IAClC,OAAA,CAAQ,YAAA;EA7UT;;;EAwVW,KAAA,IAAS,OAAA,CAAQ,YAAA;EAvLD;;;EAsMhB,KAAA,IAAS,OAAA;EAhLR;;;EA0LD,MAAA,IAAU,OAAA;EA7FF;;;EAyGR,MAAA,IAAU,OAAA;AAAA"}
@@ -1,3 +1,4 @@
1
+ import { log } from "@warlock.js/logger";
1
2
  import { v } from "@warlock.js/seal";
2
3
  import { randomUUID } from "node:crypto";
3
4
 
@@ -104,6 +105,7 @@ var RabbitMQChannel = class {
104
105
  const { consumerTag } = await this.amqpChannel.consume(this.name, async (msg) => {
105
106
  if (!msg) return;
106
107
  let ackHandled = isFireAndForget;
108
+ let parsedMessage;
107
109
  try {
108
110
  const content = JSON.parse(msg.content.toString());
109
111
  let payload = content.payload;
@@ -134,6 +136,7 @@ var RabbitMQChannel = class {
134
136
  payload,
135
137
  raw: msg
136
138
  };
139
+ parsedMessage = message;
137
140
  await handler(message, {
138
141
  ack: async () => {
139
142
  if (!ackHandled) {
@@ -169,31 +172,16 @@ var RabbitMQChannel = class {
169
172
  retry: async (delay) => {
170
173
  if (ackHandled) return;
171
174
  ackHandled = true;
172
- const retryCount = (metadata.retryCount || 0) + 1;
173
- if (retryCount > (options?.retry?.maxRetries ?? 3)) {
174
- if (options?.deadLetter) await this.sendToDeadLetter(message, options.deadLetter.channel);
175
- this.amqpChannel.ack(msg);
176
- return;
177
- }
178
- const headers = {
179
- ...msg.properties.headers,
180
- "x-retry-count": retryCount
181
- };
182
- if (delay) headers["x-delay"] = delay;
183
- this.amqpChannel.sendToQueue(this.name, msg.content, {
184
- ...msg.properties,
185
- headers
186
- });
187
- this.amqpChannel.ack(msg);
175
+ await this.retryOrGiveUp(msg, metadata.retryCount || 0, options, message, delay);
188
176
  }
189
177
  });
190
178
  if (!ackHandled) this.amqpChannel.ack(msg);
191
179
  } catch (error) {
192
180
  if (ackHandled) return;
193
- if (options?.retry) if ((msg.properties.headers?.["x-retry-count"] || 0) < options.retry.maxRetries) this.amqpChannel.nack(msg, false, true);
194
- else if (options.deadLetter) this.amqpChannel.nack(msg, false, false);
195
- else this.amqpChannel.reject(msg, false);
196
- else this.amqpChannel.nack(msg, false, false);
181
+ if (options?.retry) {
182
+ const currentRetryCount = msg.properties.headers?.["x-retry-count"] || 0;
183
+ await this.retryOrGiveUp(msg, currentRetryCount, options, parsedMessage);
184
+ } else this.amqpChannel.nack(msg, false, false);
197
185
  }
198
186
  }, consumerOptions);
199
187
  const subscription = new RabbitMQSubscription(subscriptionId, this.name, consumerTag, this.amqpChannel);
@@ -232,6 +220,61 @@ var RabbitMQChannel = class {
232
220
  this.amqpChannel.sendToQueue(deadLetterChannel, Buffer.from(content), { persistent: true });
233
221
  }
234
222
  /**
223
+ * Dead-letter a message whose body couldn't be parsed into a {@link Message}
224
+ * (e.g. `JSON.parse` itself threw before an envelope existed). Forwards the
225
+ * raw AMQP bytes/properties as-is rather than re-deriving a payload, so a
226
+ * malformed message isn't lost.
227
+ */
228
+ sendToDeadLetterRaw(msg, deadLetterChannel) {
229
+ this.amqpChannel.sendToQueue(deadLetterChannel, msg.content, {
230
+ ...msg.properties,
231
+ persistent: true
232
+ });
233
+ }
234
+ /**
235
+ * Bounded retry shared by the explicit `ctx.retry()` call and the automatic
236
+ * catch when a handler throws without calling it itself — so both paths
237
+ * honor the same cap instead of the automatic path silently requeueing
238
+ * forever (see `subscribe()`'s catch block).
239
+ *
240
+ * Under the cap: republishes with an incremented `x-retry-count` header —
241
+ * NOT a plain `nack(msg, false, true)`, which redelivers the original
242
+ * message untouched and never advances the counter.
243
+ *
244
+ * At/over the cap: dead-letters if configured, otherwise drops the message
245
+ * with a loud `log.error` (never a silent drop) so an operator can see a
246
+ * poison message was discarded instead of it vanishing without a trace.
247
+ */
248
+ async retryOrGiveUp(msg, currentRetryCount, options, parsedMessage, delay) {
249
+ const retryCount = currentRetryCount + 1;
250
+ const maxRetries = options?.retry?.maxRetries ?? 3;
251
+ if (retryCount > maxRetries) {
252
+ if (options?.deadLetter) if (parsedMessage) await this.sendToDeadLetter(parsedMessage, options.deadLetter.channel);
253
+ else this.sendToDeadLetterRaw(msg, options.deadLetter.channel);
254
+ else {
255
+ log.error("herald", "poison-message", `Dropping message on channel "${this.name}" after ${retryCount - 1} failed ${retryCount - 1 === 1 ? "retry" : "retries"} (maxRetries: ${maxRetries}) with no dead-letter channel configured.`, {
256
+ channel: this.name,
257
+ retryCount: retryCount - 1,
258
+ maxRetries
259
+ });
260
+ this.amqpChannel.reject(msg, false);
261
+ return;
262
+ }
263
+ this.amqpChannel.ack(msg);
264
+ return;
265
+ }
266
+ const headers = {
267
+ ...msg.properties.headers,
268
+ "x-retry-count": retryCount
269
+ };
270
+ if (delay) headers["x-delay"] = delay;
271
+ this.amqpChannel.sendToQueue(this.name, msg.content, {
272
+ ...msg.properties,
273
+ headers
274
+ });
275
+ this.amqpChannel.ack(msg);
276
+ }
277
+ /**
235
278
  * Request-response pattern
236
279
  */
237
280
  async request(payload, options) {
@@ -1 +1 @@
1
- {"version":3,"file":"rabbitmq-channel.mjs","names":[],"sources":["../../../../../../../../herald/src/drivers/rabbitmq/rabbitmq-channel.ts"],"sourcesContent":["import { v } from \"@warlock.js/seal\";\r\nimport { randomUUID } from \"node:crypto\";\r\nimport type { ChannelContract } from \"../../contracts\";\r\nimport type {\r\n ChannelOptions,\r\n ChannelStats,\r\n Message,\r\n MessageContext,\r\n MessageHandler,\r\n MessageMetadata,\r\n PublishOptions,\r\n RequestOptions,\r\n ResponseHandler,\r\n SubscribeOptions,\r\n Subscription,\r\n} from \"../../types\";\r\n\r\n/**\r\n * RabbitMQ Channel Implementation\r\n *\r\n * Wraps a RabbitMQ queue/exchange with a unified API.\r\n *\r\n * @template TPayload - The typed payload\r\n */\r\nexport class RabbitMQChannel<TPayload = unknown> implements ChannelContract<TPayload> {\r\n public readonly name: string;\r\n public readonly options: ChannelOptions<TPayload>;\r\n\r\n private readonly amqpChannel: any;\r\n private readonly subscriptions = new Map<string, RabbitMQSubscription>();\r\n private asserted = false;\r\n\r\n /**\r\n * Create a new RabbitMQ channel\r\n */\r\n public constructor(name: string, amqpChannel: any, options?: ChannelOptions<TPayload>) {\r\n this.name = name;\r\n this.amqpChannel = amqpChannel;\r\n this.options = options ?? {};\r\n }\r\n\r\n /**\r\n * Assert the queue exists\r\n */\r\n public async assert(): Promise<void> {\r\n if (this.asserted) return;\r\n\r\n const queueOptions = {\r\n durable: this.options.durable ?? true,\r\n autoDelete: this.options.autoDelete ?? false,\r\n exclusive: this.options.exclusive ?? false,\r\n messageTtl: this.options.messageTtl,\r\n maxLength: this.options.maxLength,\r\n deadLetterExchange: this.options.deadLetter?.channel ? \"\" : undefined,\r\n deadLetterRoutingKey: this.options.deadLetter?.channel,\r\n };\r\n\r\n await this.amqpChannel.assertQueue(this.name, queueOptions);\r\n this.asserted = true;\r\n }\r\n\r\n /**\r\n * Publish a message\r\n */\r\n public async publish(payload: TPayload, options?: PublishOptions): Promise<void> {\r\n await this.assert();\r\n\r\n // Validate with schema if provided\r\n if (this.options.schema) {\r\n const context = {\r\n allValues: payload,\r\n value: payload,\r\n };\r\n const result = await v.validate(this.options.schema, payload, { context });\r\n if (!result.isValid) {\r\n throw new Error(`Message validation failed: ${JSON.stringify(result.errors)}`);\r\n }\r\n payload = result.data as TPayload;\r\n }\r\n\r\n const messageId = randomUUID();\r\n const timestamp = new Date();\r\n\r\n const messageContent = JSON.stringify({\r\n payload,\r\n metadata: {\r\n messageId,\r\n timestamp: timestamp.toISOString(),\r\n correlationId: options?.correlationId,\r\n headers: options?.headers,\r\n },\r\n });\r\n\r\n const publishOptions: any = {\r\n persistent: options?.persistent ?? true,\r\n messageId,\r\n timestamp: timestamp.getTime(),\r\n correlationId: options?.correlationId,\r\n expiration: options?.expiration?.toString(),\r\n priority: options?.priority,\r\n headers: options?.headers,\r\n };\r\n\r\n // Handle delayed messages (requires rabbitmq-delayed-message-exchange plugin)\r\n if (options?.delay) {\r\n publishOptions.headers = {\r\n ...publishOptions.headers,\r\n \"x-delay\": options.delay,\r\n };\r\n }\r\n\r\n this.amqpChannel.sendToQueue(this.name, Buffer.from(messageContent), publishOptions);\r\n }\r\n\r\n /**\r\n * Publish multiple messages\r\n */\r\n public async publishBatch(messages: TPayload[], options?: PublishOptions): Promise<void> {\r\n for (const payload of messages) {\r\n await this.publish(payload, options);\r\n }\r\n }\r\n\r\n /**\r\n * Subscribe to messages\r\n *\r\n * Smart auto-ack behavior (when autoAck is not true):\r\n * - If handler completes successfully without explicit ack/nack/reject → auto-ack\r\n * - If handler throws an error → auto-nack (with retry if configured)\r\n * - If handler explicitly calls ack/nack/reject → respects that call\r\n */\r\n public async subscribe(\r\n handler: MessageHandler<TPayload>,\r\n options?: SubscribeOptions,\r\n ): Promise<Subscription> {\r\n await this.assert();\r\n\r\n // Use consumerId from options if provided, otherwise generate a random one\r\n const subscriptionId = options?.consumerId ?? randomUUID();\r\n\r\n // Set prefetch if specified\r\n if (options?.prefetch) {\r\n await this.amqpChannel.prefetch(options.prefetch);\r\n }\r\n\r\n // If autoAck is true, RabbitMQ handles ack immediately (fire-and-forget)\r\n const isFireAndForget = options?.autoAck === true;\r\n\r\n const consumerOptions = {\r\n noAck: isFireAndForget,\r\n exclusive: options?.exclusive ?? false,\r\n consumerTag: options?.group ?? subscriptionId,\r\n };\r\n\r\n const { consumerTag } = await this.amqpChannel.consume(\r\n this.name,\r\n async (msg: any) => {\r\n if (!msg) return;\r\n\r\n // Track if acknowledgment was handled explicitly\r\n let ackHandled = isFireAndForget;\r\n\r\n try {\r\n const content = JSON.parse(msg.content.toString());\r\n let payload = content.payload as TPayload;\r\n\r\n // Validate with schema if provided\r\n if (this.options.schema) {\r\n const context = {\r\n allValues: payload,\r\n value: payload,\r\n };\r\n const result = await v.validate(this.options.schema, payload, { context });\r\n if (!result.isValid) {\r\n // Reject invalid messages\r\n this.amqpChannel.nack(msg, false, false);\r\n return;\r\n }\r\n payload = result.data as TPayload;\r\n }\r\n\r\n const metadata: MessageMetadata = {\r\n messageId: msg.properties.messageId || content.metadata?.messageId || randomUUID(),\r\n timestamp: new Date(msg.properties.timestamp || content.metadata?.timestamp),\r\n correlationId: msg.properties.correlationId || content.metadata?.correlationId,\r\n replyTo: msg.properties.replyTo,\r\n priority: msg.properties.priority,\r\n headers: msg.properties.headers,\r\n retryCount: msg.properties.headers?.[\"x-retry-count\"] || 0,\r\n originalChannel: this.name,\r\n };\r\n\r\n const message: Message<TPayload> = {\r\n metadata,\r\n payload,\r\n raw: msg,\r\n };\r\n\r\n const context: MessageContext = {\r\n ack: async () => {\r\n if (!ackHandled) {\r\n ackHandled = true;\r\n this.amqpChannel.ack(msg);\r\n }\r\n },\r\n nack: async (requeue = true) => {\r\n if (!ackHandled) {\r\n ackHandled = true;\r\n this.amqpChannel.nack(msg, false, requeue);\r\n }\r\n },\r\n reject: async () => {\r\n if (!ackHandled) {\r\n ackHandled = true;\r\n this.amqpChannel.reject(msg, false);\r\n }\r\n },\r\n reply: async <T>(replyPayload: T) => {\r\n if (msg.properties.replyTo) {\r\n const replyContent = JSON.stringify({\r\n payload: replyPayload,\r\n metadata: {\r\n messageId: randomUUID(),\r\n timestamp: new Date().toISOString(),\r\n correlationId: msg.properties.correlationId,\r\n },\r\n });\r\n\r\n this.amqpChannel.sendToQueue(msg.properties.replyTo, Buffer.from(replyContent), {\r\n correlationId: msg.properties.correlationId,\r\n });\r\n }\r\n },\r\n retry: async (delay?: number) => {\r\n if (ackHandled) return;\r\n ackHandled = true;\r\n\r\n const retryCount = (metadata.retryCount || 0) + 1;\r\n const maxRetries = options?.retry?.maxRetries ?? 3;\r\n\r\n if (retryCount > maxRetries) {\r\n // Send to dead-letter if configured\r\n if (options?.deadLetter) {\r\n await this.sendToDeadLetter(message, options.deadLetter.channel);\r\n }\r\n this.amqpChannel.ack(msg);\r\n return;\r\n }\r\n\r\n // Republish with retry count\r\n const headers = {\r\n ...msg.properties.headers,\r\n \"x-retry-count\": retryCount,\r\n };\r\n\r\n if (delay) {\r\n headers[\"x-delay\"] = delay;\r\n }\r\n\r\n this.amqpChannel.sendToQueue(this.name, msg.content, { ...msg.properties, headers });\r\n\r\n this.amqpChannel.ack(msg);\r\n },\r\n };\r\n\r\n // Execute handler\r\n await handler(message, context);\r\n\r\n // Smart auto-ack: if handler succeeded and didn't explicitly handle ack\r\n if (!ackHandled) {\r\n this.amqpChannel.ack(msg);\r\n }\r\n } catch (error) {\r\n // Smart auto-nack: if handler threw and didn't explicitly handle ack\r\n if (ackHandled) return;\r\n\r\n // Handle errors - nack and potentially retry\r\n if (options?.retry) {\r\n const retryCount = msg.properties.headers?.[\"x-retry-count\"] || 0;\r\n if (retryCount < options.retry.maxRetries) {\r\n // Requeue for retry\r\n this.amqpChannel.nack(msg, false, true);\r\n } else if (options.deadLetter) {\r\n // Send to dead-letter\r\n this.amqpChannel.nack(msg, false, false);\r\n } else {\r\n this.amqpChannel.reject(msg, false);\r\n }\r\n } else {\r\n // No retry configured - reject without requeue\r\n this.amqpChannel.nack(msg, false, false);\r\n }\r\n }\r\n },\r\n consumerOptions,\r\n );\r\n\r\n const subscription = new RabbitMQSubscription(\r\n subscriptionId,\r\n this.name,\r\n consumerTag,\r\n this.amqpChannel,\r\n );\r\n\r\n this.subscriptions.set(subscriptionId, subscription);\r\n\r\n return subscription;\r\n }\r\n\r\n /**\r\n * Unsubscribe by consumer ID\r\n */\r\n public async unsubscribeById(consumerId: string): Promise<void> {\r\n const subscription = this.subscriptions.get(consumerId);\r\n if (subscription) {\r\n await subscription.unsubscribe();\r\n this.subscriptions.delete(consumerId);\r\n }\r\n }\r\n\r\n /**\r\n * Stop consuming messages on this channel.\r\n * Cancels all active subscriptions gracefully.\r\n */\r\n public async stopConsuming(): Promise<void> {\r\n const cancellations = Array.from(this.subscriptions.values()).map(sub =>\r\n sub.unsubscribe(),\r\n );\r\n await Promise.all(cancellations);\r\n }\r\n\r\n /**\r\n * Send message to dead-letter queue\r\n */\r\n private async sendToDeadLetter(\r\n message: Message<TPayload>,\r\n deadLetterChannel: string,\r\n ): Promise<void> {\r\n const content = JSON.stringify({\r\n payload: message.payload,\r\n metadata: {\r\n ...message.metadata,\r\n originalChannel: this.name,\r\n },\r\n });\r\n\r\n this.amqpChannel.sendToQueue(deadLetterChannel, Buffer.from(content), { persistent: true });\r\n }\r\n\r\n /**\r\n * Request-response pattern\r\n */\r\n public async request<TResponse = unknown>(\r\n payload: TPayload,\r\n options?: RequestOptions,\r\n ): Promise<TResponse> {\r\n await this.assert();\r\n\r\n const correlationId = randomUUID();\r\n const timeout = options?.timeout ?? 30000;\r\n\r\n // Create exclusive reply queue\r\n const { queue: replyQueue } = await this.amqpChannel.assertQueue(\"\", {\r\n exclusive: true,\r\n autoDelete: true,\r\n });\r\n\r\n return new Promise<TResponse>((resolve, reject) => {\r\n const timeoutId = setTimeout(() => {\r\n reject(new Error(`Request timeout after ${timeout}ms`));\r\n }, timeout);\r\n\r\n // Consume reply\r\n this.amqpChannel.consume(\r\n replyQueue,\r\n (msg: any) => {\r\n if (msg?.properties.correlationId === correlationId) {\r\n clearTimeout(timeoutId);\r\n const content = JSON.parse(msg.content.toString());\r\n resolve(content.payload as TResponse);\r\n }\r\n },\r\n { noAck: true },\r\n );\r\n\r\n // Send request\r\n const messageContent = JSON.stringify({\r\n payload,\r\n metadata: {\r\n messageId: randomUUID(),\r\n timestamp: new Date().toISOString(),\r\n correlationId,\r\n },\r\n });\r\n\r\n this.amqpChannel.sendToQueue(this.name, Buffer.from(messageContent), {\r\n correlationId,\r\n replyTo: replyQueue,\r\n expiration: timeout.toString(),\r\n ...options,\r\n });\r\n });\r\n }\r\n\r\n /**\r\n * Register response handler for RPC\r\n */\r\n public async respond<TResponse = unknown>(\r\n handler: ResponseHandler<TPayload, TResponse>,\r\n ): Promise<Subscription> {\r\n return this.subscribe(async (message, ctx) => {\r\n const response = await handler(message, ctx);\r\n await ctx.reply(response);\r\n await ctx.ack();\r\n });\r\n }\r\n\r\n /**\r\n * Get queue statistics\r\n */\r\n public async stats(): Promise<ChannelStats> {\r\n await this.assert();\r\n\r\n const queueInfo = await this.amqpChannel.checkQueue(this.name);\r\n\r\n return {\r\n name: this.name,\r\n messageCount: queueInfo.messageCount,\r\n consumerCount: queueInfo.consumerCount,\r\n };\r\n }\r\n\r\n /**\r\n * Purge all messages\r\n */\r\n public async purge(): Promise<number> {\r\n await this.assert();\r\n\r\n const result = await this.amqpChannel.purgeQueue(this.name);\r\n return result.messageCount;\r\n }\r\n\r\n /**\r\n * Check if queue exists\r\n */\r\n public async exists(): Promise<boolean> {\r\n try {\r\n await this.amqpChannel.checkQueue(this.name);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Delete the queue\r\n */\r\n public async delete(): Promise<void> {\r\n // Cancel all subscriptions\r\n for (const subscription of this.subscriptions.values()) {\r\n await subscription.unsubscribe();\r\n }\r\n this.subscriptions.clear();\r\n\r\n try {\r\n await this.amqpChannel.deleteQueue(this.name);\r\n } catch {\r\n // Ignore if queue doesn't exist\r\n }\r\n\r\n this.asserted = false;\r\n }\r\n}\r\n\r\n/**\r\n * RabbitMQ Subscription Implementation\r\n */\r\nclass RabbitMQSubscription implements Subscription {\r\n public readonly id: string;\r\n public readonly channel: string;\r\n public readonly consumerTag: string;\r\n\r\n private readonly amqpChannel: any;\r\n private _isActive = true;\r\n\r\n public constructor(id: string, channel: string, consumerTag: string, amqpChannel: any) {\r\n this.id = id;\r\n this.channel = channel;\r\n this.consumerTag = consumerTag;\r\n this.amqpChannel = amqpChannel;\r\n }\r\n\r\n public async unsubscribe(): Promise<void> {\r\n if (!this._isActive) return;\r\n\r\n await this.amqpChannel.cancel(this.consumerTag);\r\n this._isActive = false;\r\n }\r\n\r\n public async pause(): Promise<void> {\r\n // RabbitMQ doesn't have native pause, cancel consumer\r\n await this.amqpChannel.cancel(this.consumerTag);\r\n }\r\n\r\n public async resume(): Promise<void> {\r\n // Would need to re-subscribe - not directly supported\r\n throw new Error(\"Resume is not supported for RabbitMQ. Please create a new subscription.\");\r\n }\r\n\r\n public isActive(): boolean {\r\n return this._isActive;\r\n }\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"rabbitmq-channel.mjs","names":[],"sources":["../../../../../../../../herald/src/drivers/rabbitmq/rabbitmq-channel.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\r\nimport { v } from \"@warlock.js/seal\";\r\nimport { randomUUID } from \"node:crypto\";\r\nimport type { ChannelContract } from \"../../contracts\";\r\nimport type {\r\n ChannelOptions,\r\n ChannelStats,\r\n Message,\r\n MessageContext,\r\n MessageHandler,\r\n MessageMetadata,\r\n PublishOptions,\r\n RequestOptions,\r\n ResponseHandler,\r\n SubscribeOptions,\r\n Subscription,\r\n} from \"../../types\";\r\n\r\n/**\r\n * RabbitMQ Channel Implementation\r\n *\r\n * Wraps a RabbitMQ queue/exchange with a unified API.\r\n *\r\n * @template TPayload - The typed payload\r\n */\r\nexport class RabbitMQChannel<TPayload = unknown> implements ChannelContract<TPayload> {\r\n public readonly name: string;\r\n public readonly options: ChannelOptions<TPayload>;\r\n\r\n private readonly amqpChannel: any;\r\n private readonly subscriptions = new Map<string, RabbitMQSubscription>();\r\n private asserted = false;\r\n\r\n /**\r\n * Create a new RabbitMQ channel\r\n */\r\n public constructor(name: string, amqpChannel: any, options?: ChannelOptions<TPayload>) {\r\n this.name = name;\r\n this.amqpChannel = amqpChannel;\r\n this.options = options ?? {};\r\n }\r\n\r\n /**\r\n * Assert the queue exists\r\n */\r\n public async assert(): Promise<void> {\r\n if (this.asserted) return;\r\n\r\n const queueOptions = {\r\n durable: this.options.durable ?? true,\r\n autoDelete: this.options.autoDelete ?? false,\r\n exclusive: this.options.exclusive ?? false,\r\n messageTtl: this.options.messageTtl,\r\n maxLength: this.options.maxLength,\r\n deadLetterExchange: this.options.deadLetter?.channel ? \"\" : undefined,\r\n deadLetterRoutingKey: this.options.deadLetter?.channel,\r\n };\r\n\r\n await this.amqpChannel.assertQueue(this.name, queueOptions);\r\n this.asserted = true;\r\n }\r\n\r\n /**\r\n * Publish a message\r\n */\r\n public async publish(payload: TPayload, options?: PublishOptions): Promise<void> {\r\n await this.assert();\r\n\r\n // Validate with schema if provided\r\n if (this.options.schema) {\r\n const context = {\r\n allValues: payload,\r\n value: payload,\r\n };\r\n const result = await v.validate(this.options.schema, payload, { context });\r\n if (!result.isValid) {\r\n throw new Error(`Message validation failed: ${JSON.stringify(result.errors)}`);\r\n }\r\n payload = result.data as TPayload;\r\n }\r\n\r\n const messageId = randomUUID();\r\n const timestamp = new Date();\r\n\r\n const messageContent = JSON.stringify({\r\n payload,\r\n metadata: {\r\n messageId,\r\n timestamp: timestamp.toISOString(),\r\n correlationId: options?.correlationId,\r\n headers: options?.headers,\r\n },\r\n });\r\n\r\n const publishOptions: any = {\r\n persistent: options?.persistent ?? true,\r\n messageId,\r\n timestamp: timestamp.getTime(),\r\n correlationId: options?.correlationId,\r\n expiration: options?.expiration?.toString(),\r\n priority: options?.priority,\r\n headers: options?.headers,\r\n };\r\n\r\n // Handle delayed messages (requires rabbitmq-delayed-message-exchange plugin)\r\n if (options?.delay) {\r\n publishOptions.headers = {\r\n ...publishOptions.headers,\r\n \"x-delay\": options.delay,\r\n };\r\n }\r\n\r\n this.amqpChannel.sendToQueue(this.name, Buffer.from(messageContent), publishOptions);\r\n }\r\n\r\n /**\r\n * Publish multiple messages\r\n */\r\n public async publishBatch(messages: TPayload[], options?: PublishOptions): Promise<void> {\r\n for (const payload of messages) {\r\n await this.publish(payload, options);\r\n }\r\n }\r\n\r\n /**\r\n * Subscribe to messages\r\n *\r\n * Smart auto-ack behavior (when autoAck is not true):\r\n * - If handler completes successfully without explicit ack/nack/reject → auto-ack\r\n * - If handler throws an error → auto-nack (with retry if configured)\r\n * - If handler explicitly calls ack/nack/reject → respects that call\r\n */\r\n public async subscribe(\r\n handler: MessageHandler<TPayload>,\r\n options?: SubscribeOptions,\r\n ): Promise<Subscription> {\r\n await this.assert();\r\n\r\n // Use consumerId from options if provided, otherwise generate a random one\r\n const subscriptionId = options?.consumerId ?? randomUUID();\r\n\r\n // Set prefetch if specified\r\n if (options?.prefetch) {\r\n await this.amqpChannel.prefetch(options.prefetch);\r\n }\r\n\r\n // If autoAck is true, RabbitMQ handles ack immediately (fire-and-forget)\r\n const isFireAndForget = options?.autoAck === true;\r\n\r\n const consumerOptions = {\r\n noAck: isFireAndForget,\r\n exclusive: options?.exclusive ?? false,\r\n consumerTag: options?.group ?? subscriptionId,\r\n };\r\n\r\n const { consumerTag } = await this.amqpChannel.consume(\r\n this.name,\r\n async (msg: any) => {\r\n if (!msg) return;\r\n\r\n // Track if acknowledgment was handled explicitly\r\n let ackHandled = isFireAndForget;\r\n\r\n // Populated once the message is successfully parsed, so the catch\r\n // block below can dead-letter with the full envelope. Stays\r\n // `undefined` when `JSON.parse` itself is what threw.\r\n let parsedMessage: Message<TPayload> | undefined;\r\n\r\n try {\r\n const content = JSON.parse(msg.content.toString());\r\n let payload = content.payload as TPayload;\r\n\r\n // Validate with schema if provided\r\n if (this.options.schema) {\r\n const context = {\r\n allValues: payload,\r\n value: payload,\r\n };\r\n const result = await v.validate(this.options.schema, payload, { context });\r\n if (!result.isValid) {\r\n // Reject invalid messages\r\n this.amqpChannel.nack(msg, false, false);\r\n return;\r\n }\r\n payload = result.data as TPayload;\r\n }\r\n\r\n const metadata: MessageMetadata = {\r\n messageId: msg.properties.messageId || content.metadata?.messageId || randomUUID(),\r\n timestamp: new Date(msg.properties.timestamp || content.metadata?.timestamp),\r\n correlationId: msg.properties.correlationId || content.metadata?.correlationId,\r\n replyTo: msg.properties.replyTo,\r\n priority: msg.properties.priority,\r\n headers: msg.properties.headers,\r\n retryCount: msg.properties.headers?.[\"x-retry-count\"] || 0,\r\n originalChannel: this.name,\r\n };\r\n\r\n const message: Message<TPayload> = {\r\n metadata,\r\n payload,\r\n raw: msg,\r\n };\r\n\r\n parsedMessage = message;\r\n\r\n const context: MessageContext = {\r\n ack: async () => {\r\n if (!ackHandled) {\r\n ackHandled = true;\r\n this.amqpChannel.ack(msg);\r\n }\r\n },\r\n nack: async (requeue = true) => {\r\n if (!ackHandled) {\r\n ackHandled = true;\r\n this.amqpChannel.nack(msg, false, requeue);\r\n }\r\n },\r\n reject: async () => {\r\n if (!ackHandled) {\r\n ackHandled = true;\r\n this.amqpChannel.reject(msg, false);\r\n }\r\n },\r\n reply: async <T>(replyPayload: T) => {\r\n if (msg.properties.replyTo) {\r\n const replyContent = JSON.stringify({\r\n payload: replyPayload,\r\n metadata: {\r\n messageId: randomUUID(),\r\n timestamp: new Date().toISOString(),\r\n correlationId: msg.properties.correlationId,\r\n },\r\n });\r\n\r\n this.amqpChannel.sendToQueue(msg.properties.replyTo, Buffer.from(replyContent), {\r\n correlationId: msg.properties.correlationId,\r\n });\r\n }\r\n },\r\n retry: async (delay?: number) => {\r\n if (ackHandled) return;\r\n ackHandled = true;\r\n\r\n await this.retryOrGiveUp(msg, metadata.retryCount || 0, options, message, delay);\r\n },\r\n };\r\n\r\n // Execute handler\r\n await handler(message, context);\r\n\r\n // Smart auto-ack: if handler succeeded and didn't explicitly handle ack\r\n if (!ackHandled) {\r\n this.amqpChannel.ack(msg);\r\n }\r\n } catch (error) {\r\n // Smart auto-nack: if handler threw and didn't explicitly handle ack\r\n if (ackHandled) return;\r\n\r\n if (options?.retry) {\r\n // A bare `nack(msg, false, true)` redelivers the ORIGINAL message\r\n // untouched — amqplib/RabbitMQ do not add an `x-retry-count`\r\n // header on requeue, so a plain requeue here never advances the\r\n // counter and `maxRetries`/`deadLetter` are silently never\r\n // reached. Route through the same bounded-retry path `ctx.retry()`\r\n // uses, so the counter increments (and the cap/dead-letter fires)\r\n // on this automatic path too.\r\n const currentRetryCount = msg.properties.headers?.[\"x-retry-count\"] || 0;\r\n await this.retryOrGiveUp(msg, currentRetryCount, options, parsedMessage);\r\n } else {\r\n // No retry configured - reject without requeue (already bounded:\r\n // a single attempt, no requeue loop possible).\r\n this.amqpChannel.nack(msg, false, false);\r\n }\r\n }\r\n },\r\n consumerOptions,\r\n );\r\n\r\n const subscription = new RabbitMQSubscription(\r\n subscriptionId,\r\n this.name,\r\n consumerTag,\r\n this.amqpChannel,\r\n );\r\n\r\n this.subscriptions.set(subscriptionId, subscription);\r\n\r\n return subscription;\r\n }\r\n\r\n /**\r\n * Unsubscribe by consumer ID\r\n */\r\n public async unsubscribeById(consumerId: string): Promise<void> {\r\n const subscription = this.subscriptions.get(consumerId);\r\n if (subscription) {\r\n await subscription.unsubscribe();\r\n this.subscriptions.delete(consumerId);\r\n }\r\n }\r\n\r\n /**\r\n * Stop consuming messages on this channel.\r\n * Cancels all active subscriptions gracefully.\r\n */\r\n public async stopConsuming(): Promise<void> {\r\n const cancellations = Array.from(this.subscriptions.values()).map(sub =>\r\n sub.unsubscribe(),\r\n );\r\n await Promise.all(cancellations);\r\n }\r\n\r\n /**\r\n * Send message to dead-letter queue\r\n */\r\n private async sendToDeadLetter(\r\n message: Message<TPayload>,\r\n deadLetterChannel: string,\r\n ): Promise<void> {\r\n const content = JSON.stringify({\r\n payload: message.payload,\r\n metadata: {\r\n ...message.metadata,\r\n originalChannel: this.name,\r\n },\r\n });\r\n\r\n this.amqpChannel.sendToQueue(deadLetterChannel, Buffer.from(content), { persistent: true });\r\n }\r\n\r\n /**\r\n * Dead-letter a message whose body couldn't be parsed into a {@link Message}\r\n * (e.g. `JSON.parse` itself threw before an envelope existed). Forwards the\r\n * raw AMQP bytes/properties as-is rather than re-deriving a payload, so a\r\n * malformed message isn't lost.\r\n */\r\n private sendToDeadLetterRaw(msg: any, deadLetterChannel: string): void {\r\n this.amqpChannel.sendToQueue(deadLetterChannel, msg.content, {\r\n ...msg.properties,\r\n persistent: true,\r\n });\r\n }\r\n\r\n /**\r\n * Bounded retry shared by the explicit `ctx.retry()` call and the automatic\r\n * catch when a handler throws without calling it itself — so both paths\r\n * honor the same cap instead of the automatic path silently requeueing\r\n * forever (see `subscribe()`'s catch block).\r\n *\r\n * Under the cap: republishes with an incremented `x-retry-count` header —\r\n * NOT a plain `nack(msg, false, true)`, which redelivers the original\r\n * message untouched and never advances the counter.\r\n *\r\n * At/over the cap: dead-letters if configured, otherwise drops the message\r\n * with a loud `log.error` (never a silent drop) so an operator can see a\r\n * poison message was discarded instead of it vanishing without a trace.\r\n */\r\n private async retryOrGiveUp(\r\n msg: any,\r\n currentRetryCount: number,\r\n options: SubscribeOptions | undefined,\r\n parsedMessage: Message<TPayload> | undefined,\r\n delay?: number,\r\n ): Promise<void> {\r\n const retryCount = currentRetryCount + 1;\r\n const maxRetries = options?.retry?.maxRetries ?? 3;\r\n\r\n if (retryCount > maxRetries) {\r\n if (options?.deadLetter) {\r\n if (parsedMessage) {\r\n await this.sendToDeadLetter(parsedMessage, options.deadLetter.channel);\r\n } else {\r\n this.sendToDeadLetterRaw(msg, options.deadLetter.channel);\r\n }\r\n } else {\r\n log.error(\r\n \"herald\",\r\n \"poison-message\",\r\n `Dropping message on channel \"${this.name}\" after ${retryCount - 1} failed ` +\r\n `${retryCount - 1 === 1 ? \"retry\" : \"retries\"} (maxRetries: ${maxRetries}) with no ` +\r\n `dead-letter channel configured.`,\r\n { channel: this.name, retryCount: retryCount - 1, maxRetries },\r\n );\r\n this.amqpChannel.reject(msg, false);\r\n return;\r\n }\r\n\r\n this.amqpChannel.ack(msg);\r\n return;\r\n }\r\n\r\n const headers: Record<string, unknown> = {\r\n ...msg.properties.headers,\r\n \"x-retry-count\": retryCount,\r\n };\r\n\r\n if (delay) {\r\n headers[\"x-delay\"] = delay;\r\n }\r\n\r\n this.amqpChannel.sendToQueue(this.name, msg.content, { ...msg.properties, headers });\r\n\r\n this.amqpChannel.ack(msg);\r\n }\r\n\r\n /**\r\n * Request-response pattern\r\n */\r\n public async request<TResponse = unknown>(\r\n payload: TPayload,\r\n options?: RequestOptions,\r\n ): Promise<TResponse> {\r\n await this.assert();\r\n\r\n const correlationId = randomUUID();\r\n const timeout = options?.timeout ?? 30000;\r\n\r\n // Create exclusive reply queue\r\n const { queue: replyQueue } = await this.amqpChannel.assertQueue(\"\", {\r\n exclusive: true,\r\n autoDelete: true,\r\n });\r\n\r\n return new Promise<TResponse>((resolve, reject) => {\r\n const timeoutId = setTimeout(() => {\r\n reject(new Error(`Request timeout after ${timeout}ms`));\r\n }, timeout);\r\n\r\n // Consume reply\r\n this.amqpChannel.consume(\r\n replyQueue,\r\n (msg: any) => {\r\n if (msg?.properties.correlationId === correlationId) {\r\n clearTimeout(timeoutId);\r\n const content = JSON.parse(msg.content.toString());\r\n resolve(content.payload as TResponse);\r\n }\r\n },\r\n { noAck: true },\r\n );\r\n\r\n // Send request\r\n const messageContent = JSON.stringify({\r\n payload,\r\n metadata: {\r\n messageId: randomUUID(),\r\n timestamp: new Date().toISOString(),\r\n correlationId,\r\n },\r\n });\r\n\r\n this.amqpChannel.sendToQueue(this.name, Buffer.from(messageContent), {\r\n correlationId,\r\n replyTo: replyQueue,\r\n expiration: timeout.toString(),\r\n ...options,\r\n });\r\n });\r\n }\r\n\r\n /**\r\n * Register response handler for RPC\r\n */\r\n public async respond<TResponse = unknown>(\r\n handler: ResponseHandler<TPayload, TResponse>,\r\n ): Promise<Subscription> {\r\n return this.subscribe(async (message, ctx) => {\r\n const response = await handler(message, ctx);\r\n await ctx.reply(response);\r\n await ctx.ack();\r\n });\r\n }\r\n\r\n /**\r\n * Get queue statistics\r\n */\r\n public async stats(): Promise<ChannelStats> {\r\n await this.assert();\r\n\r\n const queueInfo = await this.amqpChannel.checkQueue(this.name);\r\n\r\n return {\r\n name: this.name,\r\n messageCount: queueInfo.messageCount,\r\n consumerCount: queueInfo.consumerCount,\r\n };\r\n }\r\n\r\n /**\r\n * Purge all messages\r\n */\r\n public async purge(): Promise<number> {\r\n await this.assert();\r\n\r\n const result = await this.amqpChannel.purgeQueue(this.name);\r\n return result.messageCount;\r\n }\r\n\r\n /**\r\n * Check if queue exists\r\n */\r\n public async exists(): Promise<boolean> {\r\n try {\r\n await this.amqpChannel.checkQueue(this.name);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n }\r\n\r\n /**\r\n * Delete the queue\r\n */\r\n public async delete(): Promise<void> {\r\n // Cancel all subscriptions\r\n for (const subscription of this.subscriptions.values()) {\r\n await subscription.unsubscribe();\r\n }\r\n this.subscriptions.clear();\r\n\r\n try {\r\n await this.amqpChannel.deleteQueue(this.name);\r\n } catch {\r\n // Ignore if queue doesn't exist\r\n }\r\n\r\n this.asserted = false;\r\n }\r\n}\r\n\r\n/**\r\n * RabbitMQ Subscription Implementation\r\n */\r\nclass RabbitMQSubscription implements Subscription {\r\n public readonly id: string;\r\n public readonly channel: string;\r\n public readonly consumerTag: string;\r\n\r\n private readonly amqpChannel: any;\r\n private _isActive = true;\r\n\r\n public constructor(id: string, channel: string, consumerTag: string, amqpChannel: any) {\r\n this.id = id;\r\n this.channel = channel;\r\n this.consumerTag = consumerTag;\r\n this.amqpChannel = amqpChannel;\r\n }\r\n\r\n public async unsubscribe(): Promise<void> {\r\n if (!this._isActive) return;\r\n\r\n await this.amqpChannel.cancel(this.consumerTag);\r\n this._isActive = false;\r\n }\r\n\r\n public async pause(): Promise<void> {\r\n // RabbitMQ doesn't have native pause, cancel consumer\r\n await this.amqpChannel.cancel(this.consumerTag);\r\n }\r\n\r\n public async resume(): Promise<void> {\r\n // Would need to re-subscribe - not directly supported\r\n throw new Error(\"Resume is not supported for RabbitMQ. Please create a new subscription.\");\r\n }\r\n\r\n public isActive(): boolean {\r\n return this._isActive;\r\n }\r\n}\r\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"rabbitmq-driver.d.mts","names":[],"sources":["../../../../../../../../herald/src/drivers/rabbitmq/rabbitmq-driver.ts"],"mappings":";;;;;;;;;;;;;;;AA2FA;;;;;;;;;;;;;;;cAAa,cAAA,YAA0B,oBAAA;EAAA,SACrB,IAAA;EAAA,SAEA,SAAA,EAAW,kBAAA;EAAA,iBAEV,OAAA;EAAA,iBACA,MAAA;EAAA,iBACA,QAAA;EAAA,QAET,UAAA;EAAA,QACA,WAAA;EAAA,QACA,YAAA;EAsSiC;;;;;cA/RtB,OAAA,EAAS,yBAAA;EAfZ;;;EAAA,IAsBL,WAAA;EAlBM;;;;;;;;;;EAgCV,SAAA,CAAU,QAAA,EAAU,kBAAA;EAsBpB;;;EAAA,WAAA,CAAY,QAAA,EAAU,kBAAA;EAcd;;;;EAAR,OAAA,YAAmB,MAAA,eAAqB,KAAA,EAAO,YAAA,CAAa,QAAA;EAOtD;;;EAAA,OAAA,IAAW,OAAA;EAyGX;;;EAAA,QAzCL,kBAAA;EAmEE;;;EAAA,QA/CI,eAAA;EAsDI;;;EAjCL,UAAA,IAAc,OAAA;EAwCpB;;;EAdA,EAAA,CAAG,KAAA,EAAO,WAAA,EAAa,QAAA,EAAU,mBAAA;EAgBb;;;EATpB,GAAA,CAAI,KAAA,EAAO,WAAA,EAAa,QAAA,EAAU,mBAAA;EA2B5B;;;EApBN,OAAA,qBACL,IAAA,UACA,OAAA,GAAU,cAAA,CAAe,QAAA,IACxB,eAAA,CAAgB,QAAA;EAoCN;;;EAnBA,cAAA,IAAkB,OAAA;EA0DlB;;;;EAjDA,aAAA,IAAiB,OAAA;EAmEP;AAAA;;EAzDV,WAAA,IAAe,OAAA,CAAQ,iBAAA;;;;EAgC7B,eAAA;;;;EAOM,YAAA,CAAa,IAAA,WAAe,OAAA;;;;EAWlC,aAAA;;;;EAOA,gBAAA;AAAA"}
1
+ {"version":3,"file":"rabbitmq-driver.d.mts","names":[],"sources":["../../../../../../../../herald/src/drivers/rabbitmq/rabbitmq-driver.ts"],"mappings":";;;;;;;;;;;;;;;AAwGA;;;;;;;;;;;;;;;cAAa,cAAA,YAA0B,oBAAA;EAAA,SACrB,IAAA;EAAA,SAEA,SAAA,EAAW,kBAAA;EAAA,iBAEV,OAAA;EAAA,iBACA,MAAA;EAAA,iBACA,QAAA;EAAA,QAET,UAAA;EAAA,QACA,WAAA;EAAA,QACA,YAAA;EAwSiC;;;;;cAjStB,OAAA,EAAS,yBAAA;EAfZ;;;EAAA,IAsBL,WAAA;EAlBM;;;;;;;;;;EAgCV,SAAA,CAAU,QAAA,EAAU,kBAAA;EAsBpB;;;EAAA,WAAA,CAAY,QAAA,EAAU,kBAAA;EAcd;;;;EAAR,OAAA,YAAmB,MAAA,eAAqB,KAAA,EAAO,YAAA,CAAa,QAAA;EAOtD;;;EAAA,OAAA,IAAW,OAAA;EA2GX;;;EAAA,QA5CL,kBAAA;EAsEE;;;EAAA,QA/CI,eAAA;EAsDI;;;EAjCL,UAAA,IAAc,OAAA;EAwCpB;;;EAdA,EAAA,CAAG,KAAA,EAAO,WAAA,EAAa,QAAA,EAAU,mBAAA;EAgBb;;;EATpB,GAAA,CAAI,KAAA,EAAO,WAAA,EAAa,QAAA,EAAU,mBAAA;EA2B5B;;;EApBN,OAAA,qBACL,IAAA,UACA,OAAA,GAAU,cAAA,CAAe,QAAA,IACxB,eAAA,CAAgB,QAAA;EAoCN;;;EAnBA,cAAA,IAAkB,OAAA;EA0DlB;;;;EAjDA,aAAA,IAAiB,OAAA;EAmEP;AAAA;;EAzDV,WAAA,IAAe,OAAA,CAAQ,iBAAA;;;;EAgC7B,eAAA;;;;EAOM,YAAA,CAAa,IAAA,WAAe,OAAA;;;;EAWlC,aAAA;;;;EAOA,gBAAA;AAAA"}
@@ -30,6 +30,18 @@ Or manually:
30
30
  yarn add amqplib
31
31
  `.trim();
32
32
  /**
33
+ * Strip `user:password@` credentials from any `amqp(s)://` URL embedded in a
34
+ * string. The connection URL carries plaintext broker credentials, and
35
+ * amqplib/Node's URL parser commonly echoes the offending URL verbatim in a
36
+ * malformed-URL error (e.g. an unencoded `@`/`:` in the password) — applied
37
+ * to every error `connect()` surfaces so a credential never reaches whatever
38
+ * the host app does with a thrown connection error (console.error,
39
+ * structured logging, an error tracker).
40
+ */
41
+ function redactAmqpCredentials(message) {
42
+ return message.replace(/(amqps?:\/\/)[^/@\s]+@/gi, "$1****:****@");
43
+ }
44
+ /**
33
45
  * Load amqplib, reusing the single shared load for every caller.
34
46
  *
35
47
  * @returns The amqplib module, or `undefined` when it is not installed.
@@ -146,7 +158,8 @@ var RabbitMQDriver = class {
146
158
  });
147
159
  } catch (error) {
148
160
  this._isConnected = false;
149
- throw new Error(`Failed to connect to RabbitMQ: ${error instanceof Error ? error.message : String(error)}`);
161
+ const message = error instanceof Error ? error.message : String(error);
162
+ throw new Error(`Failed to connect to RabbitMQ: ${redactAmqpCredentials(message)}`);
150
163
  }
151
164
  }
152
165
  /**
@@ -158,7 +171,7 @@ var RabbitMQDriver = class {
158
171
  const host = this.options.host ?? "localhost";
159
172
  const port = this.options.port ?? 5672;
160
173
  const vhost = this.options.vhost ?? "/";
161
- return `${protocol}://${this.options.username ?? "guest"}:${this.options.password ?? "guest"}@${host}:${port}/${encodeURIComponent(vhost)}`;
174
+ return `${protocol}://${encodeURIComponent(this.options.username ?? "guest")}:${encodeURIComponent(this.options.password ?? "guest")}@${host}:${port}/${encodeURIComponent(vhost)}`;
162
175
  }
163
176
  /**
164
177
  * Handle reconnection
@@ -1 +1 @@
1
- {"version":3,"file":"rabbitmq-driver.mjs","names":[],"sources":["../../../../../../../../herald/src/drivers/rabbitmq/rabbitmq-driver.ts"],"sourcesContent":["import { EventEmitter } from \"node:events\";\r\nimport type { BrokerDriverContract, ChannelContract } from \"../../contracts\";\r\nimport { EventMessage } from \"../../message-managers/event-message\";\r\nimport { EventConsumerClass } from \"../../message-managers/types\";\r\nimport type {\r\n BrokerDriverType,\r\n BrokerEvent,\r\n BrokerEventListener,\r\n ChannelOptions,\r\n HealthCheckResult,\r\n RabbitMQConnectionOptions,\r\n} from \"../../types\";\r\nimport { prepareConsumerSubscription } from \"./../../message-managers/prepare-consumer-subscription\";\r\nimport { RabbitMQChannel } from \"./rabbitmq-channel\";\r\n\r\n// ============================================================\r\n// Lazy-loaded amqplib Module\r\n// ============================================================\r\n\r\n/**\r\n * Shape of the lazily-imported amqplib module\r\n */\r\ntype AmqplibModule = typeof import(\"amqplib\");\r\n\r\n/**\r\n * The single amqplib load, shared by every caller.\r\n *\r\n * Memoized as a promise rather than as a resolved value so the loader stays\r\n * idempotent: without it, two callers arriving before the first `import()`\r\n * settles would each start their own load and the last writer would win, so a\r\n * caller could end up observing a module instance it never awaited.\r\n *\r\n * Resolves to `undefined` when amqplib is not installed.\r\n */\r\nlet amqplibModulePromise: Promise<AmqplibModule | undefined> | undefined;\r\n\r\n/**\r\n * Installation instructions for amqplib\r\n */\r\nconst AMQPLIB_INSTALL_INSTRUCTIONS = `\r\nRabbitMQ driver requires the amqplib package.\r\nInstall it with:\r\n\r\n npx warlock add herald --driver=rabbitmq\r\n\r\nOr manually:\r\n\r\n npm install amqplib\r\n pnpm add amqplib\r\n yarn add amqplib\r\n`.trim();\r\n\r\n/**\r\n * Load amqplib, reusing the single shared load for every caller.\r\n *\r\n * @returns The amqplib module, or `undefined` when it is not installed.\r\n */\r\nfunction loadAmqplibModule(): Promise<AmqplibModule | undefined> {\r\n if (!amqplibModulePromise) {\r\n amqplibModulePromise = import(\"amqplib\").catch(() => undefined);\r\n }\r\n\r\n return amqplibModulePromise;\r\n}\r\n\r\n// ============================================================\r\n// RabbitMQ Driver\r\n// ============================================================\r\n\r\n/**\r\n * RabbitMQ Driver\r\n *\r\n * Implementation of BrokerDriverContract for RabbitMQ/AMQP.\r\n *\r\n * **Important:** This driver requires the `amqplib` package to be installed.\r\n * Install it with: `npx warlock add herald --driver=rabbitmq` or `npm install amqplib`\r\n *\r\n * @example\r\n * ```typescript\r\n * const driver = new RabbitMQDriver({\r\n * driver: \"rabbitmq\",\r\n * host: \"localhost\",\r\n * port: 5672,\r\n * username: \"guest\",\r\n * password: \"guest\",\r\n * });\r\n *\r\n * await driver.connect();\r\n * const channel = driver.channel(\"user.created\");\r\n * ```\r\n */\r\nexport class RabbitMQDriver implements BrokerDriverContract {\r\n public readonly name = \"rabbitmq\" as const;\r\n\r\n public readonly consumers: EventConsumerClass[] = [];\r\n\r\n private readonly options: RabbitMQConnectionOptions;\r\n private readonly events = new EventEmitter();\r\n private readonly channels = new Map<string, ChannelContract<any>>();\r\n\r\n private connection: any = null;\r\n private amqpChannel: any = null;\r\n private _isConnected = false;\r\n\r\n /**\r\n * Create a new RabbitMQ driver\r\n *\r\n * @param options - RabbitMQ connection options\r\n */\r\n public constructor(options: RabbitMQConnectionOptions) {\r\n this.options = options;\r\n }\r\n\r\n /**\r\n * Whether connected to RabbitMQ\r\n */\r\n public get isConnected(): boolean {\r\n return this._isConnected;\r\n }\r\n\r\n /**\r\n * Subscribe the given consumer class to the driver\r\n *\r\n * @param consumer - Consumer class to subscribe\r\n *\r\n * @example\r\n * ```typescript\r\n * driver.subscribe(UserUpdatedConsumer);\r\n * ```\r\n */\r\n public subscribe(Consumer: EventConsumerClass) {\r\n if (this.isConnected) {\r\n this.channel(Consumer.eventName).subscribe(\r\n prepareConsumerSubscription(Consumer, (error, eventName) => {\r\n this.events.emit(\"error\", error, eventName);\r\n }),\r\n {\r\n consumerId: Consumer.consumerId,\r\n },\r\n );\r\n } else {\r\n this.consumers.push(Consumer);\r\n }\r\n\r\n return () => {\r\n this.unsubscribe(Consumer);\r\n };\r\n }\r\n\r\n /**\r\n * Unsubscribe the given consumer\r\n */\r\n public unsubscribe(Consumer: EventConsumerClass): void {\r\n if (this.isConnected) {\r\n this.channel(Consumer.eventName).unsubscribeById(Consumer.consumerId);\r\n }\r\n const index = this.consumers.indexOf(Consumer);\r\n if (index > -1) {\r\n this.consumers.splice(index, 1);\r\n }\r\n }\r\n\r\n /**\r\n * Publish the given event message.\r\n * Auto-creates the channel if it hasn't been accessed before.\r\n */\r\n public publish<TPayload = Record<string, any>>(event: EventMessage<TPayload>): void {\r\n this.channel(event.eventName).publish(event.serialize());\r\n }\r\n\r\n /**\r\n * Connect to RabbitMQ\r\n */\r\n public async connect(): Promise<void> {\r\n const amqplib = await loadAmqplibModule();\r\n\r\n if (!amqplib) {\r\n throw new Error(`amqplib is not installed.\\n\\n${AMQPLIB_INSTALL_INSTRUCTIONS}`);\r\n }\r\n\r\n try {\r\n // Build connection URL\r\n const url = this.buildConnectionUrl();\r\n\r\n // Build connection options merging our options with native client options\r\n const connectOptions = {\r\n heartbeat: this.options.heartbeat ?? 60,\r\n timeout: this.options.connectionTimeout,\r\n // Merge native amqplib client options\r\n ...this.options.clientOptions,\r\n };\r\n\r\n // Connect using cached module\r\n this.connection = await amqplib.connect(url, connectOptions);\r\n\r\n // Create channel\r\n this.amqpChannel = await this.connection.createChannel();\r\n\r\n // Set prefetch if specified\r\n if (this.options.prefetch) {\r\n await this.amqpChannel.prefetch(this.options.prefetch);\r\n }\r\n\r\n this._isConnected = true;\r\n this.events.emit(\"connected\");\r\n\r\n for (const consumer of this.consumers) {\r\n this.subscribe(consumer);\r\n }\r\n\r\n this.consumers.length = 0;\r\n\r\n // Handle connection close\r\n this.connection.on(\"close\", () => {\r\n this._isConnected = false;\r\n this.events.emit(\"disconnected\");\r\n\r\n if (this.options.reconnect !== false) {\r\n this.handleReconnect();\r\n }\r\n });\r\n\r\n // Handle errors\r\n this.connection.on(\"error\", (error: Error) => {\r\n this.events.emit(\"error\", error);\r\n });\r\n } catch (error) {\r\n this._isConnected = false;\r\n throw new Error(\r\n `Failed to connect to RabbitMQ: ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n }\r\n }\r\n\r\n /**\r\n * Build connection URL from options\r\n */\r\n private buildConnectionUrl(): string {\r\n if (this.options.uri) {\r\n return this.options.uri;\r\n }\r\n\r\n const protocol = \"amqp\";\r\n const host = this.options.host ?? \"localhost\";\r\n const port = this.options.port ?? 5672;\r\n const vhost = this.options.vhost ?? \"/\";\r\n const username = this.options.username ?? \"guest\";\r\n const password = this.options.password ?? \"guest\";\r\n\r\n const encodedVhost = encodeURIComponent(vhost);\r\n\r\n return `${protocol}://${username}:${password}@${host}:${port}/${encodedVhost}`;\r\n }\r\n\r\n /**\r\n * Handle reconnection\r\n */\r\n private async handleReconnect(): Promise<void> {\r\n const delay = this.options.reconnectDelay ?? 5000;\r\n let attempt = 0;\r\n\r\n const tryReconnect = async () => {\r\n attempt++;\r\n this.events.emit(\"reconnecting\", attempt);\r\n\r\n try {\r\n await this.connect();\r\n } catch {\r\n setTimeout(tryReconnect, delay);\r\n }\r\n };\r\n\r\n setTimeout(tryReconnect, delay);\r\n }\r\n\r\n /**\r\n * Disconnect from RabbitMQ\r\n */\r\n public async disconnect(): Promise<void> {\r\n if (this.amqpChannel) {\r\n try {\r\n await this.amqpChannel.close();\r\n } catch {\r\n // Ignore close errors\r\n }\r\n this.amqpChannel = null;\r\n }\r\n\r\n if (this.connection) {\r\n try {\r\n await this.connection.close();\r\n } catch {\r\n // Ignore close errors\r\n }\r\n this.connection = null;\r\n }\r\n\r\n this._isConnected = false;\r\n this.events.emit(\"disconnected\");\r\n }\r\n\r\n /**\r\n * Register event listener\r\n */\r\n public on(event: BrokerEvent, listener: BrokerEventListener): void {\r\n this.events.on(event, listener as any);\r\n }\r\n\r\n /**\r\n * Remove event listener\r\n */\r\n public off(event: BrokerEvent, listener: BrokerEventListener): void {\r\n this.events.off(event, listener as any);\r\n }\r\n\r\n /**\r\n * Get or create a channel\r\n */\r\n public channel<TPayload = unknown>(\r\n name: string,\r\n options?: ChannelOptions<TPayload>,\r\n ): ChannelContract<TPayload> {\r\n // Check cache\r\n const existing = this.channels.get(name);\r\n if (existing) {\r\n return existing as ChannelContract<TPayload>;\r\n }\r\n\r\n // Create new channel\r\n const channel = new RabbitMQChannel<TPayload>(name, this.amqpChannel, options);\r\n\r\n this.channels.set(name, channel);\r\n return channel;\r\n }\r\n\r\n /**\r\n * Start consuming messages\r\n */\r\n public async startConsuming(): Promise<void> {\r\n // Channels start consuming when subscribe() is called\r\n // This method is for batch start if needed\r\n }\r\n\r\n /**\r\n * Stop consuming messages from all subscribed channels.\r\n * Gracefully cancels all active consumers.\r\n */\r\n public async stopConsuming(): Promise<void> {\r\n const stops = Array.from(this.channels.values()).map(channel =>\r\n (channel as RabbitMQChannel<any>).stopConsuming(),\r\n );\r\n await Promise.all(stops);\r\n }\r\n\r\n /**\r\n * Health check\r\n */\r\n public async healthCheck(): Promise<HealthCheckResult> {\r\n if (!this._isConnected || !this.connection) {\r\n return {\r\n healthy: false,\r\n error: \"Not connected to RabbitMQ\",\r\n };\r\n }\r\n\r\n const start = Date.now();\r\n\r\n try {\r\n // Simple check - verify channel is open\r\n await this.amqpChannel.checkQueue(\"amq.rabbitmq.reply-to\").catch(() => {\r\n // Queue might not exist, but if we get here, connection is alive\r\n });\r\n\r\n return {\r\n healthy: true,\r\n latency: Date.now() - start,\r\n };\r\n } catch (error) {\r\n return {\r\n healthy: false,\r\n error: error instanceof Error ? error.message : String(error),\r\n latency: Date.now() - start,\r\n };\r\n }\r\n }\r\n\r\n /**\r\n * Get all channel names\r\n */\r\n public getChannelNames(): string[] {\r\n return Array.from(this.channels.keys());\r\n }\r\n\r\n /**\r\n * Close a specific channel\r\n */\r\n public async closeChannel(name: string): Promise<void> {\r\n const channel = this.channels.get(name);\r\n if (channel) {\r\n await channel.delete();\r\n this.channels.delete(name);\r\n }\r\n }\r\n\r\n /**\r\n * Get the raw AMQP channel (for advanced use)\r\n */\r\n public getRawChannel(): any {\r\n return this.amqpChannel;\r\n }\r\n\r\n /**\r\n * Get the raw connection (for advanced use)\r\n */\r\n public getRawConnection(): any {\r\n return this.connection;\r\n }\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"rabbitmq-driver.mjs","names":[],"sources":["../../../../../../../../herald/src/drivers/rabbitmq/rabbitmq-driver.ts"],"sourcesContent":["import { EventEmitter } from \"node:events\";\r\nimport type { BrokerDriverContract, ChannelContract } from \"../../contracts\";\r\nimport { EventMessage } from \"../../message-managers/event-message\";\r\nimport { EventConsumerClass } from \"../../message-managers/types\";\r\nimport type {\r\n BrokerDriverType,\r\n BrokerEvent,\r\n BrokerEventListener,\r\n ChannelOptions,\r\n HealthCheckResult,\r\n RabbitMQConnectionOptions,\r\n} from \"../../types\";\r\nimport { prepareConsumerSubscription } from \"./../../message-managers/prepare-consumer-subscription\";\r\nimport { RabbitMQChannel } from \"./rabbitmq-channel\";\r\n\r\n// ============================================================\r\n// Lazy-loaded amqplib Module\r\n// ============================================================\r\n\r\n/**\r\n * Shape of the lazily-imported amqplib module\r\n */\r\ntype AmqplibModule = typeof import(\"amqplib\");\r\n\r\n/**\r\n * The single amqplib load, shared by every caller.\r\n *\r\n * Memoized as a promise rather than as a resolved value so the loader stays\r\n * idempotent: without it, two callers arriving before the first `import()`\r\n * settles would each start their own load and the last writer would win, so a\r\n * caller could end up observing a module instance it never awaited.\r\n *\r\n * Resolves to `undefined` when amqplib is not installed.\r\n */\r\nlet amqplibModulePromise: Promise<AmqplibModule | undefined> | undefined;\r\n\r\n/**\r\n * Installation instructions for amqplib\r\n */\r\nconst AMQPLIB_INSTALL_INSTRUCTIONS = `\r\nRabbitMQ driver requires the amqplib package.\r\nInstall it with:\r\n\r\n npx warlock add herald --driver=rabbitmq\r\n\r\nOr manually:\r\n\r\n npm install amqplib\r\n pnpm add amqplib\r\n yarn add amqplib\r\n`.trim();\r\n\r\n/**\r\n * Strip `user:password@` credentials from any `amqp(s)://` URL embedded in a\r\n * string. The connection URL carries plaintext broker credentials, and\r\n * amqplib/Node's URL parser commonly echoes the offending URL verbatim in a\r\n * malformed-URL error (e.g. an unencoded `@`/`:` in the password) — applied\r\n * to every error `connect()` surfaces so a credential never reaches whatever\r\n * the host app does with a thrown connection error (console.error,\r\n * structured logging, an error tracker).\r\n */\r\nfunction redactAmqpCredentials(message: string): string {\r\n return message.replace(/(amqps?:\\/\\/)[^/@\\s]+@/gi, \"$1****:****@\");\r\n}\r\n\r\n/**\r\n * Load amqplib, reusing the single shared load for every caller.\r\n *\r\n * @returns The amqplib module, or `undefined` when it is not installed.\r\n */\r\nfunction loadAmqplibModule(): Promise<AmqplibModule | undefined> {\r\n if (!amqplibModulePromise) {\r\n amqplibModulePromise = import(\"amqplib\").catch(() => undefined);\r\n }\r\n\r\n return amqplibModulePromise;\r\n}\r\n\r\n// ============================================================\r\n// RabbitMQ Driver\r\n// ============================================================\r\n\r\n/**\r\n * RabbitMQ Driver\r\n *\r\n * Implementation of BrokerDriverContract for RabbitMQ/AMQP.\r\n *\r\n * **Important:** This driver requires the `amqplib` package to be installed.\r\n * Install it with: `npx warlock add herald --driver=rabbitmq` or `npm install amqplib`\r\n *\r\n * @example\r\n * ```typescript\r\n * const driver = new RabbitMQDriver({\r\n * driver: \"rabbitmq\",\r\n * host: \"localhost\",\r\n * port: 5672,\r\n * username: \"guest\",\r\n * password: \"guest\",\r\n * });\r\n *\r\n * await driver.connect();\r\n * const channel = driver.channel(\"user.created\");\r\n * ```\r\n */\r\nexport class RabbitMQDriver implements BrokerDriverContract {\r\n public readonly name = \"rabbitmq\" as const;\r\n\r\n public readonly consumers: EventConsumerClass[] = [];\r\n\r\n private readonly options: RabbitMQConnectionOptions;\r\n private readonly events = new EventEmitter();\r\n private readonly channels = new Map<string, ChannelContract<any>>();\r\n\r\n private connection: any = null;\r\n private amqpChannel: any = null;\r\n private _isConnected = false;\r\n\r\n /**\r\n * Create a new RabbitMQ driver\r\n *\r\n * @param options - RabbitMQ connection options\r\n */\r\n public constructor(options: RabbitMQConnectionOptions) {\r\n this.options = options;\r\n }\r\n\r\n /**\r\n * Whether connected to RabbitMQ\r\n */\r\n public get isConnected(): boolean {\r\n return this._isConnected;\r\n }\r\n\r\n /**\r\n * Subscribe the given consumer class to the driver\r\n *\r\n * @param consumer - Consumer class to subscribe\r\n *\r\n * @example\r\n * ```typescript\r\n * driver.subscribe(UserUpdatedConsumer);\r\n * ```\r\n */\r\n public subscribe(Consumer: EventConsumerClass) {\r\n if (this.isConnected) {\r\n this.channel(Consumer.eventName).subscribe(\r\n prepareConsumerSubscription(Consumer, (error, eventName) => {\r\n this.events.emit(\"error\", error, eventName);\r\n }),\r\n {\r\n consumerId: Consumer.consumerId,\r\n },\r\n );\r\n } else {\r\n this.consumers.push(Consumer);\r\n }\r\n\r\n return () => {\r\n this.unsubscribe(Consumer);\r\n };\r\n }\r\n\r\n /**\r\n * Unsubscribe the given consumer\r\n */\r\n public unsubscribe(Consumer: EventConsumerClass): void {\r\n if (this.isConnected) {\r\n this.channel(Consumer.eventName).unsubscribeById(Consumer.consumerId);\r\n }\r\n const index = this.consumers.indexOf(Consumer);\r\n if (index > -1) {\r\n this.consumers.splice(index, 1);\r\n }\r\n }\r\n\r\n /**\r\n * Publish the given event message.\r\n * Auto-creates the channel if it hasn't been accessed before.\r\n */\r\n public publish<TPayload = Record<string, any>>(event: EventMessage<TPayload>): void {\r\n this.channel(event.eventName).publish(event.serialize());\r\n }\r\n\r\n /**\r\n * Connect to RabbitMQ\r\n */\r\n public async connect(): Promise<void> {\r\n const amqplib = await loadAmqplibModule();\r\n\r\n if (!amqplib) {\r\n throw new Error(`amqplib is not installed.\\n\\n${AMQPLIB_INSTALL_INSTRUCTIONS}`);\r\n }\r\n\r\n try {\r\n // Build connection URL\r\n const url = this.buildConnectionUrl();\r\n\r\n // Build connection options merging our options with native client options\r\n const connectOptions = {\r\n heartbeat: this.options.heartbeat ?? 60,\r\n timeout: this.options.connectionTimeout,\r\n // Merge native amqplib client options\r\n ...this.options.clientOptions,\r\n };\r\n\r\n // Connect using cached module\r\n this.connection = await amqplib.connect(url, connectOptions);\r\n\r\n // Create channel\r\n this.amqpChannel = await this.connection.createChannel();\r\n\r\n // Set prefetch if specified\r\n if (this.options.prefetch) {\r\n await this.amqpChannel.prefetch(this.options.prefetch);\r\n }\r\n\r\n this._isConnected = true;\r\n this.events.emit(\"connected\");\r\n\r\n for (const consumer of this.consumers) {\r\n this.subscribe(consumer);\r\n }\r\n\r\n this.consumers.length = 0;\r\n\r\n // Handle connection close\r\n this.connection.on(\"close\", () => {\r\n this._isConnected = false;\r\n this.events.emit(\"disconnected\");\r\n\r\n if (this.options.reconnect !== false) {\r\n this.handleReconnect();\r\n }\r\n });\r\n\r\n // Handle errors\r\n this.connection.on(\"error\", (error: Error) => {\r\n this.events.emit(\"error\", error);\r\n });\r\n } catch (error) {\r\n this._isConnected = false;\r\n const message = error instanceof Error ? error.message : String(error);\r\n throw new Error(`Failed to connect to RabbitMQ: ${redactAmqpCredentials(message)}`);\r\n }\r\n }\r\n\r\n /**\r\n * Build connection URL from options\r\n */\r\n private buildConnectionUrl(): string {\r\n if (this.options.uri) {\r\n return this.options.uri;\r\n }\r\n\r\n const protocol = \"amqp\";\r\n const host = this.options.host ?? \"localhost\";\r\n const port = this.options.port ?? 5672;\r\n const vhost = this.options.vhost ?? \"/\";\r\n // URI-encoded so a credential containing a reserved URL character\r\n // (`@`, `:`, `/`, whitespace — common in generated secrets) can't produce\r\n // a malformed URL whose parser error echoes the raw credential back.\r\n const username = encodeURIComponent(this.options.username ?? \"guest\");\r\n const password = encodeURIComponent(this.options.password ?? \"guest\");\r\n\r\n const encodedVhost = encodeURIComponent(vhost);\r\n\r\n return `${protocol}://${username}:${password}@${host}:${port}/${encodedVhost}`;\r\n }\r\n\r\n /**\r\n * Handle reconnection\r\n */\r\n private async handleReconnect(): Promise<void> {\r\n const delay = this.options.reconnectDelay ?? 5000;\r\n let attempt = 0;\r\n\r\n const tryReconnect = async () => {\r\n attempt++;\r\n this.events.emit(\"reconnecting\", attempt);\r\n\r\n try {\r\n await this.connect();\r\n } catch {\r\n setTimeout(tryReconnect, delay);\r\n }\r\n };\r\n\r\n setTimeout(tryReconnect, delay);\r\n }\r\n\r\n /**\r\n * Disconnect from RabbitMQ\r\n */\r\n public async disconnect(): Promise<void> {\r\n if (this.amqpChannel) {\r\n try {\r\n await this.amqpChannel.close();\r\n } catch {\r\n // Ignore close errors\r\n }\r\n this.amqpChannel = null;\r\n }\r\n\r\n if (this.connection) {\r\n try {\r\n await this.connection.close();\r\n } catch {\r\n // Ignore close errors\r\n }\r\n this.connection = null;\r\n }\r\n\r\n this._isConnected = false;\r\n this.events.emit(\"disconnected\");\r\n }\r\n\r\n /**\r\n * Register event listener\r\n */\r\n public on(event: BrokerEvent, listener: BrokerEventListener): void {\r\n this.events.on(event, listener as any);\r\n }\r\n\r\n /**\r\n * Remove event listener\r\n */\r\n public off(event: BrokerEvent, listener: BrokerEventListener): void {\r\n this.events.off(event, listener as any);\r\n }\r\n\r\n /**\r\n * Get or create a channel\r\n */\r\n public channel<TPayload = unknown>(\r\n name: string,\r\n options?: ChannelOptions<TPayload>,\r\n ): ChannelContract<TPayload> {\r\n // Check cache\r\n const existing = this.channels.get(name);\r\n if (existing) {\r\n return existing as ChannelContract<TPayload>;\r\n }\r\n\r\n // Create new channel\r\n const channel = new RabbitMQChannel<TPayload>(name, this.amqpChannel, options);\r\n\r\n this.channels.set(name, channel);\r\n return channel;\r\n }\r\n\r\n /**\r\n * Start consuming messages\r\n */\r\n public async startConsuming(): Promise<void> {\r\n // Channels start consuming when subscribe() is called\r\n // This method is for batch start if needed\r\n }\r\n\r\n /**\r\n * Stop consuming messages from all subscribed channels.\r\n * Gracefully cancels all active consumers.\r\n */\r\n public async stopConsuming(): Promise<void> {\r\n const stops = Array.from(this.channels.values()).map(channel =>\r\n (channel as RabbitMQChannel<any>).stopConsuming(),\r\n );\r\n await Promise.all(stops);\r\n }\r\n\r\n /**\r\n * Health check\r\n */\r\n public async healthCheck(): Promise<HealthCheckResult> {\r\n if (!this._isConnected || !this.connection) {\r\n return {\r\n healthy: false,\r\n error: \"Not connected to RabbitMQ\",\r\n };\r\n }\r\n\r\n const start = Date.now();\r\n\r\n try {\r\n // Simple check - verify channel is open\r\n await this.amqpChannel.checkQueue(\"amq.rabbitmq.reply-to\").catch(() => {\r\n // Queue might not exist, but if we get here, connection is alive\r\n });\r\n\r\n return {\r\n healthy: true,\r\n latency: Date.now() - start,\r\n };\r\n } catch (error) {\r\n return {\r\n healthy: false,\r\n error: error instanceof Error ? error.message : String(error),\r\n latency: Date.now() - start,\r\n };\r\n }\r\n }\r\n\r\n /**\r\n * Get all channel names\r\n */\r\n public getChannelNames(): string[] {\r\n return Array.from(this.channels.keys());\r\n }\r\n\r\n /**\r\n * Close a specific channel\r\n */\r\n public async closeChannel(name: string): Promise<void> {\r\n const channel = this.channels.get(name);\r\n if (channel) {\r\n await channel.delete();\r\n this.channels.delete(name);\r\n }\r\n }\r\n\r\n /**\r\n * Get the raw AMQP channel (for advanced use)\r\n */\r\n public getRawChannel(): any {\r\n return this.amqpChannel;\r\n }\r\n\r\n /**\r\n * Get the raw connection (for advanced use)\r\n */\r\n public getRawConnection(): any {\r\n return this.connection;\r\n }\r\n}\r\n"],"mappings":"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"}
@@ -30,7 +30,7 @@ function prepareConsumerSubscription(Consumer, onError) {
30
30
  });
31
31
  ctx.ack();
32
32
  } catch (error) {
33
- ctx.nack(true);
33
+ await ctx.retry();
34
34
  if (onError) onError(error, Consumer.eventName);
35
35
  }
36
36
  };
@@ -1 +1 @@
1
- {"version":3,"file":"prepare-consumer-subscription.mjs","names":[],"sources":["../../../../../../../herald/src/message-managers/prepare-consumer-subscription.ts"],"sourcesContent":["import type { MessageHandler } from \"./../types\";\nimport { EventConsumerClass } from \"./types\";\n\nexport function prepareConsumerSubscription(\n Consumer: EventConsumerClass,\n onError?: (error: unknown, consumerName: string) => void,\n) {\n const callback: MessageHandler<any> = async (message, ctx) => {\n const envelope = message.payload;\n let eventPayload = envelope.payload;\n\n if (envelope.version) {\n if (!Consumer.isAcceptedVersion(Number(envelope.version))) {\n ctx.ack(); // Acknowledge but don't process\n return;\n }\n }\n\n const consumer = new Consumer();\n\n if (consumer.schema) {\n const result = await consumer.validate(eventPayload);\n if (!result || result.isValid === false) {\n ctx.nack();\n return;\n }\n\n eventPayload = result.data;\n }\n try {\n await consumer.handle(eventPayload, {\n payload: eventPayload,\n eventName: Consumer.eventName,\n messageId: message.metadata.messageId!,\n occurredAt: envelope.occurredAt ? new Date(envelope.occurredAt) : undefined,\n metadata: envelope.metadata,\n version: envelope.version,\n message,\n });\n ctx.ack(); // Auto-ack on success?\n } catch (error) {\n ctx.nack(true); // Requeue on failure\n if (onError) {\n onError(error, Consumer.eventName);\n }\n }\n };\n\n return callback;\n}\n"],"mappings":";AAGA,SAAgB,4BACd,UACA,SACA;CACA,MAAM,WAAgC,OAAO,SAAS,QAAQ;EAC5D,MAAM,WAAW,QAAQ;EACzB,IAAI,eAAe,SAAS;EAE5B,IAAI,SAAS,SACX;OAAI,CAAC,SAAS,kBAAkB,OAAO,SAAS,OAAO,CAAC,GAAG;IACzD,IAAI,IAAI;IACR;GACF;;EAGF,MAAM,WAAW,IAAI,SAAS;EAE9B,IAAI,SAAS,QAAQ;GACnB,MAAM,SAAS,MAAM,SAAS,SAAS,YAAY;GACnD,IAAI,CAAC,UAAU,OAAO,YAAY,OAAO;IACvC,IAAI,KAAK;IACT;GACF;GAEA,eAAe,OAAO;EACxB;EACA,IAAI;GACF,MAAM,SAAS,OAAO,cAAc;IAClC,SAAS;IACT,WAAW,SAAS;IACpB,WAAW,QAAQ,SAAS;IAC5B,YAAY,SAAS,aAAa,IAAI,KAAK,SAAS,UAAU,IAAI;IAClE,UAAU,SAAS;IACnB,SAAS,SAAS;IAClB;GACF,CAAC;GACD,IAAI,IAAI;EACV,SAAS,OAAO;GACd,IAAI,KAAK,IAAI;GACb,IAAI,SACF,QAAQ,OAAO,SAAS,SAAS;EAErC;CACF;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"prepare-consumer-subscription.mjs","names":[],"sources":["../../../../../../../herald/src/message-managers/prepare-consumer-subscription.ts"],"sourcesContent":["import type { MessageHandler } from \"./../types\";\r\nimport { EventConsumerClass } from \"./types\";\r\n\r\nexport function prepareConsumerSubscription(\r\n Consumer: EventConsumerClass,\r\n onError?: (error: unknown, consumerName: string) => void,\r\n) {\r\n const callback: MessageHandler<any> = async (message, ctx) => {\r\n const envelope = message.payload;\r\n let eventPayload = envelope.payload;\r\n\r\n if (envelope.version) {\r\n if (!Consumer.isAcceptedVersion(Number(envelope.version))) {\r\n ctx.ack(); // Acknowledge but don't process\r\n return;\r\n }\r\n }\r\n\r\n const consumer = new Consumer();\r\n\r\n if (consumer.schema) {\r\n const result = await consumer.validate(eventPayload);\r\n if (!result || result.isValid === false) {\r\n ctx.nack();\r\n return;\r\n }\r\n\r\n eventPayload = result.data;\r\n }\r\n try {\r\n await consumer.handle(eventPayload, {\r\n payload: eventPayload,\r\n eventName: Consumer.eventName,\r\n messageId: message.metadata.messageId!,\r\n occurredAt: envelope.occurredAt ? new Date(envelope.occurredAt) : undefined,\r\n metadata: envelope.metadata,\r\n version: envelope.version,\r\n message,\r\n });\r\n ctx.ack(); // Auto-ack on success?\r\n } catch (error) {\r\n // Bounded retry instead of an unconditional requeue: a message that\r\n // reliably throws (bad payload, a handler bug, a downstream outage)\r\n // would otherwise be nack+requeued forever, pinning the consumer in a\r\n // hot loop. `ctx.retry()` caps redelivery and dead-letters/drops (with\r\n // a loud log) once the cap is hit.\r\n await ctx.retry();\r\n if (onError) {\r\n onError(error, Consumer.eventName);\r\n }\r\n }\r\n };\r\n\r\n return callback;\r\n}\r\n"],"mappings":";AAGA,SAAgB,4BACd,UACA,SACA;CACA,MAAM,WAAgC,OAAO,SAAS,QAAQ;EAC5D,MAAM,WAAW,QAAQ;EACzB,IAAI,eAAe,SAAS;EAE5B,IAAI,SAAS,SACX;OAAI,CAAC,SAAS,kBAAkB,OAAO,SAAS,OAAO,CAAC,GAAG;IACzD,IAAI,IAAI;IACR;GACF;;EAGF,MAAM,WAAW,IAAI,SAAS;EAE9B,IAAI,SAAS,QAAQ;GACnB,MAAM,SAAS,MAAM,SAAS,SAAS,YAAY;GACnD,IAAI,CAAC,UAAU,OAAO,YAAY,OAAO;IACvC,IAAI,KAAK;IACT;GACF;GAEA,eAAe,OAAO;EACxB;EACA,IAAI;GACF,MAAM,SAAS,OAAO,cAAc;IAClC,SAAS;IACT,WAAW,SAAS;IACpB,WAAW,QAAQ,SAAS;IAC5B,YAAY,SAAS,aAAa,IAAI,KAAK,SAAS,UAAU,IAAI;IAClE,UAAU,SAAS;IACnB,SAAS,SAAS;IAClB;GACF,CAAC;GACD,IAAI,IAAI;EACV,SAAS,OAAO;GAMd,MAAM,IAAI,MAAM;GAChB,IAAI,SACF,QAAQ,OAAO,SAAS,SAAS;EAErC;CACF;CAEA,OAAO;AACT"}
package/llms-full.txt CHANGED
@@ -91,7 +91,7 @@ retry: {
91
91
  }
92
92
  ```
93
93
 
94
- `maxRetries` is the part that does the work: when a handler **throws**, herald reads the message's `x-retry-count` header and, while it's under `maxRetries`, nacks with requeue so the broker redelivers. Once `x-retry-count` reaches `maxRetries`, it nacks-without-requeue ( DLQ if configured) or rejects outright.
94
+ `maxRetries` is the part that does the work: when a handler **throws**, herald reads the message's `x-retry-count` header and, while it's under `maxRetries`, republishes with the header incremented so the count actually advances (a plain requeue never advances it — the broker doesn't add that header). Once `x-retry-count` reaches `maxRetries`, it dead-letters (if `deadLetter` is configured) or drops the message with a loud `log.error` — a poison message can no longer ping-pong forever. This is shared by the automatic throw path and the explicit `ctx.retry()` call, so both honor the same cap.
95
95
 
96
96
  **Caveat on `delay`.** `RetryOptions.delay` (number or `(attempt) => number`) is **not applied on the automatic throw path** — a thrown handler requeues immediately, with no wait. The only place a delay takes effect is the explicit `ctx.retry(delayMs)` call, which republishes the message with an `x-delay` header — and even that needs the RabbitMQ delayed-message-exchange plugin installed, or the delay is ignored. So if you need real backoff, call `ctx.retry(ms)` from inside the handler and install the plugin; don't rely on the channel-level `retry.delay` for timing.
97
97
 
@@ -130,12 +130,14 @@ export class UserCreatedConsumer extends EventConsumer<{ id: number; email: stri
130
130
  // handle(payload, event) — NOT (message, ctx). No ctx.ack() here.
131
131
  public async handle(payload: { id: number; email: string }, event: ConsumedEventMessage) {
132
132
  await sendWelcomeEmail(payload.email);
133
- // return cleanly → herald acks. throw → herald nacks (requeue).
133
+ // return cleanly → herald acks. throw → herald retries (bounded — see below).
134
134
  }
135
135
  }
136
136
  ```
137
137
 
138
- The handler receives the **already-unwrapped payload** as the first argument and a `ConsumedEventMessage` as the second — `{ messageId, eventName, payload, version?, occurredAt?, metadata?, message }` (where `message` is the raw `Message` if you need `message.metadata.headers`). There is **no `ctx`**: the framework auto-acks when `handle` resolves and auto-nacks-with-requeue when it throws, so you never call `ack`/`nack` yourself in this style.
138
+ The handler receives the **already-unwrapped payload** as the first argument and a `ConsumedEventMessage` as the second — `{ messageId, eventName, payload, version?, occurredAt?, metadata?, message }` (where `message` is the raw `Message` if you need `message.metadata.headers`). There is **no `ctx`**: the framework auto-acks when `handle` resolves, so you never call `ack`/`nack` yourself in this style.
139
+
140
+ **A throw is a bounded retry, not an infinite requeue.** `handle` throwing routes through the same bounded-retry routine the raw `.subscribe()` throw path uses (below) — republishes with an incremented `x-retry-count`, capped at `maxRetries: 3` (there's no per-consumer `retry`/`deadLetter` config yet, so the cap is always the default). Once the cap is hit, the message is dropped with a loud `log.error` — never a silent, unbounded ack/nack loop.
139
141
 
140
142
  Wiring: the channel name comes from `static eventName`, and `@Consumable` self-registers the moment the class module is **imported** — if a broker is already connected it subscribes immediately, otherwise it buffers and subscribes once `connectToBroker` fires. So the only wiring you need is to import the consumer file on the boot path (e.g. your module's `main.ts`).
141
143
 
@@ -253,6 +255,7 @@ yarn add @warlock.js/herald amqplib # amqplib for RabbitMQ
253
255
  5. **`@warlock.js/seal` schemas validate on publish + receive.** Pass `{ schema }` to `.channel(name, { schema })`.
254
256
  6. **Subscribers control message flow** via `ctx.ack()` / `ctx.nack()` / `ctx.reject()` / `ctx.retry(ms)`.
255
257
  7. **Smart auto-ack is the default** (`autoAck` unset/`false`). The consumer runs with manual-ack enabled, but herald acks for you when the handler returns cleanly and nacks-with-requeue when it throws — so a crash mid-handling re-delivers, and a clean handler that forgot `ctx.ack()` is still acked. Call `ctx` methods explicitly only when you need a non-default outcome (reject, DLQ, delayed retry). `autoAck: true` is the dangerous mode: the broker acks on delivery, so a crash loses the message.
258
+ 8. **`username`/`password` never leak into thrown or logged errors.** They're URI-encoded when building the internal `amqp://` connection URL, and any connect failure — including one built from a caller-supplied `uri` — has `user:pass@` redacted before it's re-thrown/logged. Safe to log a connection error as-is; it will never contain a plaintext broker credential.
256
259
 
257
260
  ## Minimal example
258
261
 
package/package.json CHANGED
@@ -3,10 +3,10 @@
3
3
  "description": "Message bus/brokers for RabbitMQ, Kafka, and more",
4
4
  "dependencies": {
5
5
  "@mongez/copper": "^2.1.2",
6
- "@mongez/events": "^2.2.6",
7
- "@mongez/reinforcements": "^3.3.0",
8
- "@warlock.js/logger": "4.15.0",
9
- "@warlock.js/seal": "4.15.0"
6
+ "@mongez/events": "^2.2.7",
7
+ "@mongez/reinforcements": "^4.0.1",
8
+ "@warlock.js/logger": "5.0.0",
9
+ "@warlock.js/seal": "5.0.0"
10
10
  },
11
11
  "repository": {
12
12
  "type": "git",
@@ -33,7 +33,7 @@
33
33
  ],
34
34
  "author": "hassanzohdy",
35
35
  "license": "MIT",
36
- "version": "4.15.0",
36
+ "version": "5.0.0",
37
37
  "main": "./cjs/index.cjs",
38
38
  "module": "./esm/index.mjs",
39
39
  "types": "./esm/index.d.mts",
@@ -83,7 +83,7 @@ retry: {
83
83
  }
84
84
  ```
85
85
 
86
- `maxRetries` is the part that does the work: when a handler **throws**, herald reads the message's `x-retry-count` header and, while it's under `maxRetries`, nacks with requeue so the broker redelivers. Once `x-retry-count` reaches `maxRetries`, it nacks-without-requeue ( DLQ if configured) or rejects outright.
86
+ `maxRetries` is the part that does the work: when a handler **throws**, herald reads the message's `x-retry-count` header and, while it's under `maxRetries`, republishes with the header incremented so the count actually advances (a plain requeue never advances it — the broker doesn't add that header). Once `x-retry-count` reaches `maxRetries`, it dead-letters (if `deadLetter` is configured) or drops the message with a loud `log.error` — a poison message can no longer ping-pong forever. This is shared by the automatic throw path and the explicit `ctx.retry()` call, so both honor the same cap.
87
87
 
88
88
  **Caveat on `delay`.** `RetryOptions.delay` (number or `(attempt) => number`) is **not applied on the automatic throw path** — a thrown handler requeues immediately, with no wait. The only place a delay takes effect is the explicit `ctx.retry(delayMs)` call, which republishes the message with an `x-delay` header — and even that needs the RabbitMQ delayed-message-exchange plugin installed, or the delay is ignored. So if you need real backoff, call `ctx.retry(ms)` from inside the handler and install the plugin; don't rely on the channel-level `retry.delay` for timing.
89
89
 
@@ -122,12 +122,14 @@ export class UserCreatedConsumer extends EventConsumer<{ id: number; email: stri
122
122
  // handle(payload, event) — NOT (message, ctx). No ctx.ack() here.
123
123
  public async handle(payload: { id: number; email: string }, event: ConsumedEventMessage) {
124
124
  await sendWelcomeEmail(payload.email);
125
- // return cleanly → herald acks. throw → herald nacks (requeue).
125
+ // return cleanly → herald acks. throw → herald retries (bounded — see below).
126
126
  }
127
127
  }
128
128
  ```
129
129
 
130
- The handler receives the **already-unwrapped payload** as the first argument and a `ConsumedEventMessage` as the second — `{ messageId, eventName, payload, version?, occurredAt?, metadata?, message }` (where `message` is the raw `Message` if you need `message.metadata.headers`). There is **no `ctx`**: the framework auto-acks when `handle` resolves and auto-nacks-with-requeue when it throws, so you never call `ack`/`nack` yourself in this style.
130
+ The handler receives the **already-unwrapped payload** as the first argument and a `ConsumedEventMessage` as the second — `{ messageId, eventName, payload, version?, occurredAt?, metadata?, message }` (where `message` is the raw `Message` if you need `message.metadata.headers`). There is **no `ctx`**: the framework auto-acks when `handle` resolves, so you never call `ack`/`nack` yourself in this style.
131
+
132
+ **A throw is a bounded retry, not an infinite requeue.** `handle` throwing routes through the same bounded-retry routine the raw `.subscribe()` throw path uses (below) — republishes with an incremented `x-retry-count`, capped at `maxRetries: 3` (there's no per-consumer `retry`/`deadLetter` config yet, so the cap is always the default). Once the cap is hit, the message is dropped with a loud `log.error` — never a silent, unbounded ack/nack loop.
131
133
 
132
134
  Wiring: the channel name comes from `static eventName`, and `@Consumable` self-registers the moment the class module is **imported** — if a broker is already connected it subscribes immediately, otherwise it buffers and subscribes once `connectToBroker` fires. So the only wiring you need is to import the consumer file on the boot path (e.g. your module's `main.ts`).
133
135
 
@@ -24,6 +24,7 @@ yarn add @warlock.js/herald amqplib # amqplib for RabbitMQ
24
24
  5. **`@warlock.js/seal` schemas validate on publish + receive.** Pass `{ schema }` to `.channel(name, { schema })`.
25
25
  6. **Subscribers control message flow** via `ctx.ack()` / `ctx.nack()` / `ctx.reject()` / `ctx.retry(ms)`.
26
26
  7. **Smart auto-ack is the default** (`autoAck` unset/`false`). The consumer runs with manual-ack enabled, but herald acks for you when the handler returns cleanly and nacks-with-requeue when it throws — so a crash mid-handling re-delivers, and a clean handler that forgot `ctx.ack()` is still acked. Call `ctx` methods explicitly only when you need a non-default outcome (reject, DLQ, delayed retry). `autoAck: true` is the dangerous mode: the broker acks on delivery, so a crash loses the message.
27
+ 8. **`username`/`password` never leak into thrown or logged errors.** They're URI-encoded when building the internal `amqp://` connection URL, and any connect failure — including one built from a caller-supplied `uri` — has `user:pass@` redacted before it's re-thrown/logged. Safe to log a connection error as-is; it will never contain a plaintext broker credential.
27
28
 
28
29
  ## Minimal example
29
30