@lindorm/iris 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/README.md +903 -213
  2. package/dist/classes/IrisSession.d.ts +28 -0
  3. package/dist/classes/IrisSession.d.ts.map +1 -0
  4. package/dist/classes/IrisSession.js +42 -0
  5. package/dist/classes/IrisSession.js.map +1 -0
  6. package/dist/classes/IrisSource.d.ts +6 -5
  7. package/dist/classes/IrisSource.d.ts.map +1 -1
  8. package/dist/classes/IrisSource.js +30 -46
  9. package/dist/classes/IrisSource.js.map +1 -1
  10. package/dist/classes/index.d.ts +1 -0
  11. package/dist/classes/index.d.ts.map +1 -1
  12. package/dist/classes/index.js +1 -0
  13. package/dist/classes/index.js.map +1 -1
  14. package/dist/cli.d.ts +3 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +35 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/decorators/AbstractMessage.js +1 -1
  19. package/dist/decorators/AbstractMessage.js.map +1 -1
  20. package/dist/decorators/AfterConsume.js +1 -1
  21. package/dist/decorators/AfterConsume.js.map +1 -1
  22. package/dist/decorators/AfterPublish.js +1 -1
  23. package/dist/decorators/AfterPublish.js.map +1 -1
  24. package/dist/decorators/BeforeConsume.js +1 -1
  25. package/dist/decorators/BeforeConsume.js.map +1 -1
  26. package/dist/decorators/BeforePublish.js +1 -1
  27. package/dist/decorators/BeforePublish.js.map +1 -1
  28. package/dist/decorators/Broadcast.js +1 -1
  29. package/dist/decorators/Broadcast.js.map +1 -1
  30. package/dist/decorators/Compressed.js +1 -1
  31. package/dist/decorators/Compressed.js.map +1 -1
  32. package/dist/decorators/CorrelationField.js +1 -1
  33. package/dist/decorators/CorrelationField.js.map +1 -1
  34. package/dist/decorators/DeadLetter.js +1 -1
  35. package/dist/decorators/DeadLetter.js.map +1 -1
  36. package/dist/decorators/Default.d.ts +3 -0
  37. package/dist/decorators/Default.d.ts.map +1 -0
  38. package/dist/decorators/Default.js +13 -0
  39. package/dist/decorators/Default.js.map +1 -0
  40. package/dist/decorators/Delay.js +1 -1
  41. package/dist/decorators/Delay.js.map +1 -1
  42. package/dist/decorators/Encrypted.js +1 -1
  43. package/dist/decorators/Encrypted.js.map +1 -1
  44. package/dist/decorators/Enum.js +1 -1
  45. package/dist/decorators/Enum.js.map +1 -1
  46. package/dist/decorators/Expiry.js +1 -1
  47. package/dist/decorators/Expiry.js.map +1 -1
  48. package/dist/decorators/Field.d.ts +2 -3
  49. package/dist/decorators/Field.d.ts.map +1 -1
  50. package/dist/decorators/Field.js +6 -7
  51. package/dist/decorators/Field.js.map +1 -1
  52. package/dist/decorators/Generated.d.ts +1 -1
  53. package/dist/decorators/Generated.d.ts.map +1 -1
  54. package/dist/decorators/Generated.js +1 -1
  55. package/dist/decorators/Generated.js.map +1 -1
  56. package/dist/decorators/Header.js +1 -1
  57. package/dist/decorators/Header.js.map +1 -1
  58. package/dist/decorators/IdentifierField.js +1 -1
  59. package/dist/decorators/IdentifierField.js.map +1 -1
  60. package/dist/decorators/MandatoryField.js +1 -1
  61. package/dist/decorators/MandatoryField.js.map +1 -1
  62. package/dist/decorators/Max.js +1 -1
  63. package/dist/decorators/Max.js.map +1 -1
  64. package/dist/decorators/Message.js +2 -2
  65. package/dist/decorators/Message.js.map +1 -1
  66. package/dist/decorators/Min.js +1 -1
  67. package/dist/decorators/Min.js.map +1 -1
  68. package/dist/decorators/Namespace.js +1 -1
  69. package/dist/decorators/Namespace.js.map +1 -1
  70. package/dist/decorators/Nullable.d.ts +2 -0
  71. package/dist/decorators/Nullable.d.ts.map +1 -0
  72. package/dist/decorators/Nullable.js +13 -0
  73. package/dist/decorators/Nullable.js.map +1 -0
  74. package/dist/decorators/OnConsumeError.js +1 -1
  75. package/dist/decorators/OnConsumeError.js.map +1 -1
  76. package/dist/decorators/OnCreate.js +1 -1
  77. package/dist/decorators/OnCreate.js.map +1 -1
  78. package/dist/decorators/OnHydrate.js +1 -1
  79. package/dist/decorators/OnHydrate.js.map +1 -1
  80. package/dist/decorators/OnValidate.js +1 -1
  81. package/dist/decorators/OnValidate.js.map +1 -1
  82. package/dist/decorators/Optional.d.ts +2 -0
  83. package/dist/decorators/Optional.d.ts.map +1 -0
  84. package/dist/decorators/Optional.js +13 -0
  85. package/dist/decorators/Optional.js.map +1 -0
  86. package/dist/decorators/Persistent.js +1 -1
  87. package/dist/decorators/Persistent.js.map +1 -1
  88. package/dist/decorators/PersistentField.js +1 -1
  89. package/dist/decorators/PersistentField.js.map +1 -1
  90. package/dist/decorators/Priority.js +1 -1
  91. package/dist/decorators/Priority.js.map +1 -1
  92. package/dist/decorators/Retry.js +1 -1
  93. package/dist/decorators/Retry.js.map +1 -1
  94. package/dist/decorators/Schema.d.ts +1 -1
  95. package/dist/decorators/Schema.d.ts.map +1 -1
  96. package/dist/decorators/Schema.js +1 -1
  97. package/dist/decorators/Schema.js.map +1 -1
  98. package/dist/decorators/TimestampField.js +1 -1
  99. package/dist/decorators/TimestampField.js.map +1 -1
  100. package/dist/decorators/Topic.js +1 -1
  101. package/dist/decorators/Topic.js.map +1 -1
  102. package/dist/decorators/Transform.d.ts +1 -1
  103. package/dist/decorators/Transform.d.ts.map +1 -1
  104. package/dist/decorators/Transform.js +1 -1
  105. package/dist/decorators/Transform.js.map +1 -1
  106. package/dist/decorators/Version.js +1 -1
  107. package/dist/decorators/Version.js.map +1 -1
  108. package/dist/decorators/index.d.ts +3 -0
  109. package/dist/decorators/index.d.ts.map +1 -1
  110. package/dist/decorators/index.js +3 -0
  111. package/dist/decorators/index.js.map +1 -1
  112. package/dist/index.d.ts +1 -0
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js +1 -0
  115. package/dist/index.js.map +1 -1
  116. package/dist/interfaces/IrisDriver.d.ts +4 -2
  117. package/dist/interfaces/IrisDriver.d.ts.map +1 -1
  118. package/dist/interfaces/IrisMessagingProvider.d.ts +21 -0
  119. package/dist/interfaces/IrisMessagingProvider.d.ts.map +1 -0
  120. package/dist/interfaces/IrisMessagingProvider.js +3 -0
  121. package/dist/interfaces/IrisMessagingProvider.js.map +1 -0
  122. package/dist/interfaces/IrisSession.d.ts +4 -0
  123. package/dist/interfaces/IrisSession.d.ts.map +1 -0
  124. package/dist/{internal/types/iris-source-init.js → interfaces/IrisSession.js} +1 -1
  125. package/dist/interfaces/IrisSession.js.map +1 -0
  126. package/dist/interfaces/IrisSource.d.ts +8 -19
  127. package/dist/interfaces/IrisSource.d.ts.map +1 -1
  128. package/dist/interfaces/index.d.ts +2 -0
  129. package/dist/interfaces/index.d.ts.map +1 -1
  130. package/dist/interfaces/index.js +2 -0
  131. package/dist/interfaces/index.js.map +1 -1
  132. package/dist/internal/cli/commands/generate-message.d.ts +7 -0
  133. package/dist/internal/cli/commands/generate-message.d.ts.map +1 -0
  134. package/dist/internal/cli/commands/generate-message.js +53 -0
  135. package/dist/internal/cli/commands/generate-message.js.map +1 -0
  136. package/dist/internal/cli/commands/init.d.ts +8 -0
  137. package/dist/internal/cli/commands/init.d.ts.map +1 -0
  138. package/dist/internal/cli/commands/init.js +55 -0
  139. package/dist/internal/cli/commands/init.js.map +1 -0
  140. package/dist/internal/cli/commands/register-generate.d.ts +3 -0
  141. package/dist/internal/cli/commands/register-generate.d.ts.map +1 -0
  142. package/dist/internal/cli/commands/register-generate.js +20 -0
  143. package/dist/internal/cli/commands/register-generate.js.map +1 -0
  144. package/dist/internal/cli/commands/register-init.d.ts +3 -0
  145. package/dist/internal/cli/commands/register-init.d.ts.map +1 -0
  146. package/dist/internal/cli/commands/register-init.js +16 -0
  147. package/dist/internal/cli/commands/register-init.js.map +1 -0
  148. package/dist/internal/drivers/kafka/classes/KafkaDriver.d.ts +5 -3
  149. package/dist/internal/drivers/kafka/classes/KafkaDriver.d.ts.map +1 -1
  150. package/dist/internal/drivers/kafka/classes/KafkaDriver.js +12 -16
  151. package/dist/internal/drivers/kafka/classes/KafkaDriver.js.map +1 -1
  152. package/dist/internal/drivers/kafka/classes/KafkaMessageBus.d.ts.map +1 -1
  153. package/dist/internal/drivers/kafka/classes/KafkaMessageBus.js.map +1 -1
  154. package/dist/internal/drivers/kafka/classes/KafkaRpcServer.js.map +1 -1
  155. package/dist/internal/drivers/kafka/classes/KafkaWorkerQueue.d.ts.map +1 -1
  156. package/dist/internal/drivers/kafka/classes/KafkaWorkerQueue.js.map +1 -1
  157. package/dist/internal/drivers/kafka/utils/create-kafka-consumer.js +1 -1
  158. package/dist/internal/drivers/kafka/utils/create-kafka-consumer.js.map +1 -1
  159. package/dist/internal/drivers/kafka/utils/stop-kafka-consumer.d.ts +3 -1
  160. package/dist/internal/drivers/kafka/utils/stop-kafka-consumer.d.ts.map +1 -1
  161. package/dist/internal/drivers/kafka/utils/stop-kafka-consumer.js +24 -5
  162. package/dist/internal/drivers/kafka/utils/stop-kafka-consumer.js.map +1 -1
  163. package/dist/internal/drivers/memory/classes/MemoryDriver.d.ts +5 -3
  164. package/dist/internal/drivers/memory/classes/MemoryDriver.d.ts.map +1 -1
  165. package/dist/internal/drivers/memory/classes/MemoryDriver.js +11 -14
  166. package/dist/internal/drivers/memory/classes/MemoryDriver.js.map +1 -1
  167. package/dist/internal/drivers/nats/classes/NatsDriver.d.ts +5 -3
  168. package/dist/internal/drivers/nats/classes/NatsDriver.d.ts.map +1 -1
  169. package/dist/internal/drivers/nats/classes/NatsDriver.js +12 -15
  170. package/dist/internal/drivers/nats/classes/NatsDriver.js.map +1 -1
  171. package/dist/internal/drivers/rabbit/classes/RabbitDriver.d.ts +5 -3
  172. package/dist/internal/drivers/rabbit/classes/RabbitDriver.d.ts.map +1 -1
  173. package/dist/internal/drivers/rabbit/classes/RabbitDriver.js +11 -14
  174. package/dist/internal/drivers/rabbit/classes/RabbitDriver.js.map +1 -1
  175. package/dist/internal/drivers/redis/classes/RedisDriver.d.ts +5 -3
  176. package/dist/internal/drivers/redis/classes/RedisDriver.d.ts.map +1 -1
  177. package/dist/internal/drivers/redis/classes/RedisDriver.js +11 -14
  178. package/dist/internal/drivers/redis/classes/RedisDriver.js.map +1 -1
  179. package/dist/internal/drivers/redis/classes/RedisRpcServer.js.map +1 -1
  180. package/dist/internal/drivers/redis/utils/create-consumer-loop.js +1 -1
  181. package/dist/internal/drivers/redis/utils/create-consumer-loop.js.map +1 -1
  182. package/dist/internal/drivers/redis/utils/stop-consumer-loop.js +2 -2
  183. package/dist/internal/drivers/redis/utils/stop-consumer-loop.js.map +1 -1
  184. package/dist/internal/message/metadata/build-message-metadata.d.ts.map +1 -1
  185. package/dist/internal/message/metadata/build-message-metadata.js +8 -0
  186. package/dist/internal/message/metadata/build-message-metadata.js.map +1 -1
  187. package/dist/internal/message/types/metadata.d.ts +1 -1
  188. package/dist/internal/message/types/metadata.d.ts.map +1 -1
  189. package/dist/internal/message/types/staged.d.ts +6 -1
  190. package/dist/internal/message/types/staged.d.ts.map +1 -1
  191. package/dist/internal/message/utils/build-schema.d.ts +1 -1
  192. package/dist/internal/message/utils/build-schema.d.ts.map +1 -1
  193. package/dist/internal/message/utils/build-schema.js +14 -14
  194. package/dist/internal/message/utils/build-schema.js.map +1 -1
  195. package/dist/internal/message/utils/encrypt.d.ts +1 -1
  196. package/dist/internal/message/utils/encrypt.d.ts.map +1 -1
  197. package/dist/internal/message/utils/encrypt.js +4 -3
  198. package/dist/internal/message/utils/encrypt.js.map +1 -1
  199. package/dist/internal/message/utils/prepare-inbound.js +1 -1
  200. package/dist/internal/message/utils/prepare-inbound.js.map +1 -1
  201. package/dist/internal/types/index.d.ts +0 -1
  202. package/dist/internal/types/index.d.ts.map +1 -1
  203. package/dist/internal/types/index.js +0 -1
  204. package/dist/internal/types/index.js.map +1 -1
  205. package/dist/mocks/create-mock-iris-session.d.ts +4 -0
  206. package/dist/mocks/create-mock-iris-session.d.ts.map +1 -0
  207. package/dist/mocks/create-mock-iris-session.js +20 -0
  208. package/dist/mocks/create-mock-iris-session.js.map +1 -0
  209. package/dist/mocks/create-mock-iris-source.d.ts +2 -1
  210. package/dist/mocks/create-mock-iris-source.d.ts.map +1 -1
  211. package/dist/mocks/create-mock-iris-source.js +5 -2
  212. package/dist/mocks/create-mock-iris-source.js.map +1 -1
  213. package/dist/mocks/index.d.ts +2 -1
  214. package/dist/mocks/index.d.ts.map +1 -1
  215. package/dist/mocks/index.js +5 -1
  216. package/dist/mocks/index.js.map +1 -1
  217. package/dist/types/decorator-options.d.ts +0 -7
  218. package/dist/types/decorator-options.d.ts.map +1 -1
  219. package/dist/types/events.d.ts +5 -0
  220. package/dist/types/events.d.ts.map +1 -0
  221. package/dist/types/events.js +3 -0
  222. package/dist/types/events.js.map +1 -0
  223. package/dist/types/index.d.ts +2 -1
  224. package/dist/types/index.d.ts.map +1 -1
  225. package/dist/types/index.js +1 -0
  226. package/dist/types/index.js.map +1 -1
  227. package/dist/types/source-options.d.ts +1 -1
  228. package/dist/types/source-options.d.ts.map +1 -1
  229. package/dist/utils/generate-message.d.ts +6 -0
  230. package/dist/utils/generate-message.d.ts.map +1 -0
  231. package/dist/utils/generate-message.js +19 -0
  232. package/dist/utils/generate-message.js.map +1 -0
  233. package/dist/utils/generate-source.d.ts +7 -0
  234. package/dist/utils/generate-source.d.ts.map +1 -0
  235. package/dist/utils/generate-source.js +42 -0
  236. package/dist/utils/generate-source.js.map +1 -0
  237. package/dist/utils/index.d.ts +5 -0
  238. package/dist/utils/index.d.ts.map +1 -0
  239. package/dist/utils/index.js +21 -0
  240. package/dist/utils/index.js.map +1 -0
  241. package/dist/utils/write-message.d.ts +7 -0
  242. package/dist/utils/write-message.d.ts.map +1 -0
  243. package/dist/utils/write-message.js +29 -0
  244. package/dist/utils/write-message.js.map +1 -0
  245. package/dist/utils/write-source.d.ts +7 -0
  246. package/dist/utils/write-source.d.ts.map +1 -0
  247. package/dist/utils/write-source.js +47 -0
  248. package/dist/utils/write-source.js.map +1 -0
  249. package/package.json +21 -19
  250. package/dist/internal/types/iris-source-init.d.ts +0 -29
  251. package/dist/internal/types/iris-source-init.d.ts.map +0 -1
  252. package/dist/internal/types/iris-source-init.js.map +0 -1
