@warlock.js/herald 4.0.174 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/README.md +36 -33
  2. package/cjs/index.cjs +1355 -0
  3. package/cjs/index.cjs.map +1 -0
  4. package/esm/communicators/broker-registry.d.mts +157 -0
  5. package/esm/communicators/broker-registry.d.mts.map +1 -0
  6. package/esm/communicators/broker-registry.mjs +204 -0
  7. package/esm/communicators/broker-registry.mjs.map +1 -0
  8. package/esm/communicators/broker.d.mts +104 -0
  9. package/esm/communicators/broker.d.mts.map +1 -0
  10. package/esm/communicators/broker.mjs +104 -0
  11. package/esm/communicators/broker.mjs.map +1 -0
  12. package/esm/communicators/index.d.mts +2 -0
  13. package/esm/communicators/index.mjs +4 -0
  14. package/esm/contracts/broker-driver.contract.d.mts +200 -0
  15. package/esm/contracts/broker-driver.contract.d.mts.map +1 -0
  16. package/esm/contracts/channel.contract.d.mts +197 -0
  17. package/esm/contracts/channel.contract.d.mts.map +1 -0
  18. package/esm/contracts/index.d.mts +2 -0
  19. package/esm/decorators/consumable.d.mts +17 -0
  20. package/esm/decorators/consumable.d.mts.map +1 -0
  21. package/esm/decorators/consumable.mjs +36 -0
  22. package/esm/decorators/consumable.mjs.map +1 -0
  23. package/esm/decorators/index.d.mts +1 -0
  24. package/esm/decorators/index.mjs +3 -0
  25. package/esm/drivers/index.d.mts +2 -0
  26. package/esm/drivers/index.mjs +5 -0
  27. package/esm/drivers/rabbitmq/index.d.mts +2 -0
  28. package/esm/drivers/rabbitmq/index.mjs +4 -0
  29. package/esm/drivers/rabbitmq/rabbitmq-channel.d.mts +85 -0
  30. package/esm/drivers/rabbitmq/rabbitmq-channel.d.mts.map +1 -0
  31. package/esm/drivers/rabbitmq/rabbitmq-channel.mjs +352 -0
  32. package/esm/drivers/rabbitmq/rabbitmq-channel.mjs.map +1 -0
  33. package/esm/drivers/rabbitmq/rabbitmq-driver.d.mts +130 -0
  34. package/esm/drivers/rabbitmq/rabbitmq-driver.d.mts.map +1 -0
  35. package/esm/drivers/rabbitmq/rabbitmq-driver.mjs +291 -0
  36. package/esm/drivers/rabbitmq/rabbitmq-driver.mjs.map +1 -0
  37. package/esm/index.d.mts +20 -0
  38. package/esm/index.mjs +16 -0
  39. package/esm/message-managers/event-consumer.d.mts +61 -0
  40. package/esm/message-managers/event-consumer.d.mts.map +1 -0
  41. package/esm/message-managers/event-consumer.mjs +63 -0
  42. package/esm/message-managers/event-consumer.mjs.map +1 -0
  43. package/esm/message-managers/event-message.d.mts +87 -0
  44. package/esm/message-managers/event-message.d.mts.map +1 -0
  45. package/esm/message-managers/event-message.mjs +68 -0
  46. package/esm/message-managers/event-message.mjs.map +1 -0
  47. package/esm/message-managers/index.d.mts +3 -0
  48. package/esm/message-managers/index.mjs +4 -0
  49. package/esm/message-managers/prepare-consumer-subscription.mjs +42 -0
  50. package/esm/message-managers/prepare-consumer-subscription.mjs.map +1 -0
  51. package/esm/message-managers/types.d.mts +23 -0
  52. package/esm/message-managers/types.d.mts.map +1 -0
  53. package/esm/types/channel.types.d.mts +23 -0
  54. package/esm/types/channel.types.d.mts.map +1 -0
  55. package/esm/types/connection.types.d.mts +67 -0
  56. package/esm/types/connection.types.d.mts.map +1 -0
  57. package/esm/types/driver.types.d.mts +13 -0
  58. package/esm/types/driver.types.d.mts.map +1 -0
  59. package/esm/types/index.d.mts +7 -0
  60. package/esm/types/message.types.d.mts +37 -0
  61. package/esm/types/message.types.d.mts.map +1 -0
  62. package/esm/types/publish.types.d.mts +16 -0
  63. package/esm/types/publish.types.d.mts.map +1 -0
  64. package/esm/types/registry.types.d.mts +8 -0
  65. package/esm/types/registry.types.d.mts.map +1 -0
  66. package/esm/types/subscribe.types.d.mts +21 -0
  67. package/esm/types/subscribe.types.d.mts.map +1 -0
  68. package/esm/use-case-broadcast.d.mts +41 -0
  69. package/esm/use-case-broadcast.d.mts.map +1 -0
  70. package/esm/use-case-broadcast.mjs +30 -0
  71. package/esm/use-case-broadcast.mjs.map +1 -0
  72. package/esm/utils/{connect-to-broker.d.ts → connect-to-broker.d.mts} +15 -10
  73. package/esm/utils/connect-to-broker.d.mts.map +1 -0
  74. package/esm/utils/connect-to-broker.mjs +168 -0
  75. package/esm/utils/connect-to-broker.mjs.map +1 -0
  76. package/esm/utils/index.d.mts +1 -0
  77. package/esm/utils/index.mjs +3 -0
  78. package/llms-full.txt +581 -0
  79. package/llms.txt +12 -0
  80. package/package.json +46 -46
  81. package/skills/README.md +21 -0
  82. package/skills/consume-message/SKILL.md +217 -0
  83. package/skills/herald-basics/SKILL.md +96 -0
  84. package/skills/publish-message/SKILL.md +172 -0
  85. package/skills/request-and-respond/SKILL.md +74 -0
  86. package/cjs/communicators/broker-registry.d.ts +0 -155
  87. package/cjs/communicators/broker-registry.d.ts.map +0 -1
  88. package/cjs/communicators/broker-registry.js +0 -206
  89. package/cjs/communicators/broker-registry.js.map +0 -1
  90. package/cjs/communicators/broker.d.ts +0 -100
  91. package/cjs/communicators/broker.d.ts.map +0 -1
  92. package/cjs/communicators/broker.js +0 -105
  93. package/cjs/communicators/broker.js.map +0 -1
  94. package/cjs/communicators/index.d.ts +0 -3
  95. package/cjs/communicators/index.d.ts.map +0 -1
  96. package/cjs/contracts/broker-driver.contract.d.ts +0 -196
  97. package/cjs/contracts/broker-driver.contract.d.ts.map +0 -1
  98. package/cjs/contracts/channel.contract.d.ts +0 -191
  99. package/cjs/contracts/channel.contract.d.ts.map +0 -1
  100. package/cjs/contracts/index.d.ts +0 -3
  101. package/cjs/contracts/index.d.ts.map +0 -1
  102. package/cjs/decorators/consumable.d.ts +0 -13
  103. package/cjs/decorators/consumable.d.ts.map +0 -1
  104. package/cjs/decorators/consumable.js +0 -33
  105. package/cjs/decorators/consumable.js.map +0 -1
  106. package/cjs/decorators/index.d.ts +0 -2
  107. package/cjs/decorators/index.d.ts.map +0 -1
  108. package/cjs/drivers/index.d.ts +0 -2
  109. package/cjs/drivers/index.d.ts.map +0 -1
  110. package/cjs/drivers/rabbitmq/index.d.ts +0 -3
  111. package/cjs/drivers/rabbitmq/index.d.ts.map +0 -1
  112. package/cjs/drivers/rabbitmq/rabbitmq-channel.d.ts +0 -79
  113. package/cjs/drivers/rabbitmq/rabbitmq-channel.d.ts.map +0 -1
  114. package/cjs/drivers/rabbitmq/rabbitmq-channel.js +0 -414
  115. package/cjs/drivers/rabbitmq/rabbitmq-channel.js.map +0 -1
  116. package/cjs/drivers/rabbitmq/rabbitmq-driver.d.ts +0 -124
  117. package/cjs/drivers/rabbitmq/rabbitmq-driver.d.ts.map +0 -1
  118. package/cjs/drivers/rabbitmq/rabbitmq-driver.js +0 -347
  119. package/cjs/drivers/rabbitmq/rabbitmq-driver.js.map +0 -1
  120. package/cjs/index.d.ts +0 -47
  121. package/cjs/index.d.ts.map +0 -1
  122. package/cjs/index.js +0 -1
  123. package/cjs/index.js.map +0 -1
  124. package/cjs/message-managers/event-consumer.d.ts +0 -64
  125. package/cjs/message-managers/event-consumer.d.ts.map +0 -1
  126. package/cjs/message-managers/event-consumer.js +0 -71
  127. package/cjs/message-managers/event-consumer.js.map +0 -1
  128. package/cjs/message-managers/event-message.d.ts +0 -90
  129. package/cjs/message-managers/event-message.d.ts.map +0 -1
  130. package/cjs/message-managers/event-message.js +0 -84
  131. package/cjs/message-managers/event-message.js.map +0 -1
  132. package/cjs/message-managers/index.d.ts +0 -4
  133. package/cjs/message-managers/index.d.ts.map +0 -1
  134. package/cjs/message-managers/prepare-consumer-subscription.d.ts +0 -4
  135. package/cjs/message-managers/prepare-consumer-subscription.d.ts.map +0 -1
  136. package/cjs/message-managers/prepare-consumer-subscription.js +0 -40
  137. package/cjs/message-managers/prepare-consumer-subscription.js.map +0 -1
  138. package/cjs/message-managers/types.d.ts +0 -20
  139. package/cjs/message-managers/types.d.ts.map +0 -1
  140. package/cjs/types/channel.types.d.ts +0 -19
  141. package/cjs/types/channel.types.d.ts.map +0 -1
  142. package/cjs/types/connection.types.d.ts +0 -65
  143. package/cjs/types/connection.types.d.ts.map +0 -1
  144. package/cjs/types/driver.types.d.ts +0 -10
  145. package/cjs/types/driver.types.d.ts.map +0 -1
  146. package/cjs/types/index.d.ts +0 -8
  147. package/cjs/types/index.d.ts.map +0 -1
  148. package/cjs/types/message.types.d.ts +0 -34
  149. package/cjs/types/message.types.d.ts.map +0 -1
  150. package/cjs/types/publish.types.d.ts +0 -13
  151. package/cjs/types/publish.types.d.ts.map +0 -1
  152. package/cjs/types/registry.types.d.ts +0 -4
  153. package/cjs/types/registry.types.d.ts.map +0 -1
  154. package/cjs/types/subscribe.types.d.ts +0 -18
  155. package/cjs/types/subscribe.types.d.ts.map +0 -1
  156. package/cjs/utils/connect-to-broker.d.ts +0 -131
  157. package/cjs/utils/connect-to-broker.d.ts.map +0 -1
  158. package/cjs/utils/connect-to-broker.js +0 -171
  159. package/cjs/utils/connect-to-broker.js.map +0 -1
  160. package/cjs/utils/index.d.ts +0 -2
  161. package/cjs/utils/index.d.ts.map +0 -1
  162. package/esm/communicators/broker-registry.d.ts +0 -155
  163. package/esm/communicators/broker-registry.d.ts.map +0 -1
  164. package/esm/communicators/broker-registry.js +0 -206
  165. package/esm/communicators/broker-registry.js.map +0 -1
  166. package/esm/communicators/broker.d.ts +0 -100
  167. package/esm/communicators/broker.d.ts.map +0 -1
  168. package/esm/communicators/broker.js +0 -105
  169. package/esm/communicators/broker.js.map +0 -1
  170. package/esm/communicators/index.d.ts +0 -3
  171. package/esm/communicators/index.d.ts.map +0 -1
  172. package/esm/contracts/broker-driver.contract.d.ts +0 -196
  173. package/esm/contracts/broker-driver.contract.d.ts.map +0 -1
  174. package/esm/contracts/channel.contract.d.ts +0 -191
  175. package/esm/contracts/channel.contract.d.ts.map +0 -1
  176. package/esm/contracts/index.d.ts +0 -3
  177. package/esm/contracts/index.d.ts.map +0 -1
  178. package/esm/decorators/consumable.d.ts +0 -13
  179. package/esm/decorators/consumable.d.ts.map +0 -1
  180. package/esm/decorators/consumable.js +0 -33
  181. package/esm/decorators/consumable.js.map +0 -1
  182. package/esm/decorators/index.d.ts +0 -2
  183. package/esm/decorators/index.d.ts.map +0 -1
  184. package/esm/drivers/index.d.ts +0 -2
  185. package/esm/drivers/index.d.ts.map +0 -1
  186. package/esm/drivers/rabbitmq/index.d.ts +0 -3
  187. package/esm/drivers/rabbitmq/index.d.ts.map +0 -1
  188. package/esm/drivers/rabbitmq/rabbitmq-channel.d.ts +0 -79
  189. package/esm/drivers/rabbitmq/rabbitmq-channel.d.ts.map +0 -1
  190. package/esm/drivers/rabbitmq/rabbitmq-channel.js +0 -414
  191. package/esm/drivers/rabbitmq/rabbitmq-channel.js.map +0 -1
  192. package/esm/drivers/rabbitmq/rabbitmq-driver.d.ts +0 -124
  193. package/esm/drivers/rabbitmq/rabbitmq-driver.d.ts.map +0 -1
  194. package/esm/drivers/rabbitmq/rabbitmq-driver.js +0 -347
  195. package/esm/drivers/rabbitmq/rabbitmq-driver.js.map +0 -1
  196. package/esm/index.d.ts +0 -47
  197. package/esm/index.d.ts.map +0 -1
  198. package/esm/index.js +0 -1
  199. package/esm/index.js.map +0 -1
  200. package/esm/message-managers/event-consumer.d.ts +0 -64
  201. package/esm/message-managers/event-consumer.d.ts.map +0 -1
  202. package/esm/message-managers/event-consumer.js +0 -71
  203. package/esm/message-managers/event-consumer.js.map +0 -1
  204. package/esm/message-managers/event-message.d.ts +0 -90
  205. package/esm/message-managers/event-message.d.ts.map +0 -1
  206. package/esm/message-managers/event-message.js +0 -84
  207. package/esm/message-managers/event-message.js.map +0 -1
  208. package/esm/message-managers/index.d.ts +0 -4
  209. package/esm/message-managers/index.d.ts.map +0 -1
  210. package/esm/message-managers/prepare-consumer-subscription.d.ts +0 -4
  211. package/esm/message-managers/prepare-consumer-subscription.d.ts.map +0 -1
  212. package/esm/message-managers/prepare-consumer-subscription.js +0 -40
  213. package/esm/message-managers/prepare-consumer-subscription.js.map +0 -1
  214. package/esm/message-managers/types.d.ts +0 -20
  215. package/esm/message-managers/types.d.ts.map +0 -1
  216. package/esm/types/channel.types.d.ts +0 -19
  217. package/esm/types/channel.types.d.ts.map +0 -1
  218. package/esm/types/connection.types.d.ts +0 -65
  219. package/esm/types/connection.types.d.ts.map +0 -1
  220. package/esm/types/driver.types.d.ts +0 -10
  221. package/esm/types/driver.types.d.ts.map +0 -1
  222. package/esm/types/index.d.ts +0 -8
  223. package/esm/types/index.d.ts.map +0 -1
  224. package/esm/types/message.types.d.ts +0 -34
  225. package/esm/types/message.types.d.ts.map +0 -1
  226. package/esm/types/publish.types.d.ts +0 -13
  227. package/esm/types/publish.types.d.ts.map +0 -1
  228. package/esm/types/registry.types.d.ts +0 -4
  229. package/esm/types/registry.types.d.ts.map +0 -1
  230. package/esm/types/subscribe.types.d.ts +0 -18
  231. package/esm/types/subscribe.types.d.ts.map +0 -1
  232. package/esm/utils/connect-to-broker.d.ts.map +0 -1
  233. package/esm/utils/connect-to-broker.js +0 -171
  234. package/esm/utils/connect-to-broker.js.map +0 -1
  235. package/esm/utils/index.d.ts +0 -2
  236. package/esm/utils/index.d.ts.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect-to-broker.mjs","names":[],"sources":["../../../../../../@warlock.js/herald/src/utils/connect-to-broker.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,eAAsB,gBAAgB,SAA6C;CAEjF,MAAM,aAAa,QAAQ,UAAU;CACrC,MAAM,aAAa,QAAQ,QAAQ;CACnC,MAAM,YAAY,QAAQ,aAAa;CAGvC,IAAI;CAEJ,QAAQ,YAAR;EACE,KAAK,YAAY;GACf,MAAM,gBAAgB;GAEtB,MAAM,EAAE,mBAAmB,MAAM,OAAO;GACxC,SAAS,IAAI,eAAe,aAAa;GACzC;EACF;EAEA,KAAK,SAGH,MAAM,IAAI,MACR,iFACF;EAGF,SACE,MAAM,IAAI,MAAM,oBAAoB,WAAW,sCAAsC;CACzF;CAGA,MAAM,SAAS,eAAe,SAAS;EACrC,MAAM;EACN;EACA;CACF,CAAC;CAGD,IAAI;EACF,MAAM,OAAO,QAAQ;CACvB,SAAS,OAAO;EACd,MAAM,IAAI,MACR,wBAAwB,WAAW,IAAI,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAC9F;CACF;CAEA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,OAAO,MAAuB;CAC5C,OAAO,eAAe,IAAI,IAAI;AAChC;;;;;;;;;;;;;;;;;AAkBA,SAAgB,cACd,MACA,SAC2B;CAC3B,OAAO,OAAO,EAAE,QAAkB,MAAM,OAAO;AACjD;;;;;;;;;;;;;AAcA,eAAsB,aAA6C,OAA+B;CAChG,OAAO,OAAO,EAAE,QAAQ,KAAK;AAC/B;;;;;;;;;;;;;AAcA,eAAsB,kBACpB,UACA;CACA,OAAO,OAAO,EAAE,UAAU,QAAQ;AACpC"}
