@lindorm/iris 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1683 -0
- package/dist/classes/IrisSession.d.ts +28 -0
- package/dist/classes/IrisSession.d.ts.map +1 -0
- package/dist/classes/IrisSession.js +42 -0
- package/dist/classes/IrisSession.js.map +1 -0
- package/dist/classes/IrisSource.d.ts +6 -5
- package/dist/classes/IrisSource.d.ts.map +1 -1
- package/dist/classes/IrisSource.js +17 -33
- package/dist/classes/IrisSource.js.map +1 -1
- package/dist/classes/index.d.ts +1 -0
- package/dist/classes/index.d.ts.map +1 -1
- package/dist/classes/index.js +1 -0
- package/dist/classes/index.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +35 -0
- package/dist/cli.js.map +1 -0
- package/dist/interfaces/IrisDriver.d.ts +4 -2
- package/dist/interfaces/IrisDriver.d.ts.map +1 -1
- package/dist/interfaces/IrisMessagingProvider.d.ts +21 -0
- package/dist/interfaces/IrisMessagingProvider.d.ts.map +1 -0
- package/dist/interfaces/IrisMessagingProvider.js +3 -0
- package/dist/interfaces/IrisMessagingProvider.js.map +1 -0
- package/dist/interfaces/IrisSession.d.ts +4 -0
- package/dist/interfaces/IrisSession.d.ts.map +1 -0
- package/dist/{internal/types/iris-source-init.js → interfaces/IrisSession.js} +1 -1
- package/dist/interfaces/IrisSession.js.map +1 -0
- package/dist/interfaces/IrisSource.d.ts +8 -19
- package/dist/interfaces/IrisSource.d.ts.map +1 -1
- package/dist/interfaces/index.d.ts +2 -0
- package/dist/interfaces/index.d.ts.map +1 -1
- package/dist/interfaces/index.js +2 -0
- package/dist/interfaces/index.js.map +1 -1
- package/dist/internal/cli/commands/generate-message.d.ts +7 -0
- package/dist/internal/cli/commands/generate-message.d.ts.map +1 -0
- package/dist/internal/cli/commands/generate-message.js +77 -0
- package/dist/internal/cli/commands/generate-message.js.map +1 -0
- package/dist/internal/cli/commands/init.d.ts +8 -0
- package/dist/internal/cli/commands/init.d.ts.map +1 -0
- package/dist/internal/cli/commands/init.js +112 -0
- package/dist/internal/cli/commands/init.js.map +1 -0
- package/dist/internal/cli/commands/register-generate.d.ts +3 -0
- package/dist/internal/cli/commands/register-generate.d.ts.map +1 -0
- package/dist/internal/cli/commands/register-generate.js +20 -0
- package/dist/internal/cli/commands/register-generate.js.map +1 -0
- package/dist/internal/cli/commands/register-init.d.ts +3 -0
- package/dist/internal/cli/commands/register-init.d.ts.map +1 -0
- package/dist/internal/cli/commands/register-init.js +16 -0
- package/dist/internal/cli/commands/register-init.js.map +1 -0
- package/dist/internal/drivers/kafka/classes/KafkaDriver.d.ts +5 -3
- package/dist/internal/drivers/kafka/classes/KafkaDriver.d.ts.map +1 -1
- package/dist/internal/drivers/kafka/classes/KafkaDriver.js +12 -16
- package/dist/internal/drivers/kafka/classes/KafkaDriver.js.map +1 -1
- package/dist/internal/drivers/kafka/classes/KafkaMessageBus.d.ts.map +1 -1
- package/dist/internal/drivers/kafka/classes/KafkaMessageBus.js.map +1 -1
- package/dist/internal/drivers/kafka/classes/KafkaRpcServer.js.map +1 -1
- package/dist/internal/drivers/kafka/classes/KafkaWorkerQueue.d.ts.map +1 -1
- package/dist/internal/drivers/kafka/classes/KafkaWorkerQueue.js.map +1 -1
- package/dist/internal/drivers/kafka/utils/create-kafka-consumer.js +1 -1
- package/dist/internal/drivers/kafka/utils/create-kafka-consumer.js.map +1 -1
- package/dist/internal/drivers/kafka/utils/stop-kafka-consumer.d.ts +3 -1
- package/dist/internal/drivers/kafka/utils/stop-kafka-consumer.d.ts.map +1 -1
- package/dist/internal/drivers/kafka/utils/stop-kafka-consumer.js +24 -5
- package/dist/internal/drivers/kafka/utils/stop-kafka-consumer.js.map +1 -1
- package/dist/internal/drivers/memory/classes/MemoryDriver.d.ts +5 -3
- package/dist/internal/drivers/memory/classes/MemoryDriver.d.ts.map +1 -1
- package/dist/internal/drivers/memory/classes/MemoryDriver.js +11 -14
- package/dist/internal/drivers/memory/classes/MemoryDriver.js.map +1 -1
- package/dist/internal/drivers/nats/classes/NatsDriver.d.ts +5 -3
- package/dist/internal/drivers/nats/classes/NatsDriver.d.ts.map +1 -1
- package/dist/internal/drivers/nats/classes/NatsDriver.js +12 -15
- package/dist/internal/drivers/nats/classes/NatsDriver.js.map +1 -1
- package/dist/internal/drivers/rabbit/classes/RabbitDriver.d.ts +5 -3
- package/dist/internal/drivers/rabbit/classes/RabbitDriver.d.ts.map +1 -1
- package/dist/internal/drivers/rabbit/classes/RabbitDriver.js +11 -14
- package/dist/internal/drivers/rabbit/classes/RabbitDriver.js.map +1 -1
- package/dist/internal/drivers/redis/classes/RedisDriver.d.ts +5 -3
- package/dist/internal/drivers/redis/classes/RedisDriver.d.ts.map +1 -1
- package/dist/internal/drivers/redis/classes/RedisDriver.js +11 -14
- package/dist/internal/drivers/redis/classes/RedisDriver.js.map +1 -1
- package/dist/internal/drivers/redis/classes/RedisRpcServer.js.map +1 -1
- package/dist/internal/drivers/redis/utils/create-consumer-loop.js +1 -1
- package/dist/internal/drivers/redis/utils/create-consumer-loop.js.map +1 -1
- package/dist/internal/drivers/redis/utils/stop-consumer-loop.js +2 -2
- package/dist/internal/drivers/redis/utils/stop-consumer-loop.js.map +1 -1
- package/dist/internal/message/utils/encrypt.d.ts +1 -1
- package/dist/internal/message/utils/encrypt.d.ts.map +1 -1
- package/dist/internal/message/utils/encrypt.js +4 -3
- package/dist/internal/message/utils/encrypt.js.map +1 -1
- package/dist/internal/message/utils/prepare-inbound.js +1 -1
- package/dist/internal/message/utils/prepare-inbound.js.map +1 -1
- package/dist/internal/types/index.d.ts +0 -1
- package/dist/internal/types/index.d.ts.map +1 -1
- package/dist/internal/types/index.js +0 -1
- package/dist/internal/types/index.js.map +1 -1
- package/dist/mocks/create-mock-iris-session.d.ts +4 -0
- package/dist/mocks/create-mock-iris-session.d.ts.map +1 -0
- package/dist/mocks/create-mock-iris-session.js +20 -0
- package/dist/mocks/create-mock-iris-session.js.map +1 -0
- package/dist/mocks/create-mock-iris-source.d.ts +2 -1
- package/dist/mocks/create-mock-iris-source.d.ts.map +1 -1
- package/dist/mocks/create-mock-iris-source.js +5 -2
- package/dist/mocks/create-mock-iris-source.js.map +1 -1
- package/dist/mocks/index.d.ts +2 -1
- package/dist/mocks/index.d.ts.map +1 -1
- package/dist/mocks/index.js +5 -1
- package/dist/mocks/index.js.map +1 -1
- package/dist/types/events.d.ts +5 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +3 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/source-options.d.ts +1 -1
- package/dist/types/source-options.d.ts.map +1 -1
- package/package.json +28 -16
- package/dist/internal/types/iris-source-init.d.ts +0 -29
- package/dist/internal/types/iris-source-init.d.ts.map +0 -1
- package/dist/internal/types/iris-source-init.js.map +0 -1
package/README.md
CHANGED
|
@@ -1 +1,1684 @@
|
|
|
1
1
|
# @lindorm/iris
|
|
2
|
+
|
|
3
|
+
Unified messaging library for Node.js with a single decorator-driven API across multiple brokers. Define messages once, deploy to any backend.
|
|
4
|
+
|
|
5
|
+
## Supported Drivers
|
|
6
|
+
|
|
7
|
+
| Driver | Peer Dependency | Use Case |
|
|
8
|
+
| ------------ | --------------- | ----------------------------------- |
|
|
9
|
+
| **Memory** | _(none)_ | Testing, prototyping |
|
|
10
|
+
| **RabbitMQ** | `amqplib` | Task queues, complex routing |
|
|
11
|
+
| **Kafka** | `kafkajs` | High-throughput event streaming |
|
|
12
|
+
| **NATS** | `nats` | Low-latency, cloud-native systems |
|
|
13
|
+
| **Redis** | `ioredis` | Lightweight streams, existing infra |
|
|
14
|
+
|
|
15
|
+
Install only the peer dependency for the driver(s) you use:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @lindorm/iris
|
|
19
|
+
|
|
20
|
+
# Pick one or more:
|
|
21
|
+
npm install amqplib # RabbitMQ
|
|
22
|
+
npm install kafkajs # Kafka
|
|
23
|
+
npm install nats # NATS
|
|
24
|
+
npm install ioredis # Redis Streams
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### 1. Define a Message
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import {
|
|
33
|
+
Message,
|
|
34
|
+
Namespace,
|
|
35
|
+
Version,
|
|
36
|
+
Field,
|
|
37
|
+
IdentifierField,
|
|
38
|
+
TimestampField,
|
|
39
|
+
} from "@lindorm/iris";
|
|
40
|
+
import type { IMessage } from "@lindorm/iris";
|
|
41
|
+
|
|
42
|
+
@Message()
|
|
43
|
+
@Namespace("orders")
|
|
44
|
+
@Version(1)
|
|
45
|
+
class OrderPlaced {
|
|
46
|
+
@IdentifierField() id!: string;
|
|
47
|
+
@TimestampField() createdAt!: Date;
|
|
48
|
+
@Field("string") orderId!: string;
|
|
49
|
+
@Field("float") total!: number;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. Create a Source and Connect
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { IrisSource } from "@lindorm/iris";
|
|
57
|
+
|
|
58
|
+
const source = new IrisSource({
|
|
59
|
+
driver: "rabbit",
|
|
60
|
+
url: "amqp://localhost",
|
|
61
|
+
logger: myLogger,
|
|
62
|
+
messages: [OrderPlaced],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await source.connect();
|
|
66
|
+
await source.setup();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3. Publish and Subscribe
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const bus = source.messageBus(OrderPlaced);
|
|
73
|
+
|
|
74
|
+
// Subscribe
|
|
75
|
+
await bus.subscribe({
|
|
76
|
+
topic: "OrderPlaced",
|
|
77
|
+
queue: "order-service",
|
|
78
|
+
callback: async (msg, envelope) => {
|
|
79
|
+
console.log(`Order ${msg.orderId} placed for $${msg.total}`);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Publish
|
|
84
|
+
const msg = bus.create({ orderId: "abc-123", total: 59.99 });
|
|
85
|
+
await bus.publish(msg);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 4. Graceful Shutdown
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
await source.drain();
|
|
92
|
+
await source.disconnect();
|
|
93
|
+
```
|
|
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
|
+
|
|
162
|
+
## Messaging Patterns
|
|
163
|
+
|
|
164
|
+
### Publisher (Fire-and-Forget)
|
|
165
|
+
|
|
166
|
+
Write-only. No subscriptions.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const pub = source.publisher(OrderPlaced);
|
|
170
|
+
|
|
171
|
+
const msg = pub.create({ orderId: "abc-123", total: 59.99 });
|
|
172
|
+
await pub.publish(msg);
|
|
173
|
+
|
|
174
|
+
// Batch publish
|
|
175
|
+
await pub.publish([msg1, msg2, msg3]);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Message Bus (Pub/Sub + Queues)
|
|
179
|
+
|
|
180
|
+
Publish with topic-based subscriptions. Supports broadcast and competing-consumer queues.
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
const bus = source.messageBus(OrderPlaced);
|
|
184
|
+
|
|
185
|
+
// Broadcast: every subscriber receives every message
|
|
186
|
+
await bus.subscribe({
|
|
187
|
+
topic: "OrderPlaced",
|
|
188
|
+
callback: async (msg) => {
|
|
189
|
+
/* ... */
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Queue: messages are distributed round-robin among consumers
|
|
194
|
+
await bus.subscribe({
|
|
195
|
+
topic: "OrderPlaced",
|
|
196
|
+
queue: "order-processors",
|
|
197
|
+
callback: async (msg) => {
|
|
198
|
+
/* ... */
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Multiple subscriptions at once
|
|
203
|
+
await bus.subscribe([
|
|
204
|
+
{ topic: "OrderPlaced", queue: "analytics", callback: handleAnalytics },
|
|
205
|
+
{ topic: "OrderPlaced", queue: "notifications", callback: handleNotify },
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
// Unsubscribe
|
|
209
|
+
await bus.unsubscribe({ topic: "OrderPlaced", queue: "analytics" });
|
|
210
|
+
await bus.unsubscribeAll();
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Worker Queue (Competing Consumers)
|
|
214
|
+
|
|
215
|
+
Specialised for job distribution where each message is processed by exactly one consumer.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
const queue = source.workerQueue(OrderPlaced);
|
|
219
|
+
|
|
220
|
+
// Register competing consumers
|
|
221
|
+
await queue.consume("process-orders", async (msg, envelope) => {
|
|
222
|
+
console.log(`Processing order ${msg.orderId} (attempt ${envelope.attempt})`);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Publish work
|
|
226
|
+
await queue.publish(queue.create({ orderId: "abc-123", total: 59.99 }));
|
|
227
|
+
|
|
228
|
+
// Clean up
|
|
229
|
+
await queue.unconsume("process-orders");
|
|
230
|
+
await queue.unconsumeAll();
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### RPC (Request/Response)
|
|
234
|
+
|
|
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
|
+
@Field("string", { nullable: true }) description!: string | null;
|
|
345
|
+
@Field("string", { optional: true }) nickname?: string;
|
|
346
|
+
@Field("integer", { default: 0 }) retryCount!: number;
|
|
347
|
+
@Field("string", { default: () => "generated" }) 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, options?: FieldDecoratorOptions)`.
|
|
653
|
+
|
|
654
|
+
**Options:**
|
|
655
|
+
|
|
656
|
+
| Field | Type | Default | Description |
|
|
657
|
+
| ----------- | -------------------------------- | ------- | ----------------------------------------- |
|
|
658
|
+
| `nullable` | `boolean` | `false` | Allow `null` values |
|
|
659
|
+
| `optional` | `boolean` | `false` | Field may be omitted |
|
|
660
|
+
| `default` | `value \| (() => value) \| null` | `null` | Default value applied on create |
|
|
661
|
+
| `transform` | `MetaTransform \| null` | `null` | Inline serialisation/deserialisation pair |
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
@Field("string", { nullable: true })
|
|
665
|
+
description!: string | null;
|
|
666
|
+
|
|
667
|
+
@Field("string", { optional: true })
|
|
668
|
+
nickname?: string;
|
|
669
|
+
|
|
670
|
+
@Field("integer", { default: 0 })
|
|
671
|
+
retryCount!: number;
|
|
672
|
+
|
|
673
|
+
@Field("string", { default: () => "generated" })
|
|
674
|
+
code!: string;
|
|
675
|
+
|
|
676
|
+
@Field("float", {
|
|
677
|
+
transform: {
|
|
678
|
+
to: (value: number) => Math.round(value * 100),
|
|
679
|
+
from: (raw: number) => raw / 100,
|
|
680
|
+
},
|
|
681
|
+
})
|
|
682
|
+
price!: number;
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
#### `@IdentifierField`
|
|
686
|
+
|
|
687
|
+
Shorthand for a UUID primary identifier field. Auto-generates a UUID v4 on message creation.
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
@IdentifierField()
|
|
691
|
+
id!: string;
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
Equivalent to `@Field("uuid", { default: () => randomUUID() })`. Non-nullable, non-optional.
|
|
695
|
+
|
|
696
|
+
No arguments.
|
|
697
|
+
|
|
698
|
+
#### `@CorrelationField`
|
|
699
|
+
|
|
700
|
+
Shorthand for a UUID correlation tracking field. Auto-generates a UUID v4 on message creation. Used to trace related messages across publish/consume chains.
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
@CorrelationField()
|
|
704
|
+
correlationId!: string;
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
Equivalent to `@Field("uuid", { default: () => randomUUID() })`. Non-nullable, non-optional.
|
|
708
|
+
|
|
709
|
+
No arguments.
|
|
710
|
+
|
|
711
|
+
#### `@TimestampField`
|
|
712
|
+
|
|
713
|
+
Shorthand for a timestamp field. Auto-generates the current `Date` on message creation.
|
|
714
|
+
|
|
715
|
+
```typescript
|
|
716
|
+
@TimestampField()
|
|
717
|
+
createdAt!: Date;
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
Equivalent to `@Field("date", { default: () => new Date() })`. Non-nullable, non-optional.
|
|
721
|
+
|
|
722
|
+
No arguments.
|
|
723
|
+
|
|
724
|
+
#### `@MandatoryField`
|
|
725
|
+
|
|
726
|
+
Shorthand for a boolean flag that defaults to `false`. Commonly used for acknowledgement or processing flags.
|
|
727
|
+
|
|
728
|
+
```typescript
|
|
729
|
+
@MandatoryField()
|
|
730
|
+
requiresApproval!: boolean;
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
Equivalent to `@Field("boolean", { default: false })`. Non-nullable, non-optional.
|
|
734
|
+
|
|
735
|
+
No arguments.
|
|
736
|
+
|
|
737
|
+
#### `@PersistentField`
|
|
738
|
+
|
|
739
|
+
Shorthand for a boolean persistence flag that defaults to `false`. Commonly used to mark whether a message should be durably stored.
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
@PersistentField()
|
|
743
|
+
shouldPersist!: boolean;
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
Equivalent to `@Field("boolean", { default: false })`. Non-nullable, non-optional.
|
|
747
|
+
|
|
748
|
+
No arguments.
|
|
749
|
+
|
|
750
|
+
---
|
|
751
|
+
|
|
752
|
+
### Field Modifiers
|
|
753
|
+
|
|
754
|
+
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.
|
|
755
|
+
|
|
756
|
+
#### `@Generated`
|
|
757
|
+
|
|
758
|
+
Marks a field for automatic value generation on message creation.
|
|
759
|
+
|
|
760
|
+
```typescript
|
|
761
|
+
@Generated("uuid") // UUID v4
|
|
762
|
+
@Field("uuid")
|
|
763
|
+
traceId!: string;
|
|
764
|
+
|
|
765
|
+
@Generated("date") // current timestamp
|
|
766
|
+
@Field("date")
|
|
767
|
+
processedAt!: Date;
|
|
768
|
+
|
|
769
|
+
@Generated("string") // random string (default length)
|
|
770
|
+
@Field("string")
|
|
771
|
+
token!: string;
|
|
772
|
+
|
|
773
|
+
@Generated("string", { length: 12 }) // random string with custom length
|
|
774
|
+
@Field("string")
|
|
775
|
+
shortCode!: string;
|
|
776
|
+
|
|
777
|
+
@Generated("integer", { min: 1, max: 1000 })
|
|
778
|
+
@Field("integer")
|
|
779
|
+
sequenceNumber!: number;
|
|
780
|
+
|
|
781
|
+
@Generated("float", { min: 0.0, max: 1.0 })
|
|
782
|
+
@Field("float")
|
|
783
|
+
weight!: number;
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
**Arguments:** `(strategy: MetaGeneratedStrategy, options?: GeneratedDecoratorOptions)`.
|
|
787
|
+
|
|
788
|
+
**Strategies:**
|
|
789
|
+
|
|
790
|
+
| Strategy | Description |
|
|
791
|
+
| ----------- | ---------------------------------------- |
|
|
792
|
+
| `"uuid"` | Generate UUID v4 |
|
|
793
|
+
| `"date"` | Current timestamp |
|
|
794
|
+
| `"string"` | Random string with configurable `length` |
|
|
795
|
+
| `"integer"` | Random integer in `[min, max]` range |
|
|
796
|
+
| `"float"` | Random float in `[min, max]` range |
|
|
797
|
+
|
|
798
|
+
**Options:** `{ length?: number, min?: number, max?: number }` — all optional, all default to `null`.
|
|
799
|
+
|
|
800
|
+
#### `@Header`
|
|
801
|
+
|
|
802
|
+
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.
|
|
803
|
+
|
|
804
|
+
```typescript
|
|
805
|
+
@Header() // header name = property name ("source")
|
|
806
|
+
@Field("string")
|
|
807
|
+
source!: string;
|
|
808
|
+
|
|
809
|
+
@Header("x-trace-id") // explicit header name
|
|
810
|
+
@Field("uuid")
|
|
811
|
+
traceId!: string;
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
**Argument:** `string?` — custom header name. Defaults to the property name. Throws `IrisMetadataError` if the resolved name is empty or whitespace-only.
|
|
815
|
+
|
|
816
|
+
#### `@Enum`
|
|
817
|
+
|
|
818
|
+
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.
|
|
819
|
+
|
|
820
|
+
```typescript
|
|
821
|
+
enum OrderStatus {
|
|
822
|
+
Pending = "pending",
|
|
823
|
+
Shipped = "shipped",
|
|
824
|
+
Delivered = "delivered",
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
@Enum(OrderStatus)
|
|
828
|
+
@Field("enum")
|
|
829
|
+
status!: OrderStatus;
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
**Argument:** `Record<string, string | number>` — the enum object or value map.
|
|
833
|
+
|
|
834
|
+
#### `@Min` / `@Max`
|
|
835
|
+
|
|
836
|
+
Set minimum/maximum bounds for numeric fields or minimum/maximum length for string fields. Enforced during Zod validation.
|
|
837
|
+
|
|
838
|
+
```typescript
|
|
839
|
+
@Min(0)
|
|
840
|
+
@Max(100)
|
|
841
|
+
@Field("integer")
|
|
842
|
+
score!: number;
|
|
843
|
+
|
|
844
|
+
@Min(1)
|
|
845
|
+
@Max(255)
|
|
846
|
+
@Field("string")
|
|
847
|
+
name!: string;
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
**Argument:** `number`.
|
|
851
|
+
|
|
852
|
+
#### `@Schema`
|
|
853
|
+
|
|
854
|
+
Attaches a Zod schema for fine-grained field validation. The schema is evaluated during message validation.
|
|
855
|
+
|
|
856
|
+
```typescript
|
|
857
|
+
import { z } from "zod";
|
|
858
|
+
|
|
859
|
+
@Schema(z.string().email())
|
|
860
|
+
@Field("email")
|
|
861
|
+
email!: string;
|
|
862
|
+
|
|
863
|
+
@Schema(z.number().int().min(13).max(150))
|
|
864
|
+
@Field("integer")
|
|
865
|
+
age!: number;
|
|
866
|
+
|
|
867
|
+
@Schema(z.string().regex(/^[A-Z]{2,3}$/))
|
|
868
|
+
@Field("string")
|
|
869
|
+
countryCode!: string;
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
**Argument:** `z.ZodType` — any Zod schema.
|
|
873
|
+
|
|
874
|
+
#### `@Transform`
|
|
875
|
+
|
|
876
|
+
Applies a bidirectional transform to a field value. `to` runs during serialisation (message -> transport), `from` runs during deserialisation (transport -> message).
|
|
877
|
+
|
|
878
|
+
```typescript
|
|
879
|
+
@Transform({
|
|
880
|
+
to: (value: string[]) => value.join(","),
|
|
881
|
+
from: (raw: string) => raw.split(","),
|
|
882
|
+
})
|
|
883
|
+
@Field("string")
|
|
884
|
+
tags!: string[];
|
|
885
|
+
|
|
886
|
+
@Transform<Date, number>({
|
|
887
|
+
to: (date) => date.getTime(),
|
|
888
|
+
from: (ms) => new Date(ms),
|
|
889
|
+
})
|
|
890
|
+
@Field("bigint")
|
|
891
|
+
timestamp!: Date;
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
**Options:** `{ to: (value: TFrom) => TTo, from: (raw: TTo) => TFrom }`.
|
|
895
|
+
|
|
896
|
+
This is a standalone decorator that uses a separate staging path. For inline transforms, use the `transform` option on `@Field` instead.
|
|
897
|
+
|
|
898
|
+
---
|
|
899
|
+
|
|
900
|
+
### Lifecycle Hook Decorators
|
|
901
|
+
|
|
902
|
+
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.
|
|
903
|
+
|
|
904
|
+
Hooks may be async (`void | Promise<void>`) unless otherwise noted.
|
|
905
|
+
|
|
906
|
+
#### `@OnCreate`
|
|
907
|
+
|
|
908
|
+
Fires when a message instance is created via `create()`. Useful for setting computed defaults or derived fields.
|
|
909
|
+
|
|
910
|
+
```typescript
|
|
911
|
+
@OnCreate((msg) => {
|
|
912
|
+
msg.slug = msg.name.toLowerCase().replace(/\s+/g, "-");
|
|
913
|
+
})
|
|
914
|
+
@Message()
|
|
915
|
+
class OrderPlaced {
|
|
916
|
+
@Field("string") name!: string;
|
|
917
|
+
@Field("string") slug!: string;
|
|
918
|
+
}
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
**Argument:** `(message: M, context?: C) => void | Promise<void>`.
|
|
922
|
+
|
|
923
|
+
#### `@OnHydrate`
|
|
924
|
+
|
|
925
|
+
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.
|
|
926
|
+
|
|
927
|
+
```typescript
|
|
928
|
+
@OnHydrate((msg) => {
|
|
929
|
+
msg.displayName = `${msg.firstName} ${msg.lastName}`;
|
|
930
|
+
})
|
|
931
|
+
@Message()
|
|
932
|
+
class UserEvent {
|
|
933
|
+
@Field("string") firstName!: string;
|
|
934
|
+
@Field("string") lastName!: string;
|
|
935
|
+
@Field("string") displayName!: string;
|
|
936
|
+
}
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
**Argument:** `(message: M, context?: C) => void | Promise<void>`.
|
|
940
|
+
|
|
941
|
+
#### `@OnValidate`
|
|
942
|
+
|
|
943
|
+
Fires during message validation, after the built-in Zod schema check. Throw to reject the message.
|
|
944
|
+
|
|
945
|
+
```typescript
|
|
946
|
+
@OnValidate((msg) => {
|
|
947
|
+
if (msg.startDate >= msg.endDate) {
|
|
948
|
+
throw new Error("startDate must be before endDate");
|
|
949
|
+
}
|
|
950
|
+
})
|
|
951
|
+
@Message()
|
|
952
|
+
class BookingRequest {
|
|
953
|
+
@Field("date") startDate!: Date;
|
|
954
|
+
@Field("date") endDate!: Date;
|
|
955
|
+
}
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
**Argument:** `(message: M, context?: C) => void | Promise<void>`.
|
|
959
|
+
|
|
960
|
+
#### `@BeforePublish` / `@AfterPublish`
|
|
961
|
+
|
|
962
|
+
Fire around publish operations. `@BeforePublish` runs before the message is handed to the transport. `@AfterPublish` runs after the transport confirms delivery.
|
|
963
|
+
|
|
964
|
+
```typescript
|
|
965
|
+
@BeforePublish(async (msg) => {
|
|
966
|
+
await validateExternalId(msg.externalId);
|
|
967
|
+
})
|
|
968
|
+
@AfterPublish(async (msg) => {
|
|
969
|
+
metrics.increment("messages.published");
|
|
970
|
+
})
|
|
971
|
+
@Message()
|
|
972
|
+
class OrderPlaced {
|
|
973
|
+
@Field("string") externalId!: string;
|
|
974
|
+
}
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
**Argument:** `(message: M, context?: C) => void | Promise<void>`.
|
|
978
|
+
|
|
979
|
+
#### `@BeforeConsume` / `@AfterConsume`
|
|
980
|
+
|
|
981
|
+
Fire around consume callback execution. `@BeforeConsume` runs after deserialisation but before the consume callback. `@AfterConsume` runs after the callback completes successfully.
|
|
982
|
+
|
|
983
|
+
```typescript
|
|
984
|
+
@BeforeConsume(async (msg, ctx) => {
|
|
985
|
+
audit.log("consuming", msg);
|
|
986
|
+
})
|
|
987
|
+
@AfterConsume(async (msg) => {
|
|
988
|
+
metrics.increment("messages.consumed");
|
|
989
|
+
})
|
|
990
|
+
@Message()
|
|
991
|
+
class OrderPlaced {
|
|
992
|
+
/* ... */
|
|
993
|
+
}
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
**Argument:** `(message: M, context?: C) => void | Promise<void>`.
|
|
997
|
+
|
|
998
|
+
#### `@OnConsumeError`
|
|
999
|
+
|
|
1000
|
+
Fires when a consume callback throws an error. Receives the error as the **first** argument, followed by the message and context.
|
|
1001
|
+
|
|
1002
|
+
```typescript
|
|
1003
|
+
@OnConsumeError(async (error, msg) => {
|
|
1004
|
+
errorTracker.capture(error, { messageId: msg.id });
|
|
1005
|
+
})
|
|
1006
|
+
@Message()
|
|
1007
|
+
class PaymentCharge {
|
|
1008
|
+
@IdentifierField() id!: string;
|
|
1009
|
+
@Field("string") chargeId!: string;
|
|
1010
|
+
}
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
**Argument:** `(error: Error, message: M, context?: C) => void | Promise<void>`.
|
|
1014
|
+
|
|
1015
|
+
Note the different signature: `error` is the first parameter, unlike all other hooks where the message comes first.
|
|
1016
|
+
|
|
1017
|
+
---
|
|
1018
|
+
|
|
1019
|
+
### Hook Execution Order
|
|
1020
|
+
|
|
1021
|
+
| Phase | Hooks (in order) |
|
|
1022
|
+
| ---------- | ------------------------------------------------------------------------------------------------------- |
|
|
1023
|
+
| Creation | `@OnCreate` |
|
|
1024
|
+
| Validation | `@OnValidate` |
|
|
1025
|
+
| Publish | `@BeforePublish` -> subscriber.beforePublish -> transport -> `@AfterPublish` -> subscriber.afterPublish |
|
|
1026
|
+
| Consume | `@BeforeConsume` -> subscriber.beforeConsume -> callback -> `@AfterConsume` -> subscriber.afterConsume |
|
|
1027
|
+
| Hydration | `@OnHydrate` |
|
|
1028
|
+
| Error | `@OnConsumeError` -> subscriber.onConsumeError (replaces AfterConsume steps) |
|
|
1029
|
+
|
|
1030
|
+
Full publish + consume lifecycle:
|
|
1031
|
+
|
|
1032
|
+
```
|
|
1033
|
+
1. @BeforePublish hook
|
|
1034
|
+
2. subscriber.beforePublish
|
|
1035
|
+
3. (transport publishes)
|
|
1036
|
+
4. (transport delivers to consumer)
|
|
1037
|
+
5. @BeforeConsume hook
|
|
1038
|
+
6. subscriber.beforeConsume
|
|
1039
|
+
7. callback executes
|
|
1040
|
+
8. @AfterConsume hook
|
|
1041
|
+
9. subscriber.afterConsume
|
|
1042
|
+
10. @AfterPublish hook
|
|
1043
|
+
11. subscriber.afterPublish
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
On error at step 7, steps 8-11 are replaced by `@OnConsumeError` and `subscriber.onConsumeError`.
|
|
1047
|
+
|
|
1048
|
+
## Retry and Dead Letter
|
|
1049
|
+
|
|
1050
|
+
Configure automatic retry with backoff strategies and dead letter routing for permanently failed messages.
|
|
1051
|
+
|
|
1052
|
+
```typescript
|
|
1053
|
+
@Retry({
|
|
1054
|
+
maxRetries: 5,
|
|
1055
|
+
strategy: "exponential", // "constant" | "linear" | "exponential"
|
|
1056
|
+
delay: 1000, // initial delay in ms
|
|
1057
|
+
delayMax: 30000, // maximum delay cap
|
|
1058
|
+
multiplier: 2, // exponential multiplier
|
|
1059
|
+
jitter: true, // add randomness to prevent thundering herd
|
|
1060
|
+
})
|
|
1061
|
+
@DeadLetter()
|
|
1062
|
+
@Message()
|
|
1063
|
+
class PaymentCharge {
|
|
1064
|
+
@Field("string") chargeId!: string;
|
|
1065
|
+
@Field("float") amount!: number;
|
|
1066
|
+
}
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
## Dynamic Topics
|
|
1070
|
+
|
|
1071
|
+
Route messages to different topics based on their content:
|
|
1072
|
+
|
|
1073
|
+
```typescript
|
|
1074
|
+
@Topic((msg: any) => `events.${msg.region}.${msg.type}`)
|
|
1075
|
+
@Message()
|
|
1076
|
+
class RegionalEvent {
|
|
1077
|
+
@Field("string") region!: string;
|
|
1078
|
+
@Field("string") type!: string;
|
|
1079
|
+
@Field("object") data!: Record<string, unknown>;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const bus = source.messageBus(RegionalEvent);
|
|
1083
|
+
const msg = bus.create({ region: "eu-west", type: "signup", data: {} });
|
|
1084
|
+
await bus.publish(msg); // Published to "events.eu-west.signup"
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
## Encryption and Compression
|
|
1088
|
+
|
|
1089
|
+
```typescript
|
|
1090
|
+
import { Encrypted, Compressed } from "@lindorm/iris";
|
|
1091
|
+
|
|
1092
|
+
@Encrypted() // Encrypt payload via amphora
|
|
1093
|
+
@Compressed("brotli") // Then compress ("gzip" | "deflate" | "brotli")
|
|
1094
|
+
@Message()
|
|
1095
|
+
class SensitivePayload {
|
|
1096
|
+
@Field("string") ssn!: string;
|
|
1097
|
+
@Field("string") name!: string;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Source must be configured with an amphora instance
|
|
1101
|
+
const source = new IrisSource({
|
|
1102
|
+
driver: "rabbit",
|
|
1103
|
+
url: "amqp://localhost",
|
|
1104
|
+
logger: myLogger,
|
|
1105
|
+
amphora: myAmphora,
|
|
1106
|
+
messages: [SensitivePayload],
|
|
1107
|
+
});
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
## Message Subscribers
|
|
1111
|
+
|
|
1112
|
+
Observe message lifecycle events across all messages in a source:
|
|
1113
|
+
|
|
1114
|
+
```typescript
|
|
1115
|
+
import type { IMessageSubscriber } from "@lindorm/iris";
|
|
1116
|
+
|
|
1117
|
+
const auditSubscriber: IMessageSubscriber = {
|
|
1118
|
+
beforePublish: async (msg) => {
|
|
1119
|
+
audit.log("publishing", msg);
|
|
1120
|
+
},
|
|
1121
|
+
afterConsume: async (msg) => {
|
|
1122
|
+
audit.log("consumed", msg);
|
|
1123
|
+
},
|
|
1124
|
+
onConsumeError: async (error, msg) => {
|
|
1125
|
+
audit.log("consume-failed", { error: error.message, msg });
|
|
1126
|
+
},
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
source.addSubscriber(auditSubscriber);
|
|
1130
|
+
|
|
1131
|
+
// Remove later
|
|
1132
|
+
source.removeSubscriber(auditSubscriber);
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
## Consume Envelope
|
|
1136
|
+
|
|
1137
|
+
Every subscribe/consume callback receives the message and an envelope with routing metadata:
|
|
1138
|
+
|
|
1139
|
+
```typescript
|
|
1140
|
+
import type { ConsumeEnvelope } from "@lindorm/iris";
|
|
1141
|
+
|
|
1142
|
+
await bus.subscribe({
|
|
1143
|
+
topic: "OrderPlaced",
|
|
1144
|
+
callback: async (msg: OrderPlaced, envelope: ConsumeEnvelope) => {
|
|
1145
|
+
console.log(envelope.topic); // "OrderPlaced"
|
|
1146
|
+
console.log(envelope.messageName); // "OrderPlaced"
|
|
1147
|
+
console.log(envelope.namespace); // "orders" | null
|
|
1148
|
+
console.log(envelope.version); // 1
|
|
1149
|
+
console.log(envelope.headers); // Record<string, string>
|
|
1150
|
+
console.log(envelope.attempt); // 1 (increments on retry)
|
|
1151
|
+
console.log(envelope.correlationId); // string | null
|
|
1152
|
+
console.log(envelope.timestamp); // Unix timestamp
|
|
1153
|
+
},
|
|
1154
|
+
});
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
## Message Manipulation
|
|
1158
|
+
|
|
1159
|
+
Every publisher, message bus, and worker queue provides utilities for working with message instances:
|
|
1160
|
+
|
|
1161
|
+
```typescript
|
|
1162
|
+
const bus = source.messageBus(OrderPlaced);
|
|
1163
|
+
|
|
1164
|
+
// Create: new instance with auto-generated fields and defaults
|
|
1165
|
+
const msg = bus.create({ orderId: "abc-123", total: 59.99 });
|
|
1166
|
+
|
|
1167
|
+
// Hydrate: reconstruct from raw data (no auto-generation)
|
|
1168
|
+
const hydrated = bus.hydrate({ orderId: "abc-123", total: 59.99, id: "existing-uuid" });
|
|
1169
|
+
|
|
1170
|
+
// Copy: deep clone with a fresh identifier
|
|
1171
|
+
const copied = bus.copy(msg);
|
|
1172
|
+
// copied.orderId === msg.orderId, but copied.id !== msg.id
|
|
1173
|
+
|
|
1174
|
+
// Validate: throws IrisValidationError if invalid
|
|
1175
|
+
bus.validate(msg);
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
## Publish Options
|
|
1179
|
+
|
|
1180
|
+
Override message-level defaults per publish call:
|
|
1181
|
+
|
|
1182
|
+
```typescript
|
|
1183
|
+
await bus.publish(msg, {
|
|
1184
|
+
delay: 5000, // delay delivery by 5 seconds
|
|
1185
|
+
priority: 8, // override @Priority
|
|
1186
|
+
expiry: 60000, // override @Expiry (TTL in ms)
|
|
1187
|
+
key: "partition-key", // routing/partition key
|
|
1188
|
+
headers: { "x-source": "api" }, // additional headers
|
|
1189
|
+
});
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
## Zod Validation
|
|
1193
|
+
|
|
1194
|
+
Use `@Schema()` with Zod for fine-grained field validation:
|
|
1195
|
+
|
|
1196
|
+
```typescript
|
|
1197
|
+
import { z } from "zod";
|
|
1198
|
+
import { Schema, Field, Message } from "@lindorm/iris";
|
|
1199
|
+
|
|
1200
|
+
@Message()
|
|
1201
|
+
class UserCreated {
|
|
1202
|
+
@Schema(z.string().email())
|
|
1203
|
+
@Field("email")
|
|
1204
|
+
email!: string;
|
|
1205
|
+
|
|
1206
|
+
@Schema(z.number().int().min(13).max(150))
|
|
1207
|
+
@Field("integer")
|
|
1208
|
+
age!: number;
|
|
1209
|
+
|
|
1210
|
+
@Schema(z.string().regex(/^[A-Z]{2,3}$/))
|
|
1211
|
+
@Field("string")
|
|
1212
|
+
countryCode!: string;
|
|
1213
|
+
}
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
## Cloning
|
|
1217
|
+
|
|
1218
|
+
Create independent source instances that share the underlying driver connection:
|
|
1219
|
+
|
|
1220
|
+
```typescript
|
|
1221
|
+
const source = new IrisSource({
|
|
1222
|
+
driver: "rabbit",
|
|
1223
|
+
url: "amqp://localhost",
|
|
1224
|
+
logger: mainLogger,
|
|
1225
|
+
messages: [OrderPlaced],
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
await source.connect();
|
|
1229
|
+
await source.setup();
|
|
1230
|
+
|
|
1231
|
+
// Clone shares the connection but has its own logger and subscriber registry
|
|
1232
|
+
const scoped = source.clone({ logger: requestLogger, context: { requestId: "abc" } });
|
|
1233
|
+
|
|
1234
|
+
const pub = scoped.publisher(OrderPlaced);
|
|
1235
|
+
await pub.publish(pub.create({ orderId: "abc-123", total: 59.99 }));
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
## Driver Configuration
|
|
1239
|
+
|
|
1240
|
+
### Memory
|
|
1241
|
+
|
|
1242
|
+
```typescript
|
|
1243
|
+
const source = new IrisSource({
|
|
1244
|
+
driver: "memory",
|
|
1245
|
+
logger,
|
|
1246
|
+
messages: [OrderPlaced],
|
|
1247
|
+
});
|
|
1248
|
+
```
|
|
1249
|
+
|
|
1250
|
+
### RabbitMQ
|
|
1251
|
+
|
|
1252
|
+
```typescript
|
|
1253
|
+
const source = new IrisSource({
|
|
1254
|
+
driver: "rabbit",
|
|
1255
|
+
url: "amqp://localhost",
|
|
1256
|
+
logger,
|
|
1257
|
+
messages: [OrderPlaced],
|
|
1258
|
+
exchange: "my-exchange", // optional
|
|
1259
|
+
prefetch: 10, // optional
|
|
1260
|
+
connection: {
|
|
1261
|
+
// optional
|
|
1262
|
+
heartbeat: 60,
|
|
1263
|
+
socketOptions: {
|
|
1264
|
+
timeout: 30000,
|
|
1265
|
+
keepAlive: true,
|
|
1266
|
+
},
|
|
1267
|
+
},
|
|
1268
|
+
});
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
### Kafka
|
|
1272
|
+
|
|
1273
|
+
```typescript
|
|
1274
|
+
const source = new IrisSource({
|
|
1275
|
+
driver: "kafka",
|
|
1276
|
+
brokers: ["localhost:9092"],
|
|
1277
|
+
logger,
|
|
1278
|
+
messages: [OrderPlaced],
|
|
1279
|
+
prefix: "my-app", // optional topic prefix
|
|
1280
|
+
prefetch: 100, // optional
|
|
1281
|
+
acks: -1, // optional: -1 (all), 0 (none), 1 (leader)
|
|
1282
|
+
sessionTimeoutMs: 30000, // optional
|
|
1283
|
+
connection: {
|
|
1284
|
+
// optional
|
|
1285
|
+
clientId: "my-service",
|
|
1286
|
+
ssl: true,
|
|
1287
|
+
sasl: {
|
|
1288
|
+
mechanism: "scram-sha-256",
|
|
1289
|
+
username: "user",
|
|
1290
|
+
password: "pass",
|
|
1291
|
+
},
|
|
1292
|
+
connectionTimeout: 10000,
|
|
1293
|
+
requestTimeout: 30000,
|
|
1294
|
+
retry: {
|
|
1295
|
+
retries: 5,
|
|
1296
|
+
initialRetryTime: 300,
|
|
1297
|
+
},
|
|
1298
|
+
},
|
|
1299
|
+
});
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
### NATS
|
|
1303
|
+
|
|
1304
|
+
```typescript
|
|
1305
|
+
const source = new IrisSource({
|
|
1306
|
+
driver: "nats",
|
|
1307
|
+
servers: "nats://localhost:4222", // string or Array<string>
|
|
1308
|
+
logger,
|
|
1309
|
+
messages: [OrderPlaced],
|
|
1310
|
+
prefix: "my-app", // optional
|
|
1311
|
+
prefetch: 50, // optional
|
|
1312
|
+
connection: {
|
|
1313
|
+
// optional
|
|
1314
|
+
user: "nats-user",
|
|
1315
|
+
pass: "nats-pass",
|
|
1316
|
+
tls: true,
|
|
1317
|
+
maxReconnectAttempts: 10,
|
|
1318
|
+
reconnectTimeWait: 2000,
|
|
1319
|
+
timeout: 10000,
|
|
1320
|
+
pingInterval: 30000,
|
|
1321
|
+
name: "my-service",
|
|
1322
|
+
},
|
|
1323
|
+
});
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
### Redis Streams
|
|
1327
|
+
|
|
1328
|
+
```typescript
|
|
1329
|
+
const source = new IrisSource({
|
|
1330
|
+
driver: "redis",
|
|
1331
|
+
url: "redis://localhost:6379",
|
|
1332
|
+
logger,
|
|
1333
|
+
messages: [OrderPlaced],
|
|
1334
|
+
prefix: "my-app", // optional
|
|
1335
|
+
prefetch: 50, // optional
|
|
1336
|
+
blockMs: 5000, // optional: XREAD block time
|
|
1337
|
+
maxStreamLength: 10000, // optional: MAXLEN cap per stream
|
|
1338
|
+
connection: {
|
|
1339
|
+
// optional
|
|
1340
|
+
host: "redis.internal",
|
|
1341
|
+
port: 6379,
|
|
1342
|
+
password: "secret",
|
|
1343
|
+
db: 0,
|
|
1344
|
+
tls: {},
|
|
1345
|
+
connectTimeout: 10000,
|
|
1346
|
+
commandTimeout: 5000,
|
|
1347
|
+
keepAlive: 30000,
|
|
1348
|
+
connectionName: "iris-worker",
|
|
1349
|
+
},
|
|
1350
|
+
});
|
|
1351
|
+
```
|
|
1352
|
+
|
|
1353
|
+
## Persistence (Delay and Dead Letter Stores)
|
|
1354
|
+
|
|
1355
|
+
Configure where delayed messages and dead letter entries are stored:
|
|
1356
|
+
|
|
1357
|
+
```typescript
|
|
1358
|
+
const source = new IrisSource({
|
|
1359
|
+
driver: "rabbit",
|
|
1360
|
+
url: "amqp://localhost",
|
|
1361
|
+
logger,
|
|
1362
|
+
messages: [OrderPlaced],
|
|
1363
|
+
persistence: {
|
|
1364
|
+
// Delay store: holds messages until their scheduled delivery time
|
|
1365
|
+
delay: { type: "memory" },
|
|
1366
|
+
// or: { type: "redis", url: "redis://localhost:6379" },
|
|
1367
|
+
// or: { type: "custom", store: myDelayStore },
|
|
1368
|
+
|
|
1369
|
+
// Dead letter store: holds messages that exhausted all retries
|
|
1370
|
+
deadLetter: { type: "memory" },
|
|
1371
|
+
// or: { type: "redis", url: "redis://localhost:6379" },
|
|
1372
|
+
// or: { type: "custom", store: myDeadLetterStore },
|
|
1373
|
+
},
|
|
1374
|
+
});
|
|
1375
|
+
```
|
|
1376
|
+
|
|
1377
|
+
### Custom Stores
|
|
1378
|
+
|
|
1379
|
+
Implement `IDelayStore` and/or `IDeadLetterStore` for custom persistence:
|
|
1380
|
+
|
|
1381
|
+
```typescript
|
|
1382
|
+
import type {
|
|
1383
|
+
IDelayStore,
|
|
1384
|
+
IDeadLetterStore,
|
|
1385
|
+
DelayedEntry,
|
|
1386
|
+
DeadLetterEntry,
|
|
1387
|
+
} from "@lindorm/iris";
|
|
1388
|
+
|
|
1389
|
+
class MyDelayStore implements IDelayStore {
|
|
1390
|
+
async schedule(entry: DelayedEntry): Promise<void> {
|
|
1391
|
+
/* ... */
|
|
1392
|
+
}
|
|
1393
|
+
async poll(now: number): Promise<Array<DelayedEntry>> {
|
|
1394
|
+
/* ... */
|
|
1395
|
+
}
|
|
1396
|
+
async cancel(id: string): Promise<boolean> {
|
|
1397
|
+
/* ... */
|
|
1398
|
+
}
|
|
1399
|
+
async size(): Promise<number> {
|
|
1400
|
+
/* ... */
|
|
1401
|
+
}
|
|
1402
|
+
async clear(): Promise<void> {
|
|
1403
|
+
/* ... */
|
|
1404
|
+
}
|
|
1405
|
+
async close(): Promise<void> {
|
|
1406
|
+
/* ... */
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
class MyDeadLetterStore implements IDeadLetterStore {
|
|
1411
|
+
async add(entry: DeadLetterEntry): Promise<void> {
|
|
1412
|
+
/* ... */
|
|
1413
|
+
}
|
|
1414
|
+
async list(options?: {
|
|
1415
|
+
topic?: string;
|
|
1416
|
+
limit?: number;
|
|
1417
|
+
offset?: number;
|
|
1418
|
+
}): Promise<Array<DeadLetterEntry>> {
|
|
1419
|
+
/* ... */
|
|
1420
|
+
}
|
|
1421
|
+
async get(id: string): Promise<DeadLetterEntry | null> {
|
|
1422
|
+
/* ... */
|
|
1423
|
+
}
|
|
1424
|
+
async remove(id: string): Promise<boolean> {
|
|
1425
|
+
/* ... */
|
|
1426
|
+
}
|
|
1427
|
+
async purge(options?: { topic?: string }): Promise<number> {
|
|
1428
|
+
/* ... */
|
|
1429
|
+
}
|
|
1430
|
+
async count(options?: { topic?: string }): Promise<number> {
|
|
1431
|
+
/* ... */
|
|
1432
|
+
}
|
|
1433
|
+
async close(): Promise<void> {
|
|
1434
|
+
/* ... */
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
```
|
|
1438
|
+
|
|
1439
|
+
## Connection State
|
|
1440
|
+
|
|
1441
|
+
```typescript
|
|
1442
|
+
const state = source.getConnectionState();
|
|
1443
|
+
// "disconnected" | "connecting" | "connected" | "reconnecting" | "draining"
|
|
1444
|
+
|
|
1445
|
+
source.onConnectionStateChange((state) => {
|
|
1446
|
+
console.log(`Connection state: ${state}`);
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
// Health check
|
|
1450
|
+
const healthy = await source.ping();
|
|
1451
|
+
```
|
|
1452
|
+
|
|
1453
|
+
## Testing with Mocks
|
|
1454
|
+
|
|
1455
|
+
All mocks are available via the `@lindorm/iris/mocks` subpath:
|
|
1456
|
+
|
|
1457
|
+
```typescript
|
|
1458
|
+
import {
|
|
1459
|
+
createMockIrisSource,
|
|
1460
|
+
createMockPublisher,
|
|
1461
|
+
createMockMessageBus,
|
|
1462
|
+
createMockWorkerQueue,
|
|
1463
|
+
createMockRpcClient,
|
|
1464
|
+
} from "@lindorm/iris/mocks";
|
|
1465
|
+
```
|
|
1466
|
+
|
|
1467
|
+
### Mock Source
|
|
1468
|
+
|
|
1469
|
+
```typescript
|
|
1470
|
+
const source = createMockIrisSource();
|
|
1471
|
+
|
|
1472
|
+
// All methods are jest.fn() mocks
|
|
1473
|
+
expect(source.connect).not.toHaveBeenCalled();
|
|
1474
|
+
|
|
1475
|
+
await source.connect();
|
|
1476
|
+
expect(source.connect).toHaveBeenCalledTimes(1);
|
|
1477
|
+
|
|
1478
|
+
// Factory methods return mocks by default
|
|
1479
|
+
const bus = source.messageBus(OrderPlaced);
|
|
1480
|
+
const pub = source.publisher(OrderPlaced);
|
|
1481
|
+
const queue = source.workerQueue(OrderPlaced);
|
|
1482
|
+
const rpc = source.rpcClient(GetPrice, PriceResponse);
|
|
1483
|
+
```
|
|
1484
|
+
|
|
1485
|
+
### Mock Publisher
|
|
1486
|
+
|
|
1487
|
+
```typescript
|
|
1488
|
+
const pub = createMockPublisher<OrderPlaced>();
|
|
1489
|
+
|
|
1490
|
+
const msg = pub.create({ orderId: "abc", total: 10 });
|
|
1491
|
+
await pub.publish(msg);
|
|
1492
|
+
|
|
1493
|
+
// Inspect published messages
|
|
1494
|
+
expect(pub.published).toHaveLength(1);
|
|
1495
|
+
|
|
1496
|
+
// Reset
|
|
1497
|
+
pub.clearPublished();
|
|
1498
|
+
expect(pub.published).toHaveLength(0);
|
|
1499
|
+
```
|
|
1500
|
+
|
|
1501
|
+
### Mock Message Bus
|
|
1502
|
+
|
|
1503
|
+
```typescript
|
|
1504
|
+
const bus = createMockMessageBus<OrderPlaced>();
|
|
1505
|
+
|
|
1506
|
+
await bus.publish(bus.create({ orderId: "abc", total: 10 }));
|
|
1507
|
+
|
|
1508
|
+
expect(bus.published).toHaveLength(1);
|
|
1509
|
+
expect(bus.subscribe).not.toHaveBeenCalled();
|
|
1510
|
+
|
|
1511
|
+
bus.clearPublished();
|
|
1512
|
+
```
|
|
1513
|
+
|
|
1514
|
+
### Mock Worker Queue
|
|
1515
|
+
|
|
1516
|
+
```typescript
|
|
1517
|
+
const queue = createMockWorkerQueue<OrderPlaced>();
|
|
1518
|
+
|
|
1519
|
+
await queue.publish(queue.create({ orderId: "abc", total: 10 }));
|
|
1520
|
+
|
|
1521
|
+
expect(queue.published).toHaveLength(1);
|
|
1522
|
+
expect(queue.consume).not.toHaveBeenCalled();
|
|
1523
|
+
|
|
1524
|
+
queue.clearPublished();
|
|
1525
|
+
```
|
|
1526
|
+
|
|
1527
|
+
### Mock RPC Client
|
|
1528
|
+
|
|
1529
|
+
```typescript
|
|
1530
|
+
// Provide a response factory
|
|
1531
|
+
const client = createMockRpcClient<GetPrice, PriceResponse>((req) => {
|
|
1532
|
+
const res = new PriceResponse();
|
|
1533
|
+
res.price = 42.0;
|
|
1534
|
+
res.currency = "USD";
|
|
1535
|
+
return res;
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
const req = new GetPrice();
|
|
1539
|
+
req.sku = "WIDGET-42";
|
|
1540
|
+
|
|
1541
|
+
const res = await client.request(req);
|
|
1542
|
+
expect(res.price).toBe(42.0);
|
|
1543
|
+
expect(client.requests).toHaveLength(1);
|
|
1544
|
+
|
|
1545
|
+
client.clearRequests();
|
|
1546
|
+
```
|
|
1547
|
+
|
|
1548
|
+
## Error Classes
|
|
1549
|
+
|
|
1550
|
+
All errors extend `IrisError`, which extends `LindormError`:
|
|
1551
|
+
|
|
1552
|
+
| Error Class | When |
|
|
1553
|
+
| ------------------------ | ----------------------------------------- |
|
|
1554
|
+
| `IrisError` | Base class for all iris errors |
|
|
1555
|
+
| `IrisDriverError` | Driver connection or operation failure |
|
|
1556
|
+
| `IrisMetadataError` | Invalid decorator configuration |
|
|
1557
|
+
| `IrisNotSupportedError` | Unsupported feature for the active driver |
|
|
1558
|
+
| `IrisPublishError` | Message publishing failure |
|
|
1559
|
+
| `IrisScannerError` | Message class scanning failure |
|
|
1560
|
+
| `IrisSerializationError` | Serialisation or deserialisation failure |
|
|
1561
|
+
| `IrisSourceError` | Source setup or configuration error |
|
|
1562
|
+
| `IrisTimeoutError` | Operation exceeded timeout |
|
|
1563
|
+
| `IrisTransportError` | Transport layer failure |
|
|
1564
|
+
| `IrisValidationError` | Message validation failure |
|
|
1565
|
+
|
|
1566
|
+
```typescript
|
|
1567
|
+
import { IrisTimeoutError, IrisValidationError } from "@lindorm/iris";
|
|
1568
|
+
|
|
1569
|
+
try {
|
|
1570
|
+
await client.request(req, { timeout: 1000 });
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
if (error instanceof IrisTimeoutError) {
|
|
1573
|
+
// handle timeout
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
```
|
|
1577
|
+
|
|
1578
|
+
## Full Example
|
|
1579
|
+
|
|
1580
|
+
```typescript
|
|
1581
|
+
import {
|
|
1582
|
+
IrisSource,
|
|
1583
|
+
Message,
|
|
1584
|
+
Namespace,
|
|
1585
|
+
Version,
|
|
1586
|
+
Field,
|
|
1587
|
+
IdentifierField,
|
|
1588
|
+
TimestampField,
|
|
1589
|
+
Retry,
|
|
1590
|
+
DeadLetter,
|
|
1591
|
+
} from "@lindorm/iris";
|
|
1592
|
+
import type { IMessage, IMessageSubscriber } from "@lindorm/iris";
|
|
1593
|
+
|
|
1594
|
+
// --- Define messages ---
|
|
1595
|
+
|
|
1596
|
+
@Message()
|
|
1597
|
+
@Namespace("payments")
|
|
1598
|
+
@Version(1)
|
|
1599
|
+
@Retry({ maxRetries: 3, strategy: "exponential", delay: 1000 })
|
|
1600
|
+
@DeadLetter()
|
|
1601
|
+
class ChargeRequested {
|
|
1602
|
+
@IdentifierField() id!: string;
|
|
1603
|
+
@TimestampField() createdAt!: Date;
|
|
1604
|
+
@Field("string") paymentId!: string;
|
|
1605
|
+
@Field("float") amount!: number;
|
|
1606
|
+
@Field("string") currency!: string;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
@Message()
|
|
1610
|
+
@Namespace("payments")
|
|
1611
|
+
@Version(1)
|
|
1612
|
+
class ChargeCompleted {
|
|
1613
|
+
@IdentifierField() id!: string;
|
|
1614
|
+
@TimestampField() completedAt!: Date;
|
|
1615
|
+
@Field("string") paymentId!: string;
|
|
1616
|
+
@Field("boolean") success!: boolean;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// --- Set up source ---
|
|
1620
|
+
|
|
1621
|
+
const source = new IrisSource({
|
|
1622
|
+
driver: "kafka",
|
|
1623
|
+
brokers: ["kafka-1:9092", "kafka-2:9092"],
|
|
1624
|
+
logger: appLogger,
|
|
1625
|
+
messages: [ChargeRequested, ChargeCompleted],
|
|
1626
|
+
persistence: {
|
|
1627
|
+
deadLetter: { type: "redis", url: "redis://localhost:6379" },
|
|
1628
|
+
},
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
await source.connect();
|
|
1632
|
+
await source.setup();
|
|
1633
|
+
|
|
1634
|
+
// --- Observe lifecycle ---
|
|
1635
|
+
|
|
1636
|
+
const metricsSubscriber: IMessageSubscriber = {
|
|
1637
|
+
afterPublish: async (msg) => metrics.increment("messages.published"),
|
|
1638
|
+
afterConsume: async (msg) => metrics.increment("messages.consumed"),
|
|
1639
|
+
onConsumeError: async (err) => metrics.increment("messages.errors"),
|
|
1640
|
+
};
|
|
1641
|
+
|
|
1642
|
+
source.addSubscriber(metricsSubscriber);
|
|
1643
|
+
|
|
1644
|
+
// --- Worker: process charges ---
|
|
1645
|
+
|
|
1646
|
+
const queue = source.workerQueue(ChargeRequested);
|
|
1647
|
+
const completedPub = source.publisher(ChargeCompleted);
|
|
1648
|
+
|
|
1649
|
+
await queue.consume("payment-workers", async (msg, envelope) => {
|
|
1650
|
+
const result = await paymentGateway.charge(msg.paymentId, msg.amount, msg.currency);
|
|
1651
|
+
|
|
1652
|
+
const completed = completedPub.create({
|
|
1653
|
+
paymentId: msg.paymentId,
|
|
1654
|
+
success: result.ok,
|
|
1655
|
+
});
|
|
1656
|
+
|
|
1657
|
+
await completedPub.publish(completed);
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
// --- Notify on completion ---
|
|
1661
|
+
|
|
1662
|
+
const completedBus = source.messageBus(ChargeCompleted);
|
|
1663
|
+
|
|
1664
|
+
await completedBus.subscribe({
|
|
1665
|
+
topic: "ChargeCompleted",
|
|
1666
|
+
queue: "notification-service",
|
|
1667
|
+
callback: async (msg) => {
|
|
1668
|
+
if (msg.success) {
|
|
1669
|
+
await emailService.sendReceipt(msg.paymentId);
|
|
1670
|
+
}
|
|
1671
|
+
},
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
// --- Shutdown ---
|
|
1675
|
+
|
|
1676
|
+
process.on("SIGTERM", async () => {
|
|
1677
|
+
await source.drain();
|
|
1678
|
+
await source.disconnect();
|
|
1679
|
+
});
|
|
1680
|
+
```
|
|
1681
|
+
|
|
1682
|
+
## License
|
|
1683
|
+
|
|
1684
|
+
AGPL-3.0-or-later
|