package/README.md CHANGED
@@ -92,6 +92,73 @@ await source.drain();
92
92
  await source.disconnect();
93
93
  ```
94
94
 
95
+ ## Table of Contents
96
+
97
+ - [Messaging Patterns](#messaging-patterns)
98
+ - [Publisher (Fire-and-Forget)](#publisher-fire-and-forget)
99
+ - [Message Bus (Pub/Sub + Queues)](#message-bus-pubsub--queues)
100
+ - [Worker Queue (Competing Consumers)](#worker-queue-competing-consumers)
101
+ - [RPC (Request/Response)](#rpc-requestresponse)
102
+ - [Stream Processor (Pipelines)](#stream-processor-pipelines)
103
+ - [Field Types](#field-types)
104
+ - [Decorators](#decorators)
105
+ - [Class-Level Decorators](#class-level-decorators)
106
+ - [`@Message`](#message)
107
+ - [`@AbstractMessage`](#abstractmessage)
108
+ - [`@Namespace`](#namespace)
109
+ - [`@Version`](#version)
110
+ - [`@Topic`](#topic)
111
+ - [`@Broadcast`](#broadcast)
112
+ - [`@Persistent`](#persistent)
113
+ - [`@Priority`](#priority)
114
+ - [`@Delay`](#delay)
115
+ - [`@Expiry`](#expiry)
116
+ - [`@Encrypted`](#encrypted)
117
+ - [`@Compressed`](#compressed)
118
+ - [`@Retry`](#retry)
119
+ - [`@DeadLetter`](#deadletter)
120
+ - [Field Decorators](#field-decorators)
121
+ - [`@Field`](#field)
122
+ - [`@IdentifierField`](#identifierfield)
123
+ - [`@CorrelationField`](#correlationfield)
124
+ - [`@TimestampField`](#timestampfield)
125
+ - [`@MandatoryField`](#mandatoryfield)
126
+ - [`@PersistentField`](#persistentfield)
127
+ - [Field Modifiers](#field-modifiers)
128
+ - [`@Generated`](#generated)
129
+ - [`@Header`](#header)
130
+ - [`@Enum`](#enum)
131
+ - [`@Min` / `@Max`](#min--max)
132
+ - [`@Schema`](#schema)
133
+ - [`@Transform`](#transform)
134
+ - [Lifecycle Hook Decorators](#lifecycle-hook-decorators)
135
+ - [`@OnCreate`](#oncreate)
136
+ - [`@OnHydrate`](#onhydrate)
137
+ - [`@OnValidate`](#onvalidate)
138
+ - [`@BeforePublish` / `@AfterPublish`](#beforepublish--afterpublish)
139
+ - [`@BeforeConsume` / `@AfterConsume`](#beforeconsume--afterconsume)
140
+ - [`@OnConsumeError`](#onconsumeerror)
141
+ - [Retry and Dead Letter](#retry-and-dead-letter)
142
+ - [Dynamic Topics](#dynamic-topics)
143
+ - [Encryption and Compression](#encryption-and-compression)
144
+ - [Message Subscribers](#message-subscribers)
145
+ - [Hook Execution Order](#hook-execution-order)
146
+ - [Consume Envelope](#consume-envelope)
147
+ - [Message Manipulation](#message-manipulation)
148
+ - [Publish Options](#publish-options)
149
+ - [Zod Validation](#zod-validation)
150
+ - [Cloning](#cloning)
151
+ - [Driver Configuration](#driver-configuration)
152
+ - [Memory](#memory)
153
+ - [RabbitMQ](#rabbitmq)
154
+ - [Kafka](#kafka)
155
+ - [NATS](#nats)
156
+ - [Redis Streams](#redis-streams)
157
+ - [Persistence (Delay and Dead Letter Stores)](#persistence-delay-and-dead-letter-stores)
158
+ - [Connection State](#connection-state)
159
+ - [Testing with Mocks](#testing-with-mocks)
160
+ - [Error Classes](#error-classes)
161
+
95
162
  ## Messaging Patterns
96
163
 
97
164
  ### Publisher (Fire-and-Forget)
@@ -165,165 +232,814 @@ await queue.unconsumeAll();
165
232
 
166
233
  ### RPC (Request/Response)
167
234
 
168
- Synchronous request/response over the message broker.
235
+ Synchronous request/response over the message broker.
236
+
237
+ ```typescript
238
+ @Message()
239
+ class GetPrice {
240
+ @Field("string") sku!: string;
241
+ }
242
+
243
+ @Message()
244
+ class PriceResponse {
245
+ @Field("float") price!: number;
246
+ @Field("string") currency!: string;
247
+ }
248
+
249
+ const client = source.rpcClient(GetPrice, PriceResponse);
250
+ const server = source.rpcServer(GetPrice, PriceResponse);
251
+
252
+ // Server: register handler
253
+ await server.serve(async (req) => {
254
+ const res = new PriceResponse();
255
+ res.price = await lookupPrice(req.sku);
256
+ res.currency = "USD";
257
+ return res;
258
+ });
259
+
260
+ // Client: send request
261
+ const req = new GetPrice();
262
+ req.sku = "WIDGET-42";
263
+
264
+ const res = await client.request(req);
265
+ console.log(`${res.price} ${res.currency}`); // 29.99 USD
266
+
267
+ // With timeout
268
+ const res2 = await client.request(req, { timeout: 5000 });
269
+
270
+ // Clean up
271
+ await client.close();
272
+ await server.unserveAll();
273
+ ```
274
+
275
+ ### Stream Processor (Pipelines)
276
+
277
+ Declarative stream processing with an immutable builder pattern.
278
+
279
+ ```typescript
280
+ @Message()
281
+ class RawEvent {
282
+ @Field("string") type!: string;
283
+ @Field("float") value!: number;
284
+ }
285
+
286
+ @Message()
287
+ class AggregatedEvent {
288
+ @Field("float") sum!: number;
289
+ @Field("integer") count!: number;
290
+ }
291
+
292
+ const pipeline = source
293
+ .stream()
294
+ .from(RawEvent)
295
+ .filter((msg) => msg.value > 0)
296
+ .map((msg) => {
297
+ const out = new AggregatedEvent();
298
+ out.sum = msg.value;
299
+ out.count = 1;
300
+ return out;
301
+ })
302
+ .to(AggregatedEvent);
303
+
304
+ await pipeline.start();
305
+ // pipeline.isRunning() === true
306
+
307
+ await pipeline.pause();
308
+ await pipeline.resume();
309
+ await pipeline.stop();
310
+ ```
311
+
312
+ ## Field Types
313
+
314
+ The `@Field()` decorator accepts the following type identifiers:
315
+
316
+ | Category | Types |
317
+ | -------------- | -------------------------------- |
318
+ | Boolean | `boolean` |
319
+ | Integer | `integer`, `bigint` |
320
+ | Floating Point | `float` |
321
+ | String | `string`, `email`, `url`, `uuid` |
322
+ | Enum | `enum` |
323
+ | Date/Time | `date` |
324
+ | Structured | `object`, `array` |
325
+
326
+ ```typescript
327
+ @Message()
328
+ class FullExample {
329
+ @IdentifierField() id!: string;
330
+ @CorrelationField() correlationId!: string;
331
+ @TimestampField() createdAt!: Date;
332
+
333
+ @Field("string") name!: string;
334
+ @Field("integer") count!: number;
335
+ @Field("float") price!: number;
336
+ @Field("boolean") active!: boolean;
337
+ @Field("date") expiresAt!: Date;
338
+ @Field("uuid") referenceId!: string;
339
+ @Field("email") contactEmail!: string;
340
+ @Field("url") callbackUrl!: string;
341
+ @Field("array") tags!: Array<string>;
342
+ @Field("object") metadata!: Record<string, unknown>;
343
+
344
+ @Nullable() @Field("string") description!: string | null;
345
+ @Optional() @Field("string") nickname?: string;
346
+ @Default(0) @Field("integer") retryCount!: number;
347
+ @Default(() => "generated") @Field("string") code!: string;
348
+ }
349
+ ```
350
+
351
+ ## Decorators
352
+
353
+ All decorators use the TC39 (stage 3) decorator specification. Class decorators receive `ClassDecoratorContext`, field decorators receive `ClassFieldDecoratorContext`. Metadata flows through the `Symbol.metadata` prototype chain, so abstract base class decorators are inherited by concrete subclasses.
354
+
355
+ ### Class-Level Decorators
356
+
357
+ These decorators are applied to classes and configure message-wide behavior.
358
+
359
+ #### `@Message`
360
+
361
+ Marks a class as a concrete message type registered in the global message registry. Every message class must have exactly one of `@Message` or `@AbstractMessage`.
362
+
363
+ ```typescript
364
+ @Message()
365
+ class OrderPlaced {
366
+ /* ... */
367
+ }
368
+
369
+ @Message({ name: "order-placed" }) // custom message name
370
+ class OrderPlaced {
371
+ /* ... */
372
+ }
373
+ ```
374
+
375
+ **Options:** `{ name?: string }` — Override the message name. Defaults to the class name with any trailing version suffix (`_v1`, `_V2`) stripped. Must not conflict with other registered message names.
376
+
377
+ #### `@AbstractMessage`
378
+
379
+ Marks a class as an abstract message base. It is **not** registered in the global message registry. Fields, hooks, and metadata are inherited by `@Message()` subclasses via the `Symbol.metadata` prototype chain.
380
+
381
+ ```typescript
382
+ @AbstractMessage()
383
+ class BaseEvent {
384
+ @IdentifierField() id!: string;
385
+ @TimestampField() createdAt!: Date;
386
+ }
387
+
388
+ @Message()
389
+ @Namespace("orders")
390
+ class OrderPlaced extends BaseEvent {
391
+ @Field("string") orderId!: string;
392
+ }
393
+ ```
394
+
395
+ Cannot be combined with `@Message` on the same class.
396
+
397
+ #### `@Namespace`
398
+
399
+ Places the message in a named namespace for logical grouping and routing.
400
+
401
+ ```typescript
402
+ @Namespace("orders")
403
+ @Message()
404
+ class OrderPlaced {
405
+ /* ... */
406
+ }
407
+ ```
408
+
409
+ **Argument:** `string` — must be non-empty. Throws `IrisMetadataError` if empty or whitespace-only.
410
+
411
+ #### `@Version`
412
+
413
+ Sets the message schema version. Useful for evolving message formats while maintaining backward compatibility.
414
+
415
+ ```typescript
416
+ @Version(1)
417
+ @Message()
418
+ class OrderPlaced {
419
+ /* ... */
420
+ }
421
+ ```
422
+
423
+ **Argument:** `number` — must be a positive integer (>= 1). Throws `IrisMetadataError` otherwise.
424
+
425
+ #### `@Topic`
426
+
427
+ Provides a dynamic topic resolution callback. Instead of using the message class name as the topic, the callback computes the topic from the message content at publish time.
428
+
429
+ ```typescript
430
+ @Topic((msg: any) => `events.${msg.region}.${msg.type}`)
431
+ @Message()
432
+ class RegionalEvent {
433
+ @Field("string") region!: string;
434
+ @Field("string") type!: string;
435
+ }
436
+ ```
437
+
438
+ **Argument:** `(message: any) => string` — receives the message instance, returns the routing topic string.
439
+
440
+ #### `@Broadcast`
441
+
442
+ Marks a message for broadcast delivery. When published, the message is delivered to **all** subscribers rather than being distributed round-robin to one consumer per queue.
443
+
444
+ ```typescript
445
+ @Broadcast()
446
+ @Message()
447
+ class SystemNotification {
448
+ @Field("string") text!: string;
449
+ }
450
+ ```
451
+
452
+ No arguments.
453
+
454
+ #### `@Persistent`
455
+
456
+ Marks a message as persistent/durable. Persistent messages survive broker restarts (where supported by the driver).
457
+
458
+ ```typescript
459
+ @Persistent()
460
+ @Message()
461
+ class PaymentCharge {
462
+ @Field("string") chargeId!: string;
463
+ }
464
+ ```
465
+
466
+ No arguments.
467
+
468
+ #### `@Priority`
469
+
470
+ Sets the default priority for a message type. Higher priority messages are delivered before lower priority ones (where supported by the driver).
471
+
472
+ ```typescript
473
+ @Priority(8)
474
+ @Message()
475
+ class UrgentAlert {
476
+ @Field("string") text!: string;
477
+ }
478
+ ```
479
+
480
+ **Argument:** `number` — integer between 0 and 10 inclusive. Throws `IrisMetadataError` if out of range or not an integer.
481
+
482
+ #### `@Delay`
483
+
484
+ Sets a default delivery delay. The message is held for the specified duration before being delivered to consumers.
485
+
486
+ ```typescript
487
+ @Delay(5000) // 5 seconds
488
+ @Message()
489
+ class ScheduledReminder {
490
+ @Field("string") text!: string;
491
+ }
492
+ ```
493
+
494
+ **Argument:** `number` — non-negative integer in milliseconds. Throws `IrisMetadataError` if negative or not an integer.
495
+
496
+ Can be overridden per-publish via `PublishOptions.delay`.
497
+
498
+ #### `@Expiry`
499
+
500
+ Sets a default message TTL (time-to-live). Messages that are not consumed within this window are discarded.
501
+
502
+ ```typescript
503
+ @Expiry(60000) // 1 minute
504
+ @Message()
505
+ class TemporaryOffer {
506
+ @Field("float") discount!: number;
507
+ }
508
+ ```
509
+
510
+ **Argument:** `number` — non-negative integer in milliseconds. Throws `IrisMetadataError` if negative or not an integer.
511
+
512
+ Can be overridden per-publish via `PublishOptions.expiry`.
513
+
514
+ #### `@Encrypted`
515
+
516
+ Enables payload encryption via `@lindorm/amphora`. The entire message payload is encrypted before publishing and decrypted on consume. Requires an `IAmphora` instance configured on `IrisSource`.
517
+
518
+ ```typescript
519
+ @Encrypted() // encrypt with any available key
520
+ @Message()
521
+ class SensitivePayload {
522
+ @Field("string") ssn!: string;
523
+ }
524
+
525
+ @Encrypted({ purpose: "pii" }) // filter keys by purpose
526
+ @Message()
527
+ class MedicalRecord {
528
+ @Field("json") data!: Record<string, unknown>;
529
+ }
530
+ ```
531
+
532
+ **Argument:** `AmphoraPredicate` (optional, defaults to `{}`) — a predicate object to filter which encryption key to use from the key store. Supports fields like `algorithm`, `encryption`, `purpose`, `type`, `ownerId`, and standard predicate operators (`$eq`, `$in`, `$neq`, etc.).
533
+
534
+ #### `@Compressed`
535
+
536
+ Enables payload compression before publishing and decompression on consume.
537
+
538
+ ```typescript
539
+ @Compressed() // gzip (default)
540
+ @Compressed("brotli") // brotli compression
541
+ @Message()
542
+ class LargePayload {
543
+ @Field("object") data!: Record<string, unknown>;
544
+ }
545
+ ```
546
+
547
+ **Argument:** `"gzip" | "deflate" | "brotli"` (optional, defaults to `"gzip"`).
548
+
549
+ When combined with `@Encrypted`, compression is applied first, then encryption.
550
+
551
+ #### `@Retry`
552
+
553
+ Configures automatic retry behavior when a consume callback throws. Failed messages are retried with configurable backoff strategies.
554
+
555
+ ```typescript
556
+ @Retry() // defaults: 3 retries, constant 1s delay
557
+ @Message()
558
+ class ProcessOrder {
559
+ @Field("string") orderId!: string;
560
+ }
561
+
562
+ @Retry({
563
+ maxRetries: 5,
564
+ strategy: "exponential",
565
+ delay: 1000,
566
+ delayMax: 30000,
567
+ multiplier: 2,
568
+ jitter: true,
569
+ })
570
+ @Message()
571
+ class PaymentCharge {
572
+ @Field("string") chargeId!: string;
573
+ }
574
+ ```
575
+
576
+ **Options:**
577
+
578
+ | Field | Type | Default | Description |
579
+ | ------------ | --------------------------------------------- | ------------ | ----------------------------------------- |
580
+ | `maxRetries` | `number` | `3` | Maximum number of retry attempts |
581
+ | `strategy` | `"constant"` \| `"linear"` \| `"exponential"` | `"constant"` | Backoff strategy |
582
+ | `delay` | `number` | `1000` | Initial delay in milliseconds |
583
+ | `delayMax` | `number` | `30000` | Maximum delay cap in milliseconds |
584
+ | `multiplier` | `number` | `2` | Multiplier for exponential backoff |
585
+ | `jitter` | `boolean` | `false` | Add randomness to prevent thundering herd |
586
+
587
+ **Retry strategies:**
588
+
589
+ | Strategy | Delay pattern (base=1000, multiplier=2) |
590
+ | --------------- | ------------------------------------------- |
591
+ | `"constant"` | 1000, 1000, 1000, ... |
592
+ | `"linear"` | 1000, 2000, 3000, ... |
593
+ | `"exponential"` | 1000, 2000, 4000, 8000, ... (capped at max) |
594
+
595
+ #### `@DeadLetter`
596
+
597
+ Routes messages that have exhausted all retry attempts to the dead letter store. Requires `@Retry` and a dead letter store configured via `IrisSource.persistence.deadLetter`.
598
+
599
+ ```typescript
600
+ @Retry({ maxRetries: 3 })
601
+ @DeadLetter()
602
+ @Message()
603
+ class PaymentCharge {
604
+ @Field("string") chargeId!: string;
605
+ }
606
+ ```
607
+
608
+ No arguments.
609
+
610
+ ---
611
+
612
+ ### Field Decorators
613
+
614
+ These decorators are applied to class properties and declare message fields. Each field decorator creates a complete field definition with its type, default value, and nullability.
615
+
616
+ #### `@Field`
617
+
618
+ The foundational field decorator. Declares a message field with an explicit type.
619
+
620
+ ```typescript
621
+ @Field("string")
622
+ name!: string;
623
+
624
+ @Field("integer")
625
+ count!: number;
626
+
627
+ @Field("float")
628
+ price!: number;
629
+
630
+ @Field("boolean")
631
+ active!: boolean;
632
+
633
+ @Field("date")
634
+ expiresAt!: Date;
635
+
636
+ @Field("uuid")
637
+ referenceId!: string;
638
+
639
+ @Field("email")
640
+ contactEmail!: string;
641
+
642
+ @Field("url")
643
+ callbackUrl!: string;
644
+
645
+ @Field("array")
646
+ tags!: Array<string>;
647
+
648
+ @Field("object")
649
+ metadata!: Record<string, unknown>;
650
+ ```
651
+
652
+ **Arguments:** `(type: MetaFieldType)`.
653
+
654
+ Modifiers (nullable, optional, default value, transform) are expressed as stackable decorators applied above `@Field`. See the modifier sections below.
655
+
656
+ ```typescript
657
+ @Nullable()
658
+ @Field("string")
659
+ description!: string | null;
660
+
661
+ @Optional()
662
+ @Field("string")
663
+ nickname?: string;
664
+
665
+ @Default(0)
666
+ @Field("integer")
667
+ retryCount!: number;
668
+
669
+ @Default(() => "generated")
670
+ @Field("string")
671
+ code!: string;
672
+
673
+ @Transform({
674
+ to: (value: unknown) => Math.round((value as number) * 100),
675
+ from: (raw: unknown) => (raw as number) / 100,
676
+ })
677
+ @Field("float")
678
+ price!: number;
679
+ ```
680
+
681
+ #### `@IdentifierField`
682
+
683
+ Shorthand for a UUID primary identifier field. Auto-generates a UUID v4 on message creation.
684
+
685
+ ```typescript
686
+ @IdentifierField()
687
+ id!: string;
688
+ ```
689
+
690
+ Equivalent to `@Default(() => randomUUID()) @Field("uuid")`. Non-nullable, non-optional.
691
+
692
+ No arguments.
693
+
694
+ #### `@CorrelationField`
695
+
696
+ Shorthand for a UUID correlation tracking field. Auto-generates a UUID v4 on message creation. Used to trace related messages across publish/consume chains.
697
+
698
+ ```typescript
699
+ @CorrelationField()
700
+ correlationId!: string;
701
+ ```
702
+
703
+ Equivalent to `@Default(() => randomUUID()) @Field("uuid")`. Non-nullable, non-optional.
704
+
705
+ No arguments.
706
+
707
+ #### `@TimestampField`
708
+
709
+ Shorthand for a timestamp field. Auto-generates the current `Date` on message creation.
710
+
711
+ ```typescript
712
+ @TimestampField()
713
+ createdAt!: Date;
714
+ ```
715
+
716
+ Equivalent to `@Default(() => new Date()) @Field("date")`. Non-nullable, non-optional.
717
+
718
+ No arguments.
719
+
720
+ #### `@MandatoryField`
721
+
722
+ Shorthand for a boolean flag that defaults to `false`. Commonly used for acknowledgement or processing flags.
723
+
724
+ ```typescript
725
+ @MandatoryField()
726
+ requiresApproval!: boolean;
727
+ ```
728
+
729
+ Equivalent to `@Default(false) @Field("boolean")`. Non-nullable, non-optional.
730
+
731
+ No arguments.
732
+
733
+ #### `@PersistentField`
734
+
735
+ Shorthand for a boolean persistence flag that defaults to `false`. Commonly used to mark whether a message should be durably stored.
736
+
737
+ ```typescript
738
+ @PersistentField()
739
+ shouldPersist!: boolean;
740
+ ```
741
+
742
+ Equivalent to `@Default(false) @Field("boolean")`. Non-nullable, non-optional.
743
+
744
+ No arguments.
745
+
746
+ ---
747
+
748
+ ### Field Modifiers
749
+
750
+ These decorators modify the behavior of a field declared with `@Field` or one of the shorthand field decorators. Stack them on the same property. Modifier decorators must appear alongside a field decorator on the same property.
751
+
752
+ #### `@Generated`
753
+
754
+ Marks a field for automatic value generation on message creation.
755
+
756
+ ```typescript
757
+ @Generated("uuid") // UUID v4
758
+ @Field("uuid")
759
+ traceId!: string;
760
+
761
+ @Generated("date") // current timestamp
762
+ @Field("date")
763
+ processedAt!: Date;
764
+
765
+ @Generated("string") // random string (default length)
766
+ @Field("string")
767
+ token!: string;
768
+
769
+ @Generated("string", { length: 12 }) // random string with custom length
770
+ @Field("string")
771
+ shortCode!: string;
772
+
773
+ @Generated("integer", { min: 1, max: 1000 })
774
+ @Field("integer")
775
+ sequenceNumber!: number;
776
+
777
+ @Generated("float", { min: 0.0, max: 1.0 })
778
+ @Field("float")
779
+ weight!: number;
780
+ ```
781
+
782
+ **Arguments:** `(strategy: MetaGeneratedStrategy, options?: GeneratedDecoratorOptions)`.
783
+
784
+ **Strategies:**
785
+
786
+ | Strategy | Description |
787
+ | ----------- | ---------------------------------------- |
788
+ | `"uuid"` | Generate UUID v4 |
789
+ | `"date"` | Current timestamp |
790
+ | `"string"` | Random string with configurable `length` |
791
+ | `"integer"` | Random integer in `[min, max]` range |
792
+ | `"float"` | Random float in `[min, max]` range |
793
+
794
+ **Options:** `{ length?: number, min?: number, max?: number }` — all optional, all default to `null`.
795
+
796
+ #### `@Header`
797
+
798
+ Promotes a field value to a message header. Headers are transported as key-value metadata alongside the payload, accessible without deserialising the full message body.
799
+
800
+ ```typescript
801
+ @Header() // header name = property name ("source")
802
+ @Field("string")
803
+ source!: string;
804
+
805
+ @Header("x-trace-id") // explicit header name
806
+ @Field("uuid")
807
+ traceId!: string;
808
+ ```
809
+
810
+ **Argument:** `string?` — custom header name. Defaults to the property name. Throws `IrisMetadataError` if the resolved name is empty or whitespace-only.
811
+
812
+ #### `@Enum`
813
+
814
+ Restricts a field to a fixed set of allowed values. Pass a TypeScript enum or a plain `Record<string, string | number>`. Enforced during Zod validation.
815
+
816
+ ```typescript
817
+ enum OrderStatus {
818
+ Pending = "pending",
819
+ Shipped = "shipped",
820
+ Delivered = "delivered",
821
+ }
822
+
823
+ @Enum(OrderStatus)
824
+ @Field("enum")
825
+ status!: OrderStatus;
826
+ ```
827
+
828
+ **Argument:** `Record<string, string | number>` — the enum object or value map.
829
+
830
+ #### `@Min` / `@Max`
831
+
832
+ Set minimum/maximum bounds for numeric fields or minimum/maximum length for string fields. Enforced during Zod validation.
833
+
834
+ ```typescript
835
+ @Min(0)
836
+ @Max(100)
837
+ @Field("integer")
838
+ score!: number;
839
+
840
+ @Min(1)
841
+ @Max(255)
842
+ @Field("string")
843
+ name!: string;
844
+ ```
845
+
846
+ **Argument:** `number`.
847
+
848
+ #### `@Schema`
849
+
850
+ Attaches a Zod schema for fine-grained field validation. The schema is evaluated during message validation.
851
+
852
+ ```typescript
853
+ import { z } from "zod";
854
+
855
+ @Schema(z.string().email())
856
+ @Field("email")
857
+ email!: string;
858
+
859
+ @Schema(z.number().int().min(13).max(150))
860
+ @Field("integer")
861
+ age!: number;
862
+
863
+ @Schema(z.string().regex(/^[A-Z]{2,3}$/))
864
+ @Field("string")
865
+ countryCode!: string;
866
+ ```
867
+
868
+ **Argument:** `z.ZodType` — any Zod schema.
869
+
870
+ #### `@Transform`
871
+
872
+ Applies a bidirectional transform to a field value. `to` runs during serialisation (message -> transport), `from` runs during deserialisation (transport -> message).
873
+
874
+ ```typescript
875
+ @Transform({
876
+ to: (value: string[]) => value.join(","),
877
+ from: (raw: string) => raw.split(","),
878
+ })
879
+ @Field("string")
880
+ tags!: string[];
881
+
882
+ @Transform<Date, number>({
883
+ to: (date) => date.getTime(),
884
+ from: (ms) => new Date(ms),
885
+ })
886
+ @Field("bigint")
887
+ timestamp!: Date;
888
+ ```
889
+
890
+ **Options:** `{ to: (value: TFrom) => TTo, from: (raw: TTo) => TFrom }`.
891
+
892
+ This is a standalone decorator that uses a separate staging path. For inline transforms, use the `transform` option on `@Field` instead.
893
+
894
+ ---
895
+
896
+ ### Lifecycle Hook Decorators
897
+
898
+ Lifecycle hooks are **class decorators** that register callbacks at specific points in the message lifecycle. All hooks receive `(message, context?)` as arguments, where `context` is the source's context value.
899
+
900
+ Hooks may be async (`void | Promise<void>`) unless otherwise noted.
901
+
902
+ #### `@OnCreate`
903
+
904
+ Fires when a message instance is created via `create()`. Useful for setting computed defaults or derived fields.
905
+
906
+ ```typescript
907
+ @OnCreate((msg) => {
908
+ msg.slug = msg.name.toLowerCase().replace(/\s+/g, "-");
909
+ })
910
+ @Message()
911
+ class OrderPlaced {
912
+ @Field("string") name!: string;
913
+ @Field("string") slug!: string;
914
+ }
915
+ ```
916
+
917
+ **Argument:** `(message: M, context?: C) => void | Promise<void>`.
918
+
919
+ #### `@OnHydrate`
920
+
921
+ Fires when a message is rehydrated from raw transport data, after all fields are populated but before the message is returned to the consume callback.
169
922
 
170
923
  ```typescript