@@ -0,0 +1 @@
1
+ import { connectToBroker, herald, heraldChannel, publishEvent, subscribeConsumer } from "./connect-to-broker.mjs";
@@ -0,0 +1,3 @@
1
+ import { connectToBroker, herald, heraldChannel, publishEvent, subscribeConsumer } from "./connect-to-broker.mjs";
2
+
3
+ export { };
package/llms-full.txt ADDED
@@ -0,0 +1,581 @@
1
+ # Warlock Herald — full skills
2
+
3
+ > Package: `@warlock.js/herald`
4
+
5
+ > Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/herald/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
6
+
7
+ ## consume-message `@warlock.js/herald/consume-message/SKILL.md`
8
+
9
+ ---
10
+ name: consume-message
11
+ description: 'Subscribe to a channel via .subscribe(handler, options?) — handler receives (message, ctx). Control flow via ctx.ack / ctx.nack(requeue?) / ctx.reject / ctx.retry(delayMs). Configure prefetch, retry policy, dead-letter, consumer groups. Triggers: `subscribe`, `ctx.ack`, `ctx.nack`, `ctx.reject`, `ctx.retry`, `prefetch`, `group`, `retry`, `deadLetter`, `@Consumable`, `EventConsumer`, `defineConsumer`, `autoAck`; "consume messages from a queue", "implement a worker", "background queue processor", "set up retry and dead-letter", "class-based event consumer"; typical import `import { herald, Consumable, EventConsumer } from "@warlock.js/herald"`. Skip: setup — `@warlock.js/herald/herald-basics/SKILL.md`; producing — `@warlock.js/herald/publish-message/SKILL.md`; RPC reply — `@warlock.js/herald/request-and-respond/SKILL.md`; competing libs `amqplib`, `bullmq`, `kafkajs`; NestJS `@MessagePattern` / `@EventPattern`.'
12
+ ---
13
+
14
+ # Consume messages
15
+
16
+ Subscribe to a channel; receive each message with a flow-control `ctx`.
17
+
18
+ ## Minimal subscribe
19
+
20
+ ```ts
21
+ import { herald } from "@warlock.js/herald";
22
+
23
+ herald()
24
+ .channel<{ userId: number; email: string }>("user.created")
25
+ .subscribe(async (message, ctx) => {
26
+ try {
27
+ await sendWelcomeEmail(message.payload.email);
28
+ await ctx.ack(); // success — message removed from queue
29
+ } catch (error) {
30
+ await ctx.nack(true); // failure — requeue for redelivery
31
+ }
32
+ });
33
+ ```
34
+
35
+ **Smart auto-ack is on by default** (when `autoAck` is unset or `false`). The consumer runs with manual-ack enabled, and herald acks/nacks based on how your handler ends:
36
+
37
+ - Handler returns without calling any `ctx` method → herald **auto-acks**.
38
+ - Handler **throws** → herald **auto-nacks** (requeue, or DLQ/reject once retries are exhausted — see below).
39
+ - Handler calls `ctx.ack()` / `ctx.nack()` / `ctx.reject()` / `ctx.retry()` → herald respects that and does nothing further.
40
+
41
+ So the `try/catch` above is optional — letting the error throw produces the same auto-nack. Call `ctx` methods explicitly only when you want a *non-default* outcome (reject without requeue, route to DLQ, delayed retry). The one mode that loses messages is `autoAck: true`, where the broker acks on delivery before your handler runs.
42
+
43
+ ## Message context — flow control
44
+
45
+ ```ts
46
+ ctx.ack(); // acknowledge — message removed from the queue
47
+ ctx.nack(requeue?); // negative ack — requeue (true) or send to DLQ (false)
48
+ ctx.reject(); // shorthand for nack(false)
49
+ ctx.retry(delayMs); // delayed retry — requeue after delay
50
+ ctx.reply(payload); // for request-response pattern (see request-and-respond skill)
51
+ ```
52
+
53
+ Pick by intent:
54
+
55
+ | Intent | Use |
56
+ | --- | --- |
57
+ | Processed cleanly | `ctx.ack()` |
58
+ | Transient failure, try again | `ctx.nack(true)` or `ctx.retry(5000)` |
59
+ | Permanent failure, send to DLQ | `ctx.nack(false)` or `ctx.reject()` |
60
+ | Validation failure (bad message) | `ctx.reject()` — don't requeue |
61
+
62
+ ## Subscribe options
63
+
64
+ ```ts
65
+ await channel.subscribe(handler, {
66
+ group: "email-workers", // consumer group / tag — multiple consumers share work
67
+ prefetch: 10, // concurrency — how many in-flight messages this consumer holds
68
+ autoAck: false, // default; keep it false in production
69
+ exclusive: false, // single consumer only on this channel?
70
+ retry: {
71
+ maxRetries: 3, // redelivery ceiling on handler throw (delay not auto-applied — see below)
72
+ delay: 1000,
73
+ },
74
+ deadLetter: {
75
+ channel: "user.created.failed",
76
+ preserveOriginal: true,
77
+ },
78
+ });
79
+ ```
80
+
81
+ `group` is the unit of "share work" — N consumers in the same group split messages across them. Different groups each receive every message (fanout).
82
+
83
+ `prefetch` is the per-consumer concurrency cap. Higher = throughput; lower = even spread. For CPU-bound handlers, set ~= CPU cores. For IO-bound, can go higher (50+).
84
+
85
+ ## Retry policy
86
+
87
+ ```ts
88
+ retry: {
89
+ maxRetries: 3,
90
+ delay: 1000, // see the caveat below — delay is not auto-applied
91
+ }
92
+ ```
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.
95
+
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
+
98
+ ## Dead-letter queue
99
+
100
+ ```ts
101
+ deadLetter: {
102
+ channel: "user.created.failed",
103
+ preserveOriginal: true, // accepted, but currently a no-op (see note)
104
+ }
105
+ ```
106
+
107
+ The DLQ always forwards the full original payload plus the message metadata, so `preserveOriginal` makes no observable difference today — the field is part of the type but the RabbitMQ driver doesn't branch on it yet. Set it or omit it; behaviour is the same.
108
+
109
+ Messages that exhausted retries land in `user.created.failed`. Subscribe to it separately for alerting / manual inspection:
110
+
111
+ ```ts
112
+ herald().channel("user.created.failed").subscribe(async (message, ctx) => {
113
+ await alerts.notify(`Failed: ${JSON.stringify(message.payload)}`);
114
+ await ctx.ack();
115
+ });
116
+ ```
117
+
118
+ ## Decorator-style consumers — `@Consumable` + `EventConsumer`
119
+
120
+ For class-based consumers that pair with the [`EventMessage`](@warlock.js/herald/publish-message/SKILL.md) producer layer. This is a **different handler shape** from the raw `.subscribe()` above — ack/nack is automatic, and you get the unwrapped payload plus an event-metadata object (no `ctx`):
121
+
122
+ ```ts
123
+ import { Consumable, EventConsumer } from "@warlock.js/herald";
124
+ import type { ConsumedEventMessage } from "@warlock.js/herald";
125
+
126
+ @Consumable() // or @Consumable({ broker: "analytics" }) to target a non-default broker
127
+ export class UserCreatedConsumer extends EventConsumer<{ id: number; email: string }> {
128
+ public static eventName = "user.created";
129
+
130
+ // handle(payload, event) — NOT (message, ctx). No ctx.ack() here.
131
+ public async handle(payload: { id: number; email: string }, event: ConsumedEventMessage) {
132
+ await sendWelcomeEmail(payload.email);
133
+ // return cleanly → herald acks. throw → herald nacks (requeue).
134
+ }
135
+ }
136
+ ```
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.
139
+
140
+ 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
+
142
+ **Pair it with the producer:** an `EventConsumer` expects the envelope shape that `publishEvent(new SomeEvent(...))` emits — it reads `envelope.payload`. Don't point one at a channel fed by a bare `channel(name).publish(rawBody)`; the payload nesting won't match.
143
+
144
+ ### Validation + version gating
145
+
146
+ Two optional gates run before `handle`, both driven by fields on the class:
147
+
148
+ ```ts
149
+ import { Consumable, EventConsumer } from "@warlock.js/herald";
150
+ import { v } from "@warlock.js/seal";
151
+
152
+ @Consumable()
153
+ export class UserCreatedConsumer extends EventConsumer {
154
+ public static eventName = "user.created";
155
+
156
+ // Only accept events whose `version` falls in [minVersion, maxVersion].
157
+ // Out-of-range events are acked and skipped (not requeued).
158
+ public static minVersion = 2;
159
+ public static maxVersion = 3;
160
+
161
+ // Seal schema — invalid payloads are nacked before handle() runs.
162
+ public schema = v.object({ id: v.int(), email: v.string().email() });
163
+
164
+ public async handle(payload, event) {
165
+ // payload is validated + version-accepted here
166
+ }
167
+ }
168
+ ```
169
+
170
+ ### `defineConsumer` — no-class shorthand
171
+
172
+ ```ts
173
+ import { defineConsumer } from "@warlock.js/herald";
174
+
175
+ export const userCreatedConsumer = defineConsumer<{ id: number; email: string }>("user.created", {
176
+ schema: userCreatedSchema, // optional seal validation
177
+ handle: async (payload, event) => {
178
+ await sendWelcomeEmail(payload.email);
179
+ },
180
+ });
181
+ ```
182
+
183
+ `defineConsumer` self-registers via `@Consumable` internally, so importing the module is all the wiring it needs — same as the class form. (`subscribeConsumer(SomeConsumerClass)` is the imperative equivalent if you'd rather register explicitly.)
184
+
185
+ ## Subscription handle
186
+
187
+ `subscribe()` resolves to a `Subscription` you can manage:
188
+
189
+ ```ts
190
+ const subscription = await channel.subscribe(handler);
191
+
192
+ subscription.id; // the consumer id (string)
193
+ subscription.isActive(); // boolean
194
+
195
+ await subscription.unsubscribe(); // cancel the consumer on the broker
196
+ await subscription.pause(); // also cancels the consumer (RabbitMQ has no native pause)
197
+ ```
198
+
199
+ On the RabbitMQ driver, `pause()` and `unsubscribe()` both cancel the consumer; `resume()` is **not supported and throws** — create a fresh subscription to start consuming again. You can also cancel by id from the channel: `await channel.unsubscribeById(consumerId)`, or drop every consumer on the channel with `await channel.stopConsuming()`.
200
+
201
+ ## Channel admin
202
+
203
+ The channel exposes queue-management helpers beyond pub/sub:
204
+
205
+ ```ts
206
+ const { messageCount, consumerCount } = await channel.stats(); // queue depth + live consumers
207
+ const purged = await channel.purge(); // drop all pending messages, returns the count
208
+ const ok = await channel.exists(); // does the queue exist on the broker?
209
+ await channel.assert(); // create the queue with its options (idempotent)
210
+ await channel.delete(); // remove the queue entirely
211
+ ```
212
+
213
+ `assert()` runs lazily on the first `publish` / `subscribe`, so you rarely call it directly — reach for it only to pre-create a queue before any traffic.
214
+
215
+ ## Things NOT to do
216
+
217
+ - Don't catch the handler's error and return normally. Smart auto-ack reads a clean return as success and **acks** — silently dropping the work. Let it throw (auto-nack) or call `ctx.reject()` / `ctx.nack(false)` deliberately.
218
+ - Don't `ctx.nack(true)` in an infinite loop. With no `retry` policy, a perpetually-failing message ping-pongs forever. Set `retry.maxRetries` + a `deadLetter` so failures eventually move out.
219
+ - Don't ignore the `deadLetter` channel. Subscribe to it (or at least monitor its depth) — DLQ growth means real failures piling up.
220
+ - Don't use `autoAck: true` for messages that matter. The broker acks on delivery, so a crash mid-handling loses the message.
221
+
222
+ ## See also
223
+
224
+ - [`@warlock.js/herald/publish-message/SKILL.md`](@warlock.js/herald/publish-message/SKILL.md) — the producing side
225
+ - [`@warlock.js/herald/request-and-respond/SKILL.md`](@warlock.js/herald/request-and-respond/SKILL.md) — when you need to reply to a message
226
+
227
+
228
+ ## herald-basics `@warlock.js/herald/herald-basics/SKILL.md`
229
+
230
+ ---
231
+ name: herald-basics
232
+ description: 'Start with @warlock.js/herald — connectToBroker config, herald() factory, channel concept, multi-broker support. Triggers: `connectToBroker`, `herald`, `channel`, `isDefault`, `autoAck`; "set up herald", "wire connectToBroker at boot", "configure multiple brokers", "notifications + analytics + events"; typical import `import { connectToBroker, herald } from "@warlock.js/herald"`. Skip: publishing — `@warlock.js/herald/publish-message/SKILL.md`; consuming — `@warlock.js/herald/consume-message/SKILL.md`; RPC — `@warlock.js/herald/request-and-respond/SKILL.md`; competing libs `amqplib`, `kafkajs`, `bullmq`, `nats`; NestJS messaging; native `EventEmitter`.'
233
+ ---
234
+
235
+ # Herald basics
236
+
237
+ Message bus library — wraps RabbitMQ (Kafka WIP) behind a unified pub/sub API. `herald()` returns a broker, `.channel(name)` returns a pub/sub interface, type-safe via TypeScript generics.
238
+
239
+ > This skill is the herald **map** — read it first, then load the specific skill for the task.
240
+
241
+ ## Install
242
+
243
+ ```bash
244
+ yarn add @warlock.js/herald amqplib # amqplib for RabbitMQ
245
+ ```
246
+
247
+ ## Foundations
248
+
249
+ 1. **`connectToBroker(config)` is the bootstrap.** Call once per broker at app startup, before any publish/subscribe.
250
+ 2. **`herald()` returns the default broker.** `herald("name")` returns a named one. Most apps have one broker; multi-broker is for "notifications + analytics + events" scale.
251
+ 3. **`.channel(name)` is the queue / topic.** Publish into it from producer, subscribe to it from consumer.
252
+ 4. **Channels are typed.** `channel<UserPayload>("user.created")` gives full TS inference on publish and subscribe.
253
+ 5. **`@warlock.js/seal` schemas validate on publish + receive.** Pass `{ schema }` to `.channel(name, { schema })`.
254
+ 6. **Subscribers control message flow** via `ctx.ack()` / `ctx.nack()` / `ctx.reject()` / `ctx.retry(ms)`.
255
+ 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.
256
+
257
+ ## Minimal example
258
+
259
+ ```ts
260
+ import { connectToBroker, herald } from "@warlock.js/herald";
261
+
262
+ // Boot
263
+ await connectToBroker({
264
+ driver: "rabbitmq",
265
+ host: "localhost",
266
+ port: 5672,
267
+ username: "guest",
268
+ password: "guest",
269
+ });
270
+
271
+ // Produce
272
+ await herald().channel("user.created").publish({ userId: 1, email: "ada@example.com" });
273
+
274
+ // Consume
275
+ herald()
276
+ .channel<{ userId: number; email: string }>("user.created")
277
+ .subscribe(async (message, ctx) => {
278
+ console.log("New user:", message.payload.userId);
279
+ await ctx.ack();
280
+ });
281
+ ```
282
+
283
+ ## Multi-broker
284
+
285
+ ```ts
286
+ await connectToBroker({
287
+ driver: "rabbitmq",
288
+ name: "notifications",
289
+ isDefault: true,
290
+ host: process.env.NOTIFICATIONS_HOST,
291
+ });
292
+
293
+ await connectToBroker({
294
+ driver: "rabbitmq",
295
+ name: "analytics",
296
+ isDefault: false, // ← required: `isDefault` defaults to true, so omitting it makes analytics the default
297
+ host: process.env.ANALYTICS_HOST,
298
+ });
299
+
300
+ herald().channel("emails").publish({ /* ... */ }); // → notifications
301
+ herald("analytics").channel("events").publish({ /* ... */ }); // → analytics
302
+ ```
303
+
304
+ `connectToBroker` defaults `isDefault` to `true`, and the registry promotes the most-recently-registered default. So when you register more than one broker, mark the secondaries `isDefault: false` — otherwise the last one wins and `herald()` returns the wrong broker.
305
+
306
+ ## Pick a skill
307
+
308
+ | If the task is about… | Load |
309
+ | --- | --- |
310
+ | Publishing messages — single + batch + publish options (priority, ttl, delay, persistent, headers) | [`@warlock.js/herald/publish-message/SKILL.md`](@warlock.js/herald/publish-message/SKILL.md) |
311
+ | Subscribing to messages — handler signature, message context (ack/nack/retry), prefetch, retry policy, dead-letter | [`@warlock.js/herald/consume-message/SKILL.md`](@warlock.js/herald/consume-message/SKILL.md) |
312
+ | Request/response (RPC) — `channel.request(...)` + `channel.respond(...)` for synchronous-style calls over a message bus | [`@warlock.js/herald/request-and-respond/SKILL.md`](@warlock.js/herald/request-and-respond/SKILL.md) |
313
+ | Schema-validated channels — `.channel(name, { schema })` with `@warlock.js/seal` | See [`@warlock.js/seal/seal-basics/SKILL.md`](@warlock.js/seal/seal-basics/SKILL.md) + skills below |
314
+
315
+ ## Things NOT to do
316
+
317
+ - Don't `connectToBroker` from inside a request handler. Call once at boot.
318
+ - Don't catch a handler error and swallow it — under smart auto-ack a thrown handler auto-nacks (re-delivers), but a caught-and-ignored error looks like success and gets auto-acked, silently dropping the work. Either let it throw or call `ctx.reject()` / `ctx.nack(false)` deliberately.
319
+ - Don't use `autoAck: true` in production. The default smart auto-ack re-delivers on a mid-handling crash; `autoAck: true` acks on delivery, so a crash loses the message.
320
+ - Don't share a typed channel across producer and consumer code without a shared type / schema file. Drift between sides causes silent payload corruption.
321
+
322
+ ## See also
323
+
324
+ - README at `@warlock.js/herald/README.md` for the full API surface and RabbitMQ / Kafka driver config
325
+ - [`@warlock.js/seal/seal-basics/SKILL.md`](@warlock.js/seal/seal-basics/SKILL.md) — schema validation
326
+
327
+
328
+ ## publish-message `@warlock.js/herald/publish-message/SKILL.md`
329
+
330
+ ---
331
+ name: publish-message
332
+ description: 'Publish messages to a channel — .publish(payload, options?) for single, .publishBatch([...], options?) for batch, with priority / ttl / delay / persistent / correlationId / headers options. Optional schema validation via .channel(name, {schema}). Triggers: `publish`, `publishBatch`, `channel`, `priority`, `ttl`, `delay`, `persistent`, `correlationId`, `headers`, `schema`; "publish a message", "emit an event after a domain change", "fan out a notification", "schedule delayed work", "batch publish"; typical import `import { herald } from "@warlock.js/herald"`. Skip: setup — `@warlock.js/herald/herald-basics/SKILL.md`; consuming — `@warlock.js/herald/consume-message/SKILL.md`; RPC — `@warlock.js/herald/request-and-respond/SKILL.md`; competing libs `amqplib`, `kafkajs`, `bullmq`; NestJS `ClientProxy.emit`.'
333
+ ---
334
+
335
+ # Publish messages
336
+
337
+ Push messages into a channel. The consumer side picks them up via [`@warlock.js/herald/consume-message/SKILL.md`](@warlock.js/herald/consume-message/SKILL.md).
338
+
339
+ ## Single publish
340
+
341
+ ```ts
342
+ import { herald } from "@warlock.js/herald";
343
+
344
+ await herald().channel("user.created").publish({
345
+ userId: 1,
346
+ email: "ada@example.com",
347
+ });
348
+ ```
349
+
350
+ Returns when the broker has accepted the message (not when a consumer has handled it — that's `.request()` territory).
351
+
352
+ ## Typed channels
353
+
354
+ ```ts
355
+ type UserPayload = { userId: number; email: string };
356
+
357
+ const channel = herald().channel<UserPayload>("user.created");
358
+
359
+ await channel.publish({ userId: 1, email: "test@example.com" }); // ✅ typed
360
+ await channel.publish({ userId: "1" } as never); // ❌ compile error
361
+ ```
362
+
363
+ Share the payload type between producer and consumer via a common `types/` file.
364
+
365
+ ## Schema-validated publish
366
+
367
+ ```ts
368
+ import { v } from "@warlock.js/seal";
369
+
370
+ const userSchema = v.object({
371
+ userId: v.int(),
372
+ email: v.string().email(),
373
+ });
374
+
375
+ const channel = herald().channel("user.created", { schema: userSchema });
376
+
377
+ await channel.publish({ userId: 1, email: "invalid" }); // Throws — fails .email()
378
+ ```
379
+
380
+ The schema runs `validate()` before the message hits the broker. Invalid payloads never leave the producer. See [`@warlock.js/seal/seal-basics/SKILL.md`](@warlock.js/seal/seal-basics/SKILL.md).
381
+
382
+ ## Publish options
383
+
384
+ ```ts
385
+ await channel.publish(payload, {
386
+ priority: 5, // 0-9, higher = served first
387
+ expiration: 60_000, // ms — message expires if not consumed (RabbitMQ uses `expiration`, not `ttl`)
388
+ delay: 5_000, // ms — delayed delivery (requires RabbitMQ delay plugin)
389
+ persistent: true, // survive broker restart (default true)
390
+ correlationId: "uuid", // for tracking across services
391
+ headers: {
392
+ tenantId: "42",
393
+ source: "billing-service",
394
+ },
395
+ });
396
+ ```
397
+
398
+ The headers field is free-form — consumers read `message.metadata.headers?.tenantId` to route or filter (headers live under `message.metadata`, alongside `messageId`, `correlationId`, `timestamp`, and `retryCount`).
399
+
400
+ ## Batch publishing
401
+
402
+ ```ts
403
+ await channel.publishBatch([
404
+ { userId: 1, email: "a@example.com" },
405
+ { userId: 2, email: "b@example.com" },
406
+ { userId: 3, email: "c@example.com" },
407
+ ], {
408
+ // Options apply to every message in the batch
409
+ persistent: true,
410
+ });
411
+ ```
412
+
413
+ Under the hood `publishBatch` iterates and calls `.publish()` per message — there's no AMQP-level batching today. It's an ergonomic shape for "emit these N items" (bulk import, fanout to many recipients), not a throughput optimization. The same `options` apply to every message.
414
+
415
+ ## Publish + transaction (outbox pattern)
416
+
417
+ Don't publish inside a database transaction — if the transaction rolls back but the publish already happened, you've emitted an event for a state that never persisted.
418
+
419
+ The outbox pattern: write to an `outbox` table inside the transaction, dispatch from the outbox via a worker after commit:
420
+
421
+ ```ts
422
+ await transaction(async () => {
423
+ await Order.create(orderData);
424
+ await Outbox.create({
425
+ channel: "order.created",
426
+ payload: { orderId: order.id, ... },
427
+ status: "pending",
428
+ });
429
+ });
430
+
431
+ // In a separate worker:
432
+ const pending = await Outbox.where("status", "pending").get();
433
+ for (const row of pending) {
434
+ await herald().channel(row.get("channel")).publish(row.get("payload"));
435
+ await row.merge({ status: "sent", sent_at: new Date() }).save();
436
+ }
437
+ ```
438
+
439
+ Full recipe at `domains/cascade/docs/recipes/outbox-pattern.md`.
440
+
441
+ ## Broadcasting use-case results
442
+
443
+ `@warlock.js/herald` ships `heraldBroadcast()` — a channel adapter that lets a `@warlock.js/core` `useCase()` publish its result to the bus automatically on success. Register it once in the use-cases config; each use case opts in with `broadcast: true`.
444
+
445
+ ```ts title="src/config/use-cases.ts"
446
+ import { type UseCaseConfigurations } from "@warlock.js/core";
447
+ import { heraldBroadcast } from "@warlock.js/herald";
448
+
449
+ export default {
450
+ broadcast: {
451
+ enabled: true,
452
+ channels: [heraldBroadcast({ broker: "default" })], // omit broker for the default
453
+ },
454
+ } satisfies UseCaseConfigurations;
455
+ ```
456
+
457
+ ```ts
458
+ // The use case opts in — channel name defaults to the use case name.
459
+ export const createUserUseCase = useCase({
460
+ name: "users.create", // → publishes to channel "users.create"
461
+ schema: createUserSchema,
462
+ handler: async (data) => User.create(data),
463
+ broadcast: true,
464
+ });
465
+ ```
466
+
467
+ The adapter publishes the broadcast envelope's `payload` to `channel(event.event)`. It's structurally typed, so herald keeps no dependency on core. See [`@warlock.js/core/write-use-case/SKILL.md`](@warlock.js/core/write-use-case/SKILL.md) for the `broadcast` option (payload projection, custom event name, payload-safety rules).
468
+
469
+ ## Event classes (`EventMessage`) — the typed event layer
470
+
471
+ Raw `channel(name).publish(payload)` is the low-level path. For domain events that travel between modules, herald ships a higher-level pair: define the event once as a class, publish instances of it, and consume them with an [`EventConsumer`](@warlock.js/herald/consume-message/SKILL.md). The channel name comes from the event's `eventName` — no string to keep in sync across producer and consumer.
472
+
473
+ ```ts
474
+ import { defineEvent, publishEvent } from "@warlock.js/herald";
475
+
476
+ // `defineEvent<IncomingData, OutgoingData>` — `toJSON` projects the wire payload.
477
+ const UserCreatedEvent = defineEvent<User, { id: number; email: string }>("user.created", {
478
+ toJSON: (user) => ({ id: user.id, email: user.email }),
479
+ // schema?: an @warlock.js/seal ObjectValidator (held on the instance; the consumer side validates)
480
+ });
481
+
482
+ // `publishEvent` serializes the instance and publishes to channel "user.created" on the default broker.
483
+ await publishEvent(new UserCreatedEvent(user));
484
+ ```
485
+
486
+ `publishEvent(event)` is shorthand for `herald().publish(event)` — it calls `event.serialize()` and sends an **envelope**: `{ payload, metadata, messageId, eventName, version, occurredAt }`. That nesting is why an `EventConsumer` reads `envelope.payload`, not the raw body — pair this only with the consumer side, not a bare `.subscribe()`.
487
+
488
+ Set an optional `version` on the event (instance field) to drive the consumer's `minVersion` / `maxVersion` acceptance gate. The class form (`class X extends EventMessage`) is equivalent — override `toJSON()` and set `eventName`, `version`, `schema` as fields.
489
+
490
+ ## Things NOT to do
491
+
492
+ - Don't publish inside a transaction. Use the outbox pattern.
493
+ - Don't pass non-JSON-serializable values (functions, `BigInt`, class instances with methods). Serialization happens at the broker boundary; functions go silent, BigInt throws.
494
+ - Don't `expiration: 0` — set a real expiry or omit. `0` means "expire immediately."
495
+ - Don't omit `persistent: true` for messages where loss matters. Default is `true` in this lib, but worth being explicit when the message represents money / customer state.
496
+ - Don't put secrets in headers. Headers travel in plaintext (encrypted only by TLS in transit, not at rest in the broker).
497
+
498
+ ## See also
499
+
500
+ - [`@warlock.js/herald/consume-message/SKILL.md`](@warlock.js/herald/consume-message/SKILL.md) — the receiving side
501
+ - [`@warlock.js/herald/request-and-respond/SKILL.md`](@warlock.js/herald/request-and-respond/SKILL.md) — when you need a reply
502
+
503
+
504
+ ## request-and-respond `@warlock.js/herald/request-and-respond/SKILL.md`
505
+
506
+ ---
507
+ name: request-and-respond
508
+ description: 'Synchronous-style RPC over the message bus — channel.request<R>(payload, {timeout}) waits for a reply, channel.respond(handler) registers the responder, ctx.reply(response) sends the answer. Triggers: `channel.request`, `channel.respond`, `ctx.reply`, `timeout`, `correlationId`, `headers.correlationId`; "RPC over message bus", "request-response across services", "wait for a reply", "internal service-to-service call instead of HTTP"; typical import `import { herald } from "@warlock.js/herald"`. Skip: setup — `@warlock.js/herald/herald-basics/SKILL.md`; fire-and-forget — `@warlock.js/herald/publish-message/SKILL.md`; consumer ctx flow — `@warlock.js/herald/consume-message/SKILL.md`; competing libs `amqplib` RPC, `nats` request/reply; NestJS `ClientProxy.send`; gRPC; HTTP.'
509
+ ---
510
+
511
+ # Request-response over the bus
512
+
513
+ Most messaging is fire-and-forget. When you need a reply, use the request/respond pair — a reply-queue + correlation-id pattern under the hood, surfaced as a typed promise.
514
+
515
+ ## Shape
516
+
517
+ ```ts
518
+ // Caller (client)
519
+ const response = await herald()
520
+ .channel<RequestPayload>("compute.tax")
521
+ .request<TaxResponse>({ amount: 1000, country: "US" }, { timeout: 30_000 });
522
+
523
+ // Responder (server)
524
+ herald()
525
+ .channel<RequestPayload>("compute.tax")
526
+ .respond(async (message, ctx) => {
527
+ const tax = await computeTax(message.payload);
528
+ return { tax, currency: "USD" }; // ← the return value IS the reply
529
+ });
530
+ ```
531
+
532
+ `request` returns a promise that resolves with the responder's return value. `respond` registers a handler and **automatically replies with whatever the handler returns** (then acks) — so just `return` the response; you don't call `ctx.reply()` yourself inside a `respond` handler. (`ctx.reply` is the lower-level primitive used when you wire a plain `.subscribe()` as a responder by hand.)
533
+
534
+ ## Timeout
535
+
536
+ ```ts
537
+ await channel.request(payload, { timeout: 30_000 });
538
+ ```
539
+
540
+ The promise rejects if no reply arrives within `timeout` ms. Pick a sane number — too short rejects mid-work; too long lets a hung responder block the caller.
541
+
542
+ ## When to use it vs HTTP
543
+
544
+ | Use HTTP | Use request/respond |
545
+ | --- | --- |
546
+ | Stateless, fast, idempotent ops | Slow / queued ops where the caller can wait |
547
+ | Public API surface | Internal service-to-service |
548
+ | Frontend consumption | Backend orchestration |
549
+ | Synchronous user-facing flow | Async-but-needs-result patterns |
550
+
551
+ The bus adds queue persistence and retry. HTTP is faster for sub-100ms ops; the bus shines when the operation has variable duration or needs a queue's backpressure.
552
+
553
+ ## When NOT to use it
554
+
555
+ - "I need a result in under 50ms" — too much overhead on the bus.
556
+ - "The responder might be down for hours" — request will time out repeatedly; consider `publish` + write the result somewhere the caller polls.
557
+ - "Many callers, the response is the same for all" — cache the result and use `publish` for invalidation.
558
+
559
+ ## Multiple responders
560
+
561
+ `respond(handler)` takes no options — there's no `group` knob here. If multiple consumers `respond()` to the same channel, they all sit on the same queue, so RabbitMQ round-robins between them: **one responder handles each request, and the caller gets exactly one reply.** That's the "share work across responders" pattern — a synchronous worker pool. Make every responder functionally identical, since the caller has no say in which one answers.
562
+
563
+ For "multiple replies expected" patterns, use a regular `subscribe` + a request id, then the caller listens on a result channel.
564
+
565
+ ## Correlation across replies
566
+
567
+ Each `request()` generates its own correlation ID and an exclusive reply queue under the hood. The reply carries the same ID; the client matches it to the awaiting promise and resolves. You don't manage correlation IDs manually — and you shouldn't pass your own `correlationId` on a `request()`, since herald overwrites it with the one it uses to match the reply. To thread your own trace id (e.g. a transaction id for logs) through to the responder, put it in `headers` instead — the responder reads it from `message.metadata.headers`.
568
+
569
+ ## Things NOT to do
570
+
571
+ - Don't use request/respond for high-throughput, low-latency ops. The reply-queue round-trip adds at least one broker hop. HTTP is the right tool.
572
+ - Don't return huge payloads from `respond`. Reply messages travel through the broker; a multi-MB response is a load on the bus and on memory. For big results, write to S3 / cache and reply with a reference.
573
+ - Don't forget to `respond` in long-lived consumers. If the responder crashes, every caller's request will time out — they don't know why.
574
+ - Don't set timeout to `Infinity`. A stuck request becomes a leaked promise — set a real timeout and handle the rejection.
575
+
576
+ ## See also
577
+
578
+ - [`@warlock.js/herald/publish-message/SKILL.md`](@warlock.js/herald/publish-message/SKILL.md) — fire-and-forget pattern
579
+ - [`@warlock.js/herald/consume-message/SKILL.md`](@warlock.js/herald/consume-message/SKILL.md) — subscribe + ctx flow control (including `ctx.reply`)
580
+
581
+
package/llms.txt ADDED
@@ -0,0 +1,12 @@
1
+ # Warlock Herald
2
+
3
+ > Package: `@warlock.js/herald`
4
+
5
+ > Message bus/brokers for RabbitMQ, Kafka, and more
6
+
7
+ ## Skills
8
+
9
+ - [consume-message](@warlock.js/herald/consume-message/SKILL.md): Subscribe to a channel via .subscribe(handler, options?) — handler receives (message, ctx). Control flow via ctx.ack / ctx.nack(requeue?) / ctx.reject / ctx.retry(delayMs). Configure prefetch, retry policy, dead-letter, consumer groups. Triggers: `subscribe`, `ctx.ack`, `ctx.nack`, `ctx.reject`, `ctx.retry`, `prefetch`, `group`, `retry`, `deadLetter`, `@Consumable`, `EventConsumer`, `defineConsumer`, `autoAck`; "consume messages from a queue", "implement a worker", "background queue processor", "set up retry and dead-letter", "class-based event consumer"; typical import `import { herald, Consumable, EventConsumer } from "@warlock.js/herald"`. Skip: setup — `@warlock.js/herald/herald-basics/SKILL.md`; producing — `@warlock.js/herald/publish-message/SKILL.md`; RPC reply — `@warlock.js/herald/request-and-respond/SKILL.md`; competing libs `amqplib`, `bullmq`, `kafkajs`; NestJS `@MessagePattern` / `@EventPattern`.
10
+ - [herald-basics](@warlock.js/herald/herald-basics/SKILL.md): Start with @warlock.js/herald — connectToBroker config, herald() factory, channel concept, multi-broker support. Triggers: `connectToBroker`, `herald`, `channel`, `isDefault`, `autoAck`; "set up herald", "wire connectToBroker at boot", "configure multiple brokers", "notifications + analytics + events"; typical import `import { connectToBroker, herald } from "@warlock.js/herald"`. Skip: publishing — `@warlock.js/herald/publish-message/SKILL.md`; consuming — `@warlock.js/herald/consume-message/SKILL.md`; RPC — `@warlock.js/herald/request-and-respond/SKILL.md`; competing libs `amqplib`, `kafkajs`, `bullmq`, `nats`; NestJS messaging; native `EventEmitter`.
11
+ - [publish-message](@warlock.js/herald/publish-message/SKILL.md): Publish messages to a channel — .publish(payload, options?) for single, .publishBatch([...], options?) for batch, with priority / ttl / delay / persistent / correlationId / headers options. Optional schema validation via .channel(name, {schema}). Triggers: `publish`, `publishBatch`, `channel`, `priority`, `ttl`, `delay`, `persistent`, `correlationId`, `headers`, `schema`; "publish a message", "emit an event after a domain change", "fan out a notification", "schedule delayed work", "batch publish"; typical import `import { herald } from "@warlock.js/herald"`. Skip: setup — `@warlock.js/herald/herald-basics/SKILL.md`; consuming — `@warlock.js/herald/consume-message/SKILL.md`; RPC — `@warlock.js/herald/request-and-respond/SKILL.md`; competing libs `amqplib`, `kafkajs`, `bullmq`; NestJS `ClientProxy.emit`.
12
+ - [request-and-respond](@warlock.js/herald/request-and-respond/SKILL.md): Synchronous-style RPC over the message bus — channel.request<R>(payload, {timeout}) waits for a reply, channel.respond(handler) registers the responder, ctx.reply(response) sends the answer. Triggers: `channel.request`, `channel.respond`, `ctx.reply`, `timeout`, `correlationId`, `headers.correlationId`; "RPC over message bus", "request-response across services", "wait for a reply", "internal service-to-service call instead of HTTP"; typical import `import { herald } from "@warlock.js/herald"`. Skip: setup — `@warlock.js/herald/herald-basics/SKILL.md`; fire-and-forget — `@warlock.js/herald/publish-message/SKILL.md`; consumer ctx flow — `@warlock.js/herald/consume-message/SKILL.md`; competing libs `amqplib` RPC, `nats` request/reply; NestJS `ClientProxy.send`; gRPC; HTTP.