@amqp-contract/contract 0.14.0 → 0.16.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/README.md CHANGED
@@ -58,10 +58,8 @@ const orderCreatedEvent = defineEventPublisher(ordersExchange, orderMessage, {
58
58
  const orderQueue = defineQueue("order-processing", { durable: true });
59
59
  const analyticsQueue = defineQueue("analytics", { durable: true });
60
60
 
61
- // Compose contract - configs go directly, bindings auto-generated
61
+ // Compose contract - exchanges, queues, bindings auto-extracted
62
62
  const contract = defineContract({
63
- exchanges: { orders: ordersExchange },
64
- queues: { orderQueue, analyticsQueue },
65
63
  publishers: {
66
64
  // EventPublisherConfig → auto-extracted to publisher
67
65
  orderCreated: orderCreatedEvent,
package/dist/index.cjs CHANGED
@@ -1,3 +1,4 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
1
2
 
2
3
  //#region src/builder/exchange.ts
3
4
  /**
@@ -269,76 +270,17 @@ function wrapWithTtlBackoffInfrastructure(queue) {
269
270
  },
270
271
  retry: resolveTtlBackoffOptions(void 0)
271
272
  };
273
+ const waitQueueBinding = defineQueueBindingInternal(waitQueue, dlx, { routingKey: waitQueueName });
274
+ const mainQueueRetryBinding = defineQueueBindingInternal(queue, dlx, { routingKey: queue.name });
272
275
  return {
273
276
  __brand: "QueueWithTtlBackoffInfrastructure",
274
277
  queue,
278
+ deadLetter: queue.deadLetter,
275
279
  waitQueue,
276
- waitQueueBinding: defineQueueBindingInternal(waitQueue, dlx, { routingKey: waitQueueName }),
277
- mainQueueRetryBinding: defineQueueBindingInternal(queue, dlx, { routingKey: queue.name })
280
+ waitQueueBinding,
281
+ mainQueueRetryBinding
278
282
  };
279
283
  }
280
- /**
281
- * Define an AMQP queue.
282
- *
283
- * A queue stores messages until they are consumed by workers. Queues can be bound to exchanges
284
- * to receive messages based on routing rules.
285
- *
286
- * By default, queues are created as quorum queues which provide better durability and
287
- * high-availability. Use `type: 'classic'` for special cases like non-durable queues
288
- * or priority queues.
289
- *
290
- * @param name - The name of the queue
291
- * @param options - Optional queue configuration
292
- * @param options.type - Queue type: 'quorum' (default, recommended) or 'classic'
293
- * @param options.durable - If true, the queue survives broker restarts. Quorum queues are always durable.
294
- * @param options.exclusive - If true, the queue can only be used by the declaring connection. Only supported with classic queues.
295
- * @param options.autoDelete - If true, the queue is deleted when the last consumer unsubscribes (default: false)
296
- * @param options.deadLetter - Dead letter configuration for handling failed messages
297
- * @param options.maxPriority - Maximum priority level for priority queue (1-255, recommended: 1-10). Only supported with classic queues.
298
- * @param options.arguments - Additional AMQP arguments (e.g., x-message-ttl)
299
- * @returns A queue definition
300
- *
301
- * @example
302
- * ```typescript
303
- * // Quorum queue (default, recommended for production)
304
- * const orderQueue = defineQueue('order-processing');
305
- *
306
- * // Explicit quorum queue with dead letter exchange
307
- * const dlx = defineExchange('orders-dlx', 'topic', { durable: true });
308
- * const orderQueueWithDLX = defineQueue('order-processing', {
309
- * type: 'quorum',
310
- * deadLetter: {
311
- * exchange: dlx,
312
- * routingKey: 'order.failed'
313
- * },
314
- * arguments: {
315
- * 'x-message-ttl': 86400000, // 24 hours
316
- * }
317
- * });
318
- *
319
- * // Classic queue (for special cases)
320
- * const tempQueue = defineQueue('temp-queue', {
321
- * type: 'classic',
322
- * durable: false,
323
- * autoDelete: true,
324
- * });
325
- *
326
- * // Priority queue (requires classic type)
327
- * const taskQueue = defineQueue('urgent-tasks', {
328
- * type: 'classic',
329
- * durable: true,
330
- * maxPriority: 10,
331
- * });
332
- *
333
- * // Queue with TTL-backoff retry (returns infrastructure automatically)
334
- * const dlx = defineExchange('orders-dlx', 'direct', { durable: true });
335
- * const orderQueue = defineQueue('order-processing', {
336
- * deadLetter: { exchange: dlx },
337
- * retry: { mode: 'ttl-backoff', maxRetries: 5 },
338
- * });
339
- * // orderQueue is QueueWithTtlBackoffInfrastructure, pass directly to defineContract
340
- * ```
341
- */
342
284
  function defineQueue(name, options) {
343
285
  const opts = options ?? {};
344
286
  const type = opts.type ?? "quorum";
@@ -353,18 +295,18 @@ function defineQueue(name, options) {
353
295
  if (inputRetry.mode === "quorum-native") {
354
296
  if (quorumOpts.deliveryLimit === void 0) throw new Error(`Queue "${name}" uses quorum-native retry mode but deliveryLimit is not configured. Quorum-native retry requires deliveryLimit to be set.`);
355
297
  }
356
- const retry$1 = inputRetry.mode === "quorum-native" ? inputRetry : resolveTtlBackoffOptions(inputRetry);
357
- const queueDefinition$1 = {
298
+ const retry = inputRetry.mode === "quorum-native" ? inputRetry : resolveTtlBackoffOptions(inputRetry);
299
+ const queueDefinition = {
358
300
  ...baseProps,
359
301
  type: "quorum",
360
- retry: retry$1
302
+ retry
361
303
  };
362
304
  if (quorumOpts.deliveryLimit !== void 0) {
363
305
  if (quorumOpts.deliveryLimit < 1 || !Number.isInteger(quorumOpts.deliveryLimit)) throw new Error(`Invalid deliveryLimit: ${quorumOpts.deliveryLimit}. Must be a positive integer.`);
364
- queueDefinition$1.deliveryLimit = quorumOpts.deliveryLimit;
306
+ queueDefinition.deliveryLimit = quorumOpts.deliveryLimit;
365
307
  }
366
- if (retry$1.mode === "ttl-backoff" && queueDefinition$1.deadLetter) return wrapWithTtlBackoffInfrastructure(queueDefinition$1);
367
- return queueDefinition$1;
308
+ if (retry.mode === "ttl-backoff" && queueDefinition.deadLetter) return wrapWithTtlBackoffInfrastructure(queueDefinition);
309
+ return queueDefinition;
368
310
  }
369
311
  const classicOpts = opts;
370
312
  if (classicOpts.retry?.mode === "quorum-native") throw new Error(`Queue "${name}" uses quorum-native retry mode but is a classic queue. Quorum-native retry requires quorum queues (type: "quorum").`);
@@ -411,10 +353,10 @@ function defineQueue(name, options) {
411
353
  * deliveryLimit: 3, // Retry up to 3 times
412
354
  * });
413
355
  *
356
+ * // Use in a contract — exchanges, queues, and bindings are auto-extracted
414
357
  * const contract = defineContract({
415
- * exchanges: { dlx },
416
- * queues: { orderProcessing: orderQueue },
417
- * // ...
358
+ * publishers: { ... },
359
+ * consumers: { processOrder: defineEventConsumer(event, orderQueue) },
418
360
  * });
419
361
  * ```
420
362
  *
@@ -469,10 +411,10 @@ function defineQuorumQueue(name, options) {
469
411
  * maxDelayMs: 30000, // Cap at 30s
470
412
  * });
471
413
  *
414
+ * // Use in a contract — wait queue, bindings, and DLX are auto-extracted
472
415
  * const contract = defineContract({
473
- * exchanges: { dlx },
474
- * queues: { orderProcessing: orderQueue }, // Wait queue auto-added
475
- * // ... bindings auto-generated
416
+ * publishers: { ... },
417
+ * consumers: { processOrder: defineEventConsumer(event, extractQueue(orderQueue)) },
476
418
  * });
477
419
  *
478
420
  * // To access the underlying queue definition (e.g., for the queue name):
@@ -682,10 +624,14 @@ function defineEventConsumer(eventPublisher, queue, options) {
682
624
  const bindingArguments = options?.arguments ?? eventPublisher.arguments;
683
625
  if (bindingArguments !== void 0) bindingOptions.arguments = bindingArguments;
684
626
  const binding = defineQueueBindingInternal(queue, exchange, bindingOptions);
627
+ const consumer = defineConsumer(queue, message);
685
628
  return {
686
629
  __brand: "EventConsumerResult",
687
- consumer: defineConsumer(queue, message),
688
- binding
630
+ consumer,
631
+ binding,
632
+ exchange,
633
+ queue: consumer.queue,
634
+ deadLetterExchange: consumer.queue.deadLetter?.exchange
689
635
  };
690
636
  }
691
637
  /**
@@ -714,11 +660,14 @@ function isEventConsumerResult(value) {
714
660
  * @internal
715
661
  */
716
662
  function defineCommandConsumer(queue, exchange, message, options) {
663
+ const consumer = defineConsumer(queue, message);
717
664
  return {
718
665
  __brand: "CommandConsumerConfig",
719
- consumer: defineConsumer(queue, message),
666
+ consumer,
720
667
  binding: defineQueueBindingInternal(queue, exchange, options),
721
668
  exchange,
669
+ queue: consumer.queue,
670
+ deadLetterExchange: consumer.queue.deadLetter?.exchange,
722
671
  message,
723
672
  routingKey: options?.routingKey
724
673
  };
@@ -750,19 +699,17 @@ function isCommandConsumerConfig(value) {
750
699
  * Define an AMQP contract.
751
700
  *
752
701
  * A contract is the central definition of your AMQP messaging topology. It brings together
753
- * all exchanges, queues, bindings, publishers, and consumers in a single, type-safe definition.
702
+ * publishers and consumers in a single, type-safe definition. Exchanges, queues, and bindings
703
+ * are automatically extracted from publishers and consumers.
754
704
  *
755
705
  * The contract is used by both clients (for publishing) and workers (for consuming) to ensure
756
706
  * type safety throughout your messaging infrastructure. TypeScript will infer all message types
757
707
  * and publisher/consumer names from the contract.
758
708
  *
759
- * @param definition - The contract definition containing all AMQP resources
760
- * @param definition.exchanges - Named exchange definitions
761
- * @param definition.queues - Named queue definitions
762
- * @param definition.bindings - Named binding definitions (queue-to-exchange or exchange-to-exchange)
709
+ * @param definition - The contract definition containing publishers and consumers
763
710
  * @param definition.publishers - Named publisher definitions for sending messages
764
711
  * @param definition.consumers - Named consumer definitions for receiving messages
765
- * @returns The same contract definition with full type inference
712
+ * @returns The contract definition with fully inferred exchanges, queues, bindings, publishers, and consumers
766
713
  *
767
714
  * @example
768
715
  * ```typescript
@@ -770,16 +717,20 @@ function isCommandConsumerConfig(value) {
770
717
  * defineContract,
771
718
  * defineExchange,
772
719
  * defineQueue,
773
- * defineQueueBinding,
774
- * definePublisher,
775
- * defineConsumer,
720
+ * defineEventPublisher,
721
+ * defineEventConsumer,
776
722
  * defineMessage,
777
723
  * } from '@amqp-contract/contract';
778
724
  * import { z } from 'zod';
779
725
  *
780
726
  * // Define resources
781
727
  * const ordersExchange = defineExchange('orders', 'topic', { durable: true });
782
- * const orderQueue = defineQueue('order-processing', { durable: true });
728
+ * const dlx = defineExchange('orders-dlx', 'direct', { durable: true });
729
+ * const orderQueue = defineQueue('order-processing', {
730
+ * deadLetter: { exchange: dlx },
731
+ * retry: { mode: 'quorum-native' },
732
+ * deliveryLimit: 3,
733
+ * });
783
734
  * const orderMessage = defineMessage(
784
735
  * z.object({
785
736
  * orderId: z.string(),
@@ -787,76 +738,114 @@ function isCommandConsumerConfig(value) {
787
738
  * })
788
739
  * );
789
740
  *
790
- * // Compose contract
741
+ * // Define event publisher
742
+ * const orderCreatedEvent = defineEventPublisher(ordersExchange, orderMessage, {
743
+ * routingKey: 'order.created',
744
+ * });
745
+ *
746
+ * // Compose contract - exchanges, queues, bindings are auto-extracted
791
747
  * export const contract = defineContract({
792
- * exchanges: {
793
- * orders: ordersExchange,
794
- * },
795
- * queues: {
796
- * orderProcessing: orderQueue,
797
- * },
798
- * bindings: {
799
- * orderBinding: defineQueueBinding(orderQueue, ordersExchange, {
800
- * routingKey: 'order.created',
801
- * }),
802
- * },
803
748
  * publishers: {
804
- * orderCreated: definePublisher(ordersExchange, orderMessage, {
805
- * routingKey: 'order.created',
806
- * }),
749
+ * orderCreated: orderCreatedEvent,
807
750
  * },
808
751
  * consumers: {
809
- * processOrder: defineConsumer(orderQueue, orderMessage),
752
+ * processOrder: defineEventConsumer(orderCreatedEvent, orderQueue),
810
753
  * },
811
754
  * });
812
755
  *
813
756
  * // TypeScript now knows:
757
+ * // - contract.exchanges.orders, contract.exchanges['orders-dlx']
758
+ * // - contract.queues['order-processing']
759
+ * // - contract.bindings.processOrderBinding
814
760
  * // - client.publish('orderCreated', { orderId: string, amount: number })
815
- * // - handler: async (message: { orderId: string, amount: number }) => void
761
+ * // - handler: (message: { orderId: string, amount: number }) => Future<Result<void, HandlerError>>
816
762
  * ```
817
763
  */
818
764
  function defineContract(definition) {
819
- const { publishers: inputPublishers, consumers: inputConsumers, ...rest } = definition;
820
- const result = rest;
821
- if (definition.queues && Object.keys(definition.queues).length > 0) {
822
- const expandedQueues = {};
823
- const queueBindings = {};
824
- for (const [name, entry] of Object.entries(definition.queues)) if (isQueueWithTtlBackoffInfrastructure(entry)) {
825
- expandedQueues[name] = entry.queue;
826
- expandedQueues[`${name}Wait`] = entry.waitQueue;
827
- queueBindings[`${name}WaitBinding`] = entry.waitQueueBinding;
828
- queueBindings[`${name}RetryBinding`] = entry.mainQueueRetryBinding;
829
- } else expandedQueues[name] = entry;
830
- result.queues = expandedQueues;
831
- if (Object.keys(queueBindings).length > 0) result.bindings = {
832
- ...result.bindings,
833
- ...queueBindings
834
- };
835
- }
765
+ const { publishers: inputPublishers, consumers: inputConsumers } = definition;
766
+ const result = {
767
+ exchanges: {},
768
+ queues: {},
769
+ bindings: {},
770
+ publishers: {},
771
+ consumers: {}
772
+ };
836
773
  if (inputPublishers && Object.keys(inputPublishers).length > 0) {
837
774
  const processedPublishers = {};
775
+ const exchanges = {};
838
776
  for (const [name, entry] of Object.entries(inputPublishers)) if (isEventPublisherConfig(entry)) {
777
+ exchanges[entry.exchange.name] = entry.exchange;
839
778
  const publisherOptions = {};
840
779
  if (entry.routingKey !== void 0) publisherOptions.routingKey = entry.routingKey;
841
780
  processedPublishers[name] = definePublisherInternal(entry.exchange, entry.message, publisherOptions);
842
- } else processedPublishers[name] = entry;
781
+ } else {
782
+ const publisher = entry;
783
+ exchanges[publisher.exchange.name] = publisher.exchange;
784
+ processedPublishers[name] = publisher;
785
+ }
843
786
  result.publishers = processedPublishers;
787
+ result.exchanges = {
788
+ ...result.exchanges,
789
+ ...exchanges
790
+ };
844
791
  }
845
792
  if (inputConsumers && Object.keys(inputConsumers).length > 0) {
846
793
  const processedConsumers = {};
847
794
  const consumerBindings = {};
795
+ const queues = {};
796
+ const exchanges = {};
848
797
  for (const [name, entry] of Object.entries(inputConsumers)) if (isEventConsumerResult(entry)) {
849
798
  processedConsumers[name] = entry.consumer;
850
799
  consumerBindings[`${name}Binding`] = entry.binding;
800
+ const queueEntry = entry.consumer.queue;
801
+ queues[queueEntry.name] = queueEntry;
802
+ exchanges[entry.binding.exchange.name] = entry.binding.exchange;
803
+ if (queueEntry.deadLetter?.exchange) exchanges[queueEntry.deadLetter.exchange.name] = queueEntry.deadLetter.exchange;
851
804
  } else if (isCommandConsumerConfig(entry)) {
852
805
  processedConsumers[name] = entry.consumer;
853
806
  consumerBindings[`${name}Binding`] = entry.binding;
854
- } else processedConsumers[name] = entry;
807
+ const queueEntry = entry.consumer.queue;
808
+ queues[queueEntry.name] = queueEntry;
809
+ exchanges[entry.exchange.name] = entry.exchange;
810
+ if (queueEntry.deadLetter?.exchange) exchanges[queueEntry.deadLetter.exchange.name] = queueEntry.deadLetter.exchange;
811
+ } else {
812
+ const consumer = entry;
813
+ processedConsumers[name] = consumer;
814
+ const queueEntry = consumer.queue;
815
+ queues[queueEntry.name] = queueEntry;
816
+ if (queueEntry.deadLetter?.exchange) exchanges[queueEntry.deadLetter.exchange.name] = queueEntry.deadLetter.exchange;
817
+ }
818
+ for (const queue of Object.values(queues)) if (queue.retry?.mode === "ttl-backoff" && queue.deadLetter) {
819
+ const dlx = queue.deadLetter.exchange;
820
+ const waitQueueName = `${queue.name}-wait`;
821
+ const waitQueue = {
822
+ name: waitQueueName,
823
+ type: "quorum",
824
+ durable: queue.durable ?? true,
825
+ deadLetter: {
826
+ exchange: dlx,
827
+ routingKey: queue.name
828
+ },
829
+ retry: resolveTtlBackoffOptions(void 0)
830
+ };
831
+ queues[waitQueueName] = waitQueue;
832
+ consumerBindings[`${queue.name}WaitBinding`] = defineQueueBindingInternal(waitQueue, dlx, { routingKey: waitQueueName });
833
+ consumerBindings[`${queue.name}RetryBinding`] = defineQueueBindingInternal(queue, dlx, { routingKey: queue.name });
834
+ exchanges[dlx.name] = dlx;
835
+ }
855
836
  result.consumers = processedConsumers;
856
- if (Object.keys(consumerBindings).length > 0) result.bindings = {
837
+ result.bindings = {
857
838
  ...result.bindings,
858
839
  ...consumerBindings
859
840
  };
841
+ result.queues = {
842
+ ...result.queues,
843
+ ...queues
844
+ };
845
+ result.exchanges = {
846
+ ...result.exchanges,
847
+ ...exchanges
848
+ };
860
849
  }
861
850
  return result;
862
851
  }
@@ -894,23 +883,15 @@ function defineContract(definition) {
894
883
  * },
895
884
  * });
896
885
  *
897
- * // Generate TTL-backoff infrastructure
898
- * const retryInfra = defineTtlBackoffRetryInfrastructure(orderQueue);
899
- *
900
- * // Spread into contract
886
+ * // Infrastructure is auto-extracted when using defineContract:
901
887
  * const contract = defineContract({
902
- * exchanges: { dlx },
903
- * queues: {
904
- * orderProcessing: orderQueue,
905
- * orderProcessingWait: retryInfra.waitQueue,
906
- * },
907
- * bindings: {
908
- * ...// your other bindings
909
- * orderWaitBinding: retryInfra.waitQueueBinding,
910
- * orderRetryBinding: retryInfra.mainQueueRetryBinding,
911
- * },
912
- * // ... publishers and consumers
888
+ * publishers: { ... },
889
+ * consumers: { processOrder: defineEventConsumer(event, extractQueue(orderQueue)) },
913
890
  * });
891
+ * // contract.queues includes the wait queue, contract.bindings includes retry bindings
892
+ *
893
+ * // Or generate manually for advanced use cases:
894
+ * const retryInfra = defineTtlBackoffRetryInfrastructure(orderQueue);
914
895
  * ```
915
896
  */
916
897
  function defineTtlBackoffRetryInfrastructure(queueEntry, options) {