924
+ @OnHydrate((msg) => {
925
+ msg.displayName = `${msg.firstName} ${msg.lastName}`;
926
+ })
171
927
  @Message()
172
- class GetPrice {
173
- @Field("string") sku!: string;
928
+ class UserEvent {
929
+ @Field("string") firstName!: string;
930
+ @Field("string") lastName!: string;
931
+ @Field("string") displayName!: string;
174
932
  }
933
+ ```
175
934
 
176
- @Message()
177
- class PriceResponse {
178
- @Field("float") price!: number;
179
- @Field("string") currency!: string;
180
- }
935
+ **Argument:** `(message: M, context?: C) => void | Promise<void>`.
181
936
 
182
- const client = source.rpcClient(GetPrice, PriceResponse);
183
- const server = source.rpcServer(GetPrice, PriceResponse);
937
+ #### `@OnValidate`
184
938
 
185
- // Server: register handler
186
- await server.serve(async (req) => {
187
- const res = new PriceResponse();
188
- res.price = await lookupPrice(req.sku);
189
- res.currency = "USD";
190
- return res;
191
- });
939
+ Fires during message validation, after the built-in Zod schema check. Throw to reject the message.
192
940
 
193
- // Client: send request
194
- const req = new GetPrice();
195
- req.sku = "WIDGET-42";
941
+ ```typescript
942
+ @OnValidate((msg) => {
943
+ if (msg.startDate >= msg.endDate) {
944
+ throw new Error("startDate must be before endDate");
945
+ }
946
+ })
947
+ @Message()
948
+ class BookingRequest {
949
+ @Field("date") startDate!: Date;
950
+ @Field("date") endDate!: Date;
951
+ }
952
+ ```
196
953
 
197
- const res = await client.request(req);
198
- console.log(`${res.price} ${res.currency}`); // 29.99 USD
954
+ **Argument:** `(message: M, context?: C) => void | Promise<void>`.
199
955
 
200
- // With timeout
201
- const res2 = await client.request(req, { timeout: 5000 });
956
+ #### `@BeforePublish` / `@AfterPublish`
202
957
 
203
- // Clean up
204
- await client.close();
205
- await server.unserveAll();
958
+ Fire around publish operations. `@BeforePublish` runs before the message is handed to the transport. `@AfterPublish` runs after the transport confirms delivery.
959
+
960
+ ```typescript
961
+ @BeforePublish(async (msg) => {
962
+ await validateExternalId(msg.externalId);
963
+ })
964
+ @AfterPublish(async (msg) => {
965
+ metrics.increment("messages.published");
966
+ })
967
+ @Message()
968
+ class OrderPlaced {
969
+ @Field("string") externalId!: string;
970
+ }
206
971
  ```
207
972
 
208
- ### Stream Processor (Pipelines)
973
+ **Argument:** `(message: M, context?: C) => void | Promise<void>`.
209
974
 
210
- Declarative stream processing with an immutable builder pattern.
975
+ #### `@BeforeConsume` / `@AfterConsume`
976
+
977
+ Fire around consume callback execution. `@BeforeConsume` runs after deserialisation but before the consume callback. `@AfterConsume` runs after the callback completes successfully.
211
978
 
212
979
  ```typescript
980
+ @BeforeConsume(async (msg, ctx) => {
981
+ audit.log("consuming", msg);
982
+ })
983
+ @AfterConsume(async (msg) => {
984
+ metrics.increment("messages.consumed");
985
+ })
213
986
  @Message()
214
- class RawEvent {
215
- @Field("string") type!: string;
216
- @Field("float") value!: number;
987
+ class OrderPlaced {
988
+ /* ... */
217
989
  }
990
+ ```
218
991
 
219
- @Message()
220
- class AggregatedEvent {
221
- @Field("float") sum!: number;
222
- @Field("integer") count!: number;
223
- }
992
+ **Argument:** `(message: M, context?: C) => void | Promise<void>`.
224
993
 
225
- const pipeline = source
226
- .stream()
227
- .from(RawEvent)
228
- .filter((msg) => msg.value > 0)
229
- .map((msg) => {
230
- const out = new AggregatedEvent();
231
- out.sum = msg.value;
232
- out.count = 1;
233
- return out;
234
- })
235
- .to(AggregatedEvent);
994
+ #### `@OnConsumeError`
236
995
 
237
- await pipeline.start();
238
- // pipeline.isRunning() === true
996
+ Fires when a consume callback throws an error. Receives the error as the **first** argument, followed by the message and context.
239
997
 
240
- await pipeline.pause();
241
- await pipeline.resume();
242
- await pipeline.stop();
998
+ ```typescript
999
+ @OnConsumeError(async (error, msg) => {
1000
+ errorTracker.capture(error, { messageId: msg.id });
1001
+ })
1002
+ @Message()
1003
+ class PaymentCharge {
1004
+ @IdentifierField() id!: string;
1005
+ @Field("string") chargeId!: string;
1006
+ }
243
1007
  ```
244
1008
 
245
- ## Message Decorators
246
-
247
- ### Class-Level
248
-
249
- | Decorator | Description |
250
- | -------------------- | ------------------------------------------------------ |
251
- | `@Message(opts?)` | Mark class as a message type |
252
- | `@AbstractMessage()` | Mark as abstract (non-concrete base) |
253
- | `@Namespace(ns)` | Set message namespace |
254
- | `@Version(n)` | Set message version (positive integer) |
255
- | `@Topic(fn)` | Dynamic topic resolution callback |
256
- | `@Broadcast()` | Deliver to all subscribers (not just one per queue) |
257
- | `@Persistent()` | Mark message as persistent/durable |
258
- | `@Priority(n)` | Set priority (integer 0-10) |
259
- | `@Delay(ms)` | Default delivery delay in milliseconds |
260
- | `@Expiry(ms)` | Message expiration in milliseconds |
261
- | `@Encrypted(pred?)` | Enable payload encryption via `@lindorm/amphora` |
262
- | `@Compressed(alg?)` | Enable compression (`"gzip"`, `"deflate"`, `"brotli"`) |
263
- | `@Retry(opts?)` | Configure retry behaviour on consume failure |
264
- | `@DeadLetter()` | Route failed messages to dead letter store |
265
-
266
- ### Field-Level
267
-
268
- | Decorator | Description |
269
- | ---------------------- | ---------------------------------------------------------------------------- |
270
- | `@Field(type, opts?)` | Declare field with type and options |
271
- | `@IdentifierField()` | Auto-generated UUID field |
272
- | `@CorrelationField()` | Auto-generated UUID for correlation tracking |
273
- | `@TimestampField()` | Auto-generated Date field |
274
- | `@MandatoryField()` | Boolean field, defaults to `false` |
275
- | `@PersistentField()` | Boolean persistence flag, defaults to `false` |
276
- | `@Generated(strategy)` | Auto-generate value (`"uuid"`, `"date"`, `"string"`, `"integer"`, `"float"`) |
277
- | `@Header(name?)` | Promote field to message header |
278
- | `@Enum(values)` | Restrict to enum values |
279
- | `@Min(n)` | Minimum value constraint |
280
- | `@Max(n)` | Maximum value constraint |
281
- | `@Schema(zodType)` | Zod schema validation |
282
- | `@Transform(opts)` | Custom serialisation/deserialisation transform |
283
-
284
- ### Lifecycle Hooks
285
-
286
- | Decorator | Description |
287
- | --------------------- | ----------------------------------------------- |
288
- | `@OnCreate(fn)` | Called when message instance is created |
289
- | `@OnHydrate(fn)` | Called when message is rehydrated from raw data |
290
- | `@OnValidate(fn)` | Called when message is validated |
291
- | `@BeforePublish(fn)` | Called before publishing |
292
- | `@AfterPublish(fn)` | Called after publishing |
293
- | `@BeforeConsume(fn)` | Called before consume callback |
294
- | `@AfterConsume(fn)` | Called after successful consume |
295
- | `@OnConsumeError(fn)` | Called when consume callback throws |
1009
+ **Argument:** `(error: Error, message: M, context?: C) => void | Promise<void>`.
296
1010
 
297
- ## Field Types
1011
+ Note the different signature: `error` is the first parameter, unlike all other hooks where the message comes first.
298
1012
 
299
- The `@Field()` decorator accepts the following type identifiers:
1013
+ ---
300
1014
 
301
- `"array"` | `"bigint"` | `"boolean"` | `"date"` | `"email"` | `"enum"` | `"float"` | `"integer"` | `"object"` | `"string"` | `"url"` | `"uuid"`
1015
+ ### Hook Execution Order
302
1016
 
303
- ```typescript
304
- @Message()
305
- class FullExample {
306
- @IdentifierField() id!: string;
307
- @CorrelationField() correlationId!: string;
308
- @TimestampField() createdAt!: Date;
1017
+ | Phase | Hooks (in order) |
1018
+ | ---------- | ------------------------------------------------------------------------------------------------------- |
1019
+ | Creation | `@OnCreate` |
1020
+ | Validation | `@OnValidate` |
1021
+ | Publish | `@BeforePublish` -> subscriber.beforePublish -> transport -> `@AfterPublish` -> subscriber.afterPublish |
1022
+ | Consume | `@BeforeConsume` -> subscriber.beforeConsume -> callback -> `@AfterConsume` -> subscriber.afterConsume |
1023
+ | Hydration | `@OnHydrate` |
1024
+ | Error | `@OnConsumeError` -> subscriber.onConsumeError (replaces AfterConsume steps) |
309
1025
 
310
- @Field("string") name!: string;
311
- @Field("integer") count!: number;
312
- @Field("float") price!: number;
313
- @Field("boolean") active!: boolean;
314
- @Field("date") expiresAt!: Date;
315
- @Field("uuid") referenceId!: string;
316
- @Field("email") contactEmail!: string;
317
- @Field("url") callbackUrl!: string;
318
- @Field("array") tags!: Array<string>;
319
- @Field("object") metadata!: Record<string, unknown>;
1026
+ Full publish + consume lifecycle:
320
1027
 
321
- @Field("string", { nullable: true }) description!: string | null;
322
- @Field("string", { optional: true }) nickname?: string;
323
- @Field("integer", { default: 0 }) retryCount!: number;
324
- @Field("string", { default: () => "generated" }) code!: string;
325
- }
326
1028
  ```
1029
+ 1. @BeforePublish hook
1030
+ 2. subscriber.beforePublish
1031
+ 3. (transport publishes)
1032
+ 4. (transport delivers to consumer)
1033
+ 5. @BeforeConsume hook
1034
+ 6. subscriber.beforeConsume
1035
+ 7. callback executes
1036
+ 8. @AfterConsume hook
1037
+ 9. subscriber.afterConsume
1038
+ 10. @AfterPublish hook
1039
+ 11. subscriber.afterPublish
1040
+ ```
1041
+
1042
+ On error at step 7, steps 8-11 are replaced by `@OnConsumeError` and `subscriber.onConsumeError`.
327
1043
 
328
1044
  ## Retry and Dead Letter
329
1045
 
@@ -346,14 +1062,6 @@ class PaymentCharge {
346
1062
  }
347
1063
  ```
348
1064
 
349
- **Retry strategies:**
350
-
351
- | Strategy | Delay pattern (base=1000, multiplier=2) |
352
- | --------------- | ------------------------------------------- |
353
- | `"constant"` | 1000, 1000, 1000, ... |
354
- | `"linear"` | 1000, 2000, 3000, ... |
355
- | `"exponential"` | 1000, 2000, 4000, 8000, ... (capped at max) |
356
-
357
1065
  ## Dynamic Topics
358
1066
 
359
1067
  Route messages to different topics based on their content:
@@ -420,24 +1128,6 @@ source.addSubscriber(auditSubscriber);
420
1128
  source.removeSubscriber(auditSubscriber);
421
1129
  ```
422
1130
 
423
- **Hook execution order on publish + consume:**
424
-
425
- ```
426
- 1. @BeforePublish hook
427
- 2. subscriber.beforePublish
428
- 3. (transport publishes)
429
- 4. (transport delivers to consumer)
430
- 5. @BeforeConsume hook
431
- 6. subscriber.beforeConsume
432
- 7. callback executes
433
- 8. @AfterConsume hook
434
- 9. subscriber.afterConsume
435
- 10. @AfterPublish hook
436
- 11. subscriber.afterPublish
437
- ```
438
-
439
- On error at step 7, steps 8-11 are replaced by `@OnConsumeError` and `subscriber.onConsumeError`.
440
-
441
1131
  ## Consume Envelope
442
1132
 
443
1133
  Every subscribe/consume callback receives the message and an envelope with routing metadata:
@@ -460,6 +1150,65 @@ await bus.subscribe({
460
1150
  });
461
1151
  ```
462
1152
 
1153
+ ## Message Manipulation
1154
+
1155
+ Every publisher, message bus, and worker queue provides utilities for working with message instances:
1156
+
1157
+ ```typescript
1158
+ const bus = source.messageBus(OrderPlaced);
1159
+
1160
+ // Create: new instance with auto-generated fields and defaults
1161
+ const msg = bus.create({ orderId: "abc-123", total: 59.99 });
1162
+
1163
+ // Hydrate: reconstruct from raw data (no auto-generation)
1164
+ const hydrated = bus.hydrate({ orderId: "abc-123", total: 59.99, id: "existing-uuid" });
1165
+
1166
+ // Copy: deep clone with a fresh identifier
1167
+ const copied = bus.copy(msg);
1168
+ // copied.orderId === msg.orderId, but copied.id !== msg.id
1169
+
1170
+ // Validate: throws IrisValidationError if invalid
1171
+ bus.validate(msg);
1172
+ ```
1173
+
1174
+ ## Publish Options
1175
+
1176
+ Override message-level defaults per publish call:
1177
+
1178
+ ```typescript
1179
+ await bus.publish(msg, {
1180
+ delay: 5000, // delay delivery by 5 seconds
1181
+ priority: 8, // override @Priority
1182
+ expiry: 60000, // override @Expiry (TTL in ms)
1183
+ key: "partition-key", // routing/partition key
1184
+ headers: { "x-source": "api" }, // additional headers
1185
+ });
1186
+ ```
1187
+
1188
+ ## Zod Validation
1189
+
1190
+ Use `@Schema()` with Zod for fine-grained field validation:
1191
+
1192
+ ```typescript
1193
+ import { z } from "zod";
1194
+ import { Schema, Field, Message } from "@lindorm/iris";
1195
+
1196
+ @Message()
1197
+ class UserCreated {
1198
+ @Schema(z.string().email())
1199
+ @Field("email")
1200
+ email!: string;
1201
+
1202
+ @Schema(z.number().int().min(13).max(150))
1203
+ @Field("integer")
1204
+ age!: number;
1205
+
1206
+ @Schema(z.string().regex(/^[A-Z]{2,3}$/))
1207
+ @Field("string")
1208
+ countryCode!: string;
1209
+ }
1210
+ ```
1211
+
463
1212
  ## Cloning
464
1213
 
465
1214
  Create independent source instances that share the underlying driver connection:
@@ -697,65 +1446,6 @@ source.onConnectionStateChange((state) => {
697
1446
  const healthy = await source.ping();
698
1447
  ```
699
1448
 
700
- ## Message Manipulation
701
-
702
- Every publisher, message bus, and worker queue provides utilities for working with message instances:
703
-
704
- ```typescript
705
- const bus = source.messageBus(OrderPlaced);
706
-
707
- // Create: new instance with auto-generated fields and defaults
708
- const msg = bus.create({ orderId: "abc-123", total: 59.99 });
709
-
710
- // Hydrate: reconstruct from raw data (no auto-generation)
711
- const hydrated = bus.hydrate({ orderId: "abc-123", total: 59.99, id: "existing-uuid" });
712
-
713
- // Copy: deep clone with a fresh identifier
714
- const copied = bus.copy(msg);
715
- // copied.orderId === msg.orderId, but copied.id !== msg.id
716
-
717
- // Validate: throws IrisValidationError if invalid
718
- bus.validate(msg);
719
- ```
720
-
721
- ## Publish Options
722
-
723
- Override message-level defaults per publish call:
724
-
725
- ```typescript
726
- await bus.publish(msg, {
727
- delay: 5000, // delay delivery by 5 seconds
728
- priority: 8, // override @Priority
729
- expiry: 60000, // override @Expiry (TTL in ms)
730
- key: "partition-key", // routing/partition key
731
- headers: { "x-source": "api" }, // additional headers
732
- });
733
- ```
734
-
735
- ## Zod Validation
736
-
737
- Use `@Schema()` with Zod for fine-grained field validation:
738
-
739
- ```typescript
740
- import { z } from "zod";
741
- import { Schema, Field, Message } from "@lindorm/iris";
742
-
743
- @Message()
744
- class UserCreated {
745
- @Schema(z.string().email())
746
- @Field("email")
747
- email!: string;
748
-
749
- @Schema(z.number().int().min(13).max(150))
750
- @Field("integer")
751
- age!: number;
752
-
753
- @Schema(z.string().regex(/^[A-Z]{2,3}$/))
754
- @Field("string")
755
- countryCode!: string;
756
- }
757
- ```
758
-
759
1449
  ## Testing with Mocks
760
1450
 
761
1451
  All mocks are available via the `@lindorm/iris/mocks` subpath: