@lindorm/iris 0.1.0 → 0.1.1
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 +989 -0
- package/package.json +19 -12
package/README.md
CHANGED
|
@@ -1 +1,990 @@
|
|
|
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
|
+
## Messaging Patterns
|
|
96
|
+
|
|
97
|
+
### Publisher (Fire-and-Forget)
|
|
98
|
+
|
|
99
|
+
Write-only. No subscriptions.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const pub = source.publisher(OrderPlaced);
|
|
103
|
+
|
|
104
|
+
const msg = pub.create({ orderId: "abc-123", total: 59.99 });
|
|
105
|
+
await pub.publish(msg);
|
|
106
|
+
|
|
107
|
+
// Batch publish
|
|
108
|
+
await pub.publish([msg1, msg2, msg3]);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Message Bus (Pub/Sub + Queues)
|
|
112
|
+
|
|
113
|
+
Publish with topic-based subscriptions. Supports broadcast and competing-consumer queues.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const bus = source.messageBus(OrderPlaced);
|
|
117
|
+
|
|
118
|
+
// Broadcast: every subscriber receives every message
|
|
119
|
+
await bus.subscribe({
|
|
120
|
+
topic: "OrderPlaced",
|
|
121
|
+
callback: async (msg) => {
|
|
122
|
+
/* ... */
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Queue: messages are distributed round-robin among consumers
|
|
127
|
+
await bus.subscribe({
|
|
128
|
+
topic: "OrderPlaced",
|
|
129
|
+
queue: "order-processors",
|
|
130
|
+
callback: async (msg) => {
|
|
131
|
+
/* ... */
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Multiple subscriptions at once
|
|
136
|
+
await bus.subscribe([
|
|
137
|
+
{ topic: "OrderPlaced", queue: "analytics", callback: handleAnalytics },
|
|
138
|
+
{ topic: "OrderPlaced", queue: "notifications", callback: handleNotify },
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
// Unsubscribe
|
|
142
|
+
await bus.unsubscribe({ topic: "OrderPlaced", queue: "analytics" });
|
|
143
|
+
await bus.unsubscribeAll();
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Worker Queue (Competing Consumers)
|
|
147
|
+
|
|
148
|
+
Specialised for job distribution where each message is processed by exactly one consumer.
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const queue = source.workerQueue(OrderPlaced);
|
|
152
|
+
|
|
153
|
+
// Register competing consumers
|
|
154
|
+
await queue.consume("process-orders", async (msg, envelope) => {
|
|
155
|
+
console.log(`Processing order ${msg.orderId} (attempt ${envelope.attempt})`);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Publish work
|
|
159
|
+
await queue.publish(queue.create({ orderId: "abc-123", total: 59.99 }));
|
|
160
|
+
|
|
161
|
+
// Clean up
|
|
162
|
+
await queue.unconsume("process-orders");
|
|
163
|
+
await queue.unconsumeAll();
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### RPC (Request/Response)
|
|
167
|
+
|
|
168
|
+
Synchronous request/response over the message broker.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
@Message()
|
|
172
|
+
class GetPrice {
|
|
173
|
+
@Field("string") sku!: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@Message()
|
|
177
|
+
class PriceResponse {
|
|
178
|
+
@Field("float") price!: number;
|
|
179
|
+
@Field("string") currency!: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const client = source.rpcClient(GetPrice, PriceResponse);
|
|
183
|
+
const server = source.rpcServer(GetPrice, PriceResponse);
|
|
184
|
+
|
|
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
|
+
});
|
|
192
|
+
|
|
193
|
+
// Client: send request
|
|
194
|
+
const req = new GetPrice();
|
|
195
|
+
req.sku = "WIDGET-42";
|
|
196
|
+
|
|
197
|
+
const res = await client.request(req);
|
|
198
|
+
console.log(`${res.price} ${res.currency}`); // 29.99 USD
|
|
199
|
+
|
|
200
|
+
// With timeout
|
|
201
|
+
const res2 = await client.request(req, { timeout: 5000 });
|
|
202
|
+
|
|
203
|
+
// Clean up
|
|
204
|
+
await client.close();
|
|
205
|
+
await server.unserveAll();
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Stream Processor (Pipelines)
|
|
209
|
+
|
|
210
|
+
Declarative stream processing with an immutable builder pattern.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
@Message()
|
|
214
|
+
class RawEvent {
|
|
215
|
+
@Field("string") type!: string;
|
|
216
|
+
@Field("float") value!: number;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@Message()
|
|
220
|
+
class AggregatedEvent {
|
|
221
|
+
@Field("float") sum!: number;
|
|
222
|
+
@Field("integer") count!: number;
|
|
223
|
+
}
|
|
224
|
+
|
|
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);
|
|
236
|
+
|
|
237
|
+
await pipeline.start();
|
|
238
|
+
// pipeline.isRunning() === true
|
|
239
|
+
|
|
240
|
+
await pipeline.pause();
|
|
241
|
+
await pipeline.resume();
|
|
242
|
+
await pipeline.stop();
|
|
243
|
+
```
|
|
244
|
+
|
|
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 |
|
|
296
|
+
|
|
297
|
+
## Field Types
|
|
298
|
+
|
|
299
|
+
The `@Field()` decorator accepts the following type identifiers:
|
|
300
|
+
|
|
301
|
+
`"array"` | `"bigint"` | `"boolean"` | `"date"` | `"email"` | `"enum"` | `"float"` | `"integer"` | `"object"` | `"string"` | `"url"` | `"uuid"`
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
@Message()
|
|
305
|
+
class FullExample {
|
|
306
|
+
@IdentifierField() id!: string;
|
|
307
|
+
@CorrelationField() correlationId!: string;
|
|
308
|
+
@TimestampField() createdAt!: Date;
|
|
309
|
+
|
|
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>;
|
|
320
|
+
|
|
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
|
+
```
|
|
327
|
+
|
|
328
|
+
## Retry and Dead Letter
|
|
329
|
+
|
|
330
|
+
Configure automatic retry with backoff strategies and dead letter routing for permanently failed messages.
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
@Retry({
|
|
334
|
+
maxRetries: 5,
|
|
335
|
+
strategy: "exponential", // "constant" | "linear" | "exponential"
|
|
336
|
+
delay: 1000, // initial delay in ms
|
|
337
|
+
delayMax: 30000, // maximum delay cap
|
|
338
|
+
multiplier: 2, // exponential multiplier
|
|
339
|
+
jitter: true, // add randomness to prevent thundering herd
|
|
340
|
+
})
|
|
341
|
+
@DeadLetter()
|
|
342
|
+
@Message()
|
|
343
|
+
class PaymentCharge {
|
|
344
|
+
@Field("string") chargeId!: string;
|
|
345
|
+
@Field("float") amount!: number;
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
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
|
+
## Dynamic Topics
|
|
358
|
+
|
|
359
|
+
Route messages to different topics based on their content:
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
@Topic((msg: any) => `events.${msg.region}.${msg.type}`)
|
|
363
|
+
@Message()
|
|
364
|
+
class RegionalEvent {
|
|
365
|
+
@Field("string") region!: string;
|
|
366
|
+
@Field("string") type!: string;
|
|
367
|
+
@Field("object") data!: Record<string, unknown>;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const bus = source.messageBus(RegionalEvent);
|
|
371
|
+
const msg = bus.create({ region: "eu-west", type: "signup", data: {} });
|
|
372
|
+
await bus.publish(msg); // Published to "events.eu-west.signup"
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Encryption and Compression
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
import { Encrypted, Compressed } from "@lindorm/iris";
|
|
379
|
+
|
|
380
|
+
@Encrypted() // Encrypt payload via amphora
|
|
381
|
+
@Compressed("brotli") // Then compress ("gzip" | "deflate" | "brotli")
|
|
382
|
+
@Message()
|
|
383
|
+
class SensitivePayload {
|
|
384
|
+
@Field("string") ssn!: string;
|
|
385
|
+
@Field("string") name!: string;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Source must be configured with an amphora instance
|
|
389
|
+
const source = new IrisSource({
|
|
390
|
+
driver: "rabbit",
|
|
391
|
+
url: "amqp://localhost",
|
|
392
|
+
logger: myLogger,
|
|
393
|
+
amphora: myAmphora,
|
|
394
|
+
messages: [SensitivePayload],
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Message Subscribers
|
|
399
|
+
|
|
400
|
+
Observe message lifecycle events across all messages in a source:
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
import type { IMessageSubscriber } from "@lindorm/iris";
|
|
404
|
+
|
|
405
|
+
const auditSubscriber: IMessageSubscriber = {
|
|
406
|
+
beforePublish: async (msg) => {
|
|
407
|
+
audit.log("publishing", msg);
|
|
408
|
+
},
|
|
409
|
+
afterConsume: async (msg) => {
|
|
410
|
+
audit.log("consumed", msg);
|
|
411
|
+
},
|
|
412
|
+
onConsumeError: async (error, msg) => {
|
|
413
|
+
audit.log("consume-failed", { error: error.message, msg });
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
source.addSubscriber(auditSubscriber);
|
|
418
|
+
|
|
419
|
+
// Remove later
|
|
420
|
+
source.removeSubscriber(auditSubscriber);
|
|
421
|
+
```
|
|
422
|
+
|
|
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
|
+
## Consume Envelope
|
|
442
|
+
|
|
443
|
+
Every subscribe/consume callback receives the message and an envelope with routing metadata:
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
import type { ConsumeEnvelope } from "@lindorm/iris";
|
|
447
|
+
|
|
448
|
+
await bus.subscribe({
|
|
449
|
+
topic: "OrderPlaced",
|
|
450
|
+
callback: async (msg: OrderPlaced, envelope: ConsumeEnvelope) => {
|
|
451
|
+
console.log(envelope.topic); // "OrderPlaced"
|
|
452
|
+
console.log(envelope.messageName); // "OrderPlaced"
|
|
453
|
+
console.log(envelope.namespace); // "orders" | null
|
|
454
|
+
console.log(envelope.version); // 1
|
|
455
|
+
console.log(envelope.headers); // Record<string, string>
|
|
456
|
+
console.log(envelope.attempt); // 1 (increments on retry)
|
|
457
|
+
console.log(envelope.correlationId); // string | null
|
|
458
|
+
console.log(envelope.timestamp); // Unix timestamp
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
## Cloning
|
|
464
|
+
|
|
465
|
+
Create independent source instances that share the underlying driver connection:
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
const source = new IrisSource({
|
|
469
|
+
driver: "rabbit",
|
|
470
|
+
url: "amqp://localhost",
|
|
471
|
+
logger: mainLogger,
|
|
472
|
+
messages: [OrderPlaced],
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
await source.connect();
|
|
476
|
+
await source.setup();
|
|
477
|
+
|
|
478
|
+
// Clone shares the connection but has its own logger and subscriber registry
|
|
479
|
+
const scoped = source.clone({ logger: requestLogger, context: { requestId: "abc" } });
|
|
480
|
+
|
|
481
|
+
const pub = scoped.publisher(OrderPlaced);
|
|
482
|
+
await pub.publish(pub.create({ orderId: "abc-123", total: 59.99 }));
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
## Driver Configuration
|
|
486
|
+
|
|
487
|
+
### Memory
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
const source = new IrisSource({
|
|
491
|
+
driver: "memory",
|
|
492
|
+
logger,
|
|
493
|
+
messages: [OrderPlaced],
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### RabbitMQ
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
const source = new IrisSource({
|
|
501
|
+
driver: "rabbit",
|
|
502
|
+
url: "amqp://localhost",
|
|
503
|
+
logger,
|
|
504
|
+
messages: [OrderPlaced],
|
|
505
|
+
exchange: "my-exchange", // optional
|
|
506
|
+
prefetch: 10, // optional
|
|
507
|
+
connection: {
|
|
508
|
+
// optional
|
|
509
|
+
heartbeat: 60,
|
|
510
|
+
socketOptions: {
|
|
511
|
+
timeout: 30000,
|
|
512
|
+
keepAlive: true,
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Kafka
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
const source = new IrisSource({
|
|
522
|
+
driver: "kafka",
|
|
523
|
+
brokers: ["localhost:9092"],
|
|
524
|
+
logger,
|
|
525
|
+
messages: [OrderPlaced],
|
|
526
|
+
prefix: "my-app", // optional topic prefix
|
|
527
|
+
prefetch: 100, // optional
|
|
528
|
+
acks: -1, // optional: -1 (all), 0 (none), 1 (leader)
|
|
529
|
+
sessionTimeoutMs: 30000, // optional
|
|
530
|
+
connection: {
|
|
531
|
+
// optional
|
|
532
|
+
clientId: "my-service",
|
|
533
|
+
ssl: true,
|
|
534
|
+
sasl: {
|
|
535
|
+
mechanism: "scram-sha-256",
|
|
536
|
+
username: "user",
|
|
537
|
+
password: "pass",
|
|
538
|
+
},
|
|
539
|
+
connectionTimeout: 10000,
|
|
540
|
+
requestTimeout: 30000,
|
|
541
|
+
retry: {
|
|
542
|
+
retries: 5,
|
|
543
|
+
initialRetryTime: 300,
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### NATS
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
const source = new IrisSource({
|
|
553
|
+
driver: "nats",
|
|
554
|
+
servers: "nats://localhost:4222", // string or Array<string>
|
|
555
|
+
logger,
|
|
556
|
+
messages: [OrderPlaced],
|
|
557
|
+
prefix: "my-app", // optional
|
|
558
|
+
prefetch: 50, // optional
|
|
559
|
+
connection: {
|
|
560
|
+
// optional
|
|
561
|
+
user: "nats-user",
|
|
562
|
+
pass: "nats-pass",
|
|
563
|
+
tls: true,
|
|
564
|
+
maxReconnectAttempts: 10,
|
|
565
|
+
reconnectTimeWait: 2000,
|
|
566
|
+
timeout: 10000,
|
|
567
|
+
pingInterval: 30000,
|
|
568
|
+
name: "my-service",
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### Redis Streams
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
const source = new IrisSource({
|
|
577
|
+
driver: "redis",
|
|
578
|
+
url: "redis://localhost:6379",
|
|
579
|
+
logger,
|
|
580
|
+
messages: [OrderPlaced],
|
|
581
|
+
prefix: "my-app", // optional
|
|
582
|
+
prefetch: 50, // optional
|
|
583
|
+
blockMs: 5000, // optional: XREAD block time
|
|
584
|
+
maxStreamLength: 10000, // optional: MAXLEN cap per stream
|
|
585
|
+
connection: {
|
|
586
|
+
// optional
|
|
587
|
+
host: "redis.internal",
|
|
588
|
+
port: 6379,
|
|
589
|
+
password: "secret",
|
|
590
|
+
db: 0,
|
|
591
|
+
tls: {},
|
|
592
|
+
connectTimeout: 10000,
|
|
593
|
+
commandTimeout: 5000,
|
|
594
|
+
keepAlive: 30000,
|
|
595
|
+
connectionName: "iris-worker",
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
## Persistence (Delay and Dead Letter Stores)
|
|
601
|
+
|
|
602
|
+
Configure where delayed messages and dead letter entries are stored:
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
const source = new IrisSource({
|
|
606
|
+
driver: "rabbit",
|
|
607
|
+
url: "amqp://localhost",
|
|
608
|
+
logger,
|
|
609
|
+
messages: [OrderPlaced],
|
|
610
|
+
persistence: {
|
|
611
|
+
// Delay store: holds messages until their scheduled delivery time
|
|
612
|
+
delay: { type: "memory" },
|
|
613
|
+
// or: { type: "redis", url: "redis://localhost:6379" },
|
|
614
|
+
// or: { type: "custom", store: myDelayStore },
|
|
615
|
+
|
|
616
|
+
// Dead letter store: holds messages that exhausted all retries
|
|
617
|
+
deadLetter: { type: "memory" },
|
|
618
|
+
// or: { type: "redis", url: "redis://localhost:6379" },
|
|
619
|
+
// or: { type: "custom", store: myDeadLetterStore },
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Custom Stores
|
|
625
|
+
|
|
626
|
+
Implement `IDelayStore` and/or `IDeadLetterStore` for custom persistence:
|
|
627
|
+
|
|
628
|
+
```typescript
|
|
629
|
+
import type {
|
|
630
|
+
IDelayStore,
|
|
631
|
+
IDeadLetterStore,
|
|
632
|
+
DelayedEntry,
|
|
633
|
+
DeadLetterEntry,
|
|
634
|
+
} from "@lindorm/iris";
|
|
635
|
+
|
|
636
|
+
class MyDelayStore implements IDelayStore {
|
|
637
|
+
async schedule(entry: DelayedEntry): Promise<void> {
|
|
638
|
+
/* ... */
|
|
639
|
+
}
|
|
640
|
+
async poll(now: number): Promise<Array<DelayedEntry>> {
|
|
641
|
+
/* ... */
|
|
642
|
+
}
|
|
643
|
+
async cancel(id: string): Promise<boolean> {
|
|
644
|
+
/* ... */
|
|
645
|
+
}
|
|
646
|
+
async size(): Promise<number> {
|
|
647
|
+
/* ... */
|
|
648
|
+
}
|
|
649
|
+
async clear(): Promise<void> {
|
|
650
|
+
/* ... */
|
|
651
|
+
}
|
|
652
|
+
async close(): Promise<void> {
|
|
653
|
+
/* ... */
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
class MyDeadLetterStore implements IDeadLetterStore {
|
|
658
|
+
async add(entry: DeadLetterEntry): Promise<void> {
|
|
659
|
+
/* ... */
|
|
660
|
+
}
|
|
661
|
+
async list(options?: {
|
|
662
|
+
topic?: string;
|
|
663
|
+
limit?: number;
|
|
664
|
+
offset?: number;
|
|
665
|
+
}): Promise<Array<DeadLetterEntry>> {
|
|
666
|
+
/* ... */
|
|
667
|
+
}
|
|
668
|
+
async get(id: string): Promise<DeadLetterEntry | null> {
|
|
669
|
+
/* ... */
|
|
670
|
+
}
|
|
671
|
+
async remove(id: string): Promise<boolean> {
|
|
672
|
+
/* ... */
|
|
673
|
+
}
|
|
674
|
+
async purge(options?: { topic?: string }): Promise<number> {
|
|
675
|
+
/* ... */
|
|
676
|
+
}
|
|
677
|
+
async count(options?: { topic?: string }): Promise<number> {
|
|
678
|
+
/* ... */
|
|
679
|
+
}
|
|
680
|
+
async close(): Promise<void> {
|
|
681
|
+
/* ... */
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
## Connection State
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
const state = source.getConnectionState();
|
|
690
|
+
// "disconnected" | "connecting" | "connected" | "reconnecting" | "draining"
|
|
691
|
+
|
|
692
|
+
source.onConnectionStateChange((state) => {
|
|
693
|
+
console.log(`Connection state: ${state}`);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// Health check
|
|
697
|
+
const healthy = await source.ping();
|
|
698
|
+
```
|
|
699
|
+
|
|
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
|
+
## Testing with Mocks
|
|
760
|
+
|
|
761
|
+
All mocks are available via the `@lindorm/iris/mocks` subpath:
|
|
762
|
+
|
|
763
|
+
```typescript
|
|
764
|
+
import {
|
|
765
|
+
createMockIrisSource,
|
|
766
|
+
createMockPublisher,
|
|
767
|
+
createMockMessageBus,
|
|
768
|
+
createMockWorkerQueue,
|
|
769
|
+
createMockRpcClient,
|
|
770
|
+
} from "@lindorm/iris/mocks";
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### Mock Source
|
|
774
|
+
|
|
775
|
+
```typescript
|
|
776
|
+
const source = createMockIrisSource();
|
|
777
|
+
|
|
778
|
+
// All methods are jest.fn() mocks
|
|
779
|
+
expect(source.connect).not.toHaveBeenCalled();
|
|
780
|
+
|
|
781
|
+
await source.connect();
|
|
782
|
+
expect(source.connect).toHaveBeenCalledTimes(1);
|
|
783
|
+
|
|
784
|
+
// Factory methods return mocks by default
|
|
785
|
+
const bus = source.messageBus(OrderPlaced);
|
|
786
|
+
const pub = source.publisher(OrderPlaced);
|
|
787
|
+
const queue = source.workerQueue(OrderPlaced);
|
|
788
|
+
const rpc = source.rpcClient(GetPrice, PriceResponse);
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
### Mock Publisher
|
|
792
|
+
|
|
793
|
+
```typescript
|
|
794
|
+
const pub = createMockPublisher<OrderPlaced>();
|
|
795
|
+
|
|
796
|
+
const msg = pub.create({ orderId: "abc", total: 10 });
|
|
797
|
+
await pub.publish(msg);
|
|
798
|
+
|
|
799
|
+
// Inspect published messages
|
|
800
|
+
expect(pub.published).toHaveLength(1);
|
|
801
|
+
|
|
802
|
+
// Reset
|
|
803
|
+
pub.clearPublished();
|
|
804
|
+
expect(pub.published).toHaveLength(0);
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
### Mock Message Bus
|
|
808
|
+
|
|
809
|
+
```typescript
|
|
810
|
+
const bus = createMockMessageBus<OrderPlaced>();
|
|
811
|
+
|
|
812
|
+
await bus.publish(bus.create({ orderId: "abc", total: 10 }));
|
|
813
|
+
|
|
814
|
+
expect(bus.published).toHaveLength(1);
|
|
815
|
+
expect(bus.subscribe).not.toHaveBeenCalled();
|
|
816
|
+
|
|
817
|
+
bus.clearPublished();
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
### Mock Worker Queue
|
|
821
|
+
|
|
822
|
+
```typescript
|
|
823
|
+
const queue = createMockWorkerQueue<OrderPlaced>();
|
|
824
|
+
|
|
825
|
+
await queue.publish(queue.create({ orderId: "abc", total: 10 }));
|
|
826
|
+
|
|
827
|
+
expect(queue.published).toHaveLength(1);
|
|
828
|
+
expect(queue.consume).not.toHaveBeenCalled();
|
|
829
|
+
|
|
830
|
+
queue.clearPublished();
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
### Mock RPC Client
|
|
834
|
+
|
|
835
|
+
```typescript
|
|
836
|
+
// Provide a response factory
|
|
837
|
+
const client = createMockRpcClient<GetPrice, PriceResponse>((req) => {
|
|
838
|
+
const res = new PriceResponse();
|
|
839
|
+
res.price = 42.0;
|
|
840
|
+
res.currency = "USD";
|
|
841
|
+
return res;
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
const req = new GetPrice();
|
|
845
|
+
req.sku = "WIDGET-42";
|
|
846
|
+
|
|
847
|
+
const res = await client.request(req);
|
|
848
|
+
expect(res.price).toBe(42.0);
|
|
849
|
+
expect(client.requests).toHaveLength(1);
|
|
850
|
+
|
|
851
|
+
client.clearRequests();
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
## Error Classes
|
|
855
|
+
|
|
856
|
+
All errors extend `IrisError`, which extends `LindormError`:
|
|
857
|
+
|
|
858
|
+
| Error Class | When |
|
|
859
|
+
| ------------------------ | ----------------------------------------- |
|
|
860
|
+
| `IrisError` | Base class for all iris errors |
|
|
861
|
+
| `IrisDriverError` | Driver connection or operation failure |
|
|
862
|
+
| `IrisMetadataError` | Invalid decorator configuration |
|
|
863
|
+
| `IrisNotSupportedError` | Unsupported feature for the active driver |
|
|
864
|
+
| `IrisPublishError` | Message publishing failure |
|
|
865
|
+
| `IrisScannerError` | Message class scanning failure |
|
|
866
|
+
| `IrisSerializationError` | Serialisation or deserialisation failure |
|
|
867
|
+
| `IrisSourceError` | Source setup or configuration error |
|
|
868
|
+
| `IrisTimeoutError` | Operation exceeded timeout |
|
|
869
|
+
| `IrisTransportError` | Transport layer failure |
|
|
870
|
+
| `IrisValidationError` | Message validation failure |
|
|
871
|
+
|
|
872
|
+
```typescript
|
|
873
|
+
import { IrisTimeoutError, IrisValidationError } from "@lindorm/iris";
|
|
874
|
+
|
|
875
|
+
try {
|
|
876
|
+
await client.request(req, { timeout: 1000 });
|
|
877
|
+
} catch (error) {
|
|
878
|
+
if (error instanceof IrisTimeoutError) {
|
|
879
|
+
// handle timeout
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
## Full Example
|
|
885
|
+
|
|
886
|
+
```typescript
|
|
887
|
+
import {
|
|
888
|
+
IrisSource,
|
|
889
|
+
Message,
|
|
890
|
+
Namespace,
|
|
891
|
+
Version,
|
|
892
|
+
Field,
|
|
893
|
+
IdentifierField,
|
|
894
|
+
TimestampField,
|
|
895
|
+
Retry,
|
|
896
|
+
DeadLetter,
|
|
897
|
+
} from "@lindorm/iris";
|
|
898
|
+
import type { IMessage, IMessageSubscriber } from "@lindorm/iris";
|
|
899
|
+
|
|
900
|
+
// --- Define messages ---
|
|
901
|
+
|
|
902
|
+
@Message()
|
|
903
|
+
@Namespace("payments")
|
|
904
|
+
@Version(1)
|
|
905
|
+
@Retry({ maxRetries: 3, strategy: "exponential", delay: 1000 })
|
|
906
|
+
@DeadLetter()
|
|
907
|
+
class ChargeRequested {
|
|
908
|
+
@IdentifierField() id!: string;
|
|
909
|
+
@TimestampField() createdAt!: Date;
|
|
910
|
+
@Field("string") paymentId!: string;
|
|
911
|
+
@Field("float") amount!: number;
|
|
912
|
+
@Field("string") currency!: string;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
@Message()
|
|
916
|
+
@Namespace("payments")
|
|
917
|
+
@Version(1)
|
|
918
|
+
class ChargeCompleted {
|
|
919
|
+
@IdentifierField() id!: string;
|
|
920
|
+
@TimestampField() completedAt!: Date;
|
|
921
|
+
@Field("string") paymentId!: string;
|
|
922
|
+
@Field("boolean") success!: boolean;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// --- Set up source ---
|
|
926
|
+
|
|
927
|
+
const source = new IrisSource({
|
|
928
|
+
driver: "kafka",
|
|
929
|
+
brokers: ["kafka-1:9092", "kafka-2:9092"],
|
|
930
|
+
logger: appLogger,
|
|
931
|
+
messages: [ChargeRequested, ChargeCompleted],
|
|
932
|
+
persistence: {
|
|
933
|
+
deadLetter: { type: "redis", url: "redis://localhost:6379" },
|
|
934
|
+
},
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
await source.connect();
|
|
938
|
+
await source.setup();
|
|
939
|
+
|
|
940
|
+
// --- Observe lifecycle ---
|
|
941
|
+
|
|
942
|
+
const metricsSubscriber: IMessageSubscriber = {
|
|
943
|
+
afterPublish: async (msg) => metrics.increment("messages.published"),
|
|
944
|
+
afterConsume: async (msg) => metrics.increment("messages.consumed"),
|
|
945
|
+
onConsumeError: async (err) => metrics.increment("messages.errors"),
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
source.addSubscriber(metricsSubscriber);
|
|
949
|
+
|
|
950
|
+
// --- Worker: process charges ---
|
|
951
|
+
|
|
952
|
+
const queue = source.workerQueue(ChargeRequested);
|
|
953
|
+
const completedPub = source.publisher(ChargeCompleted);
|
|
954
|
+
|
|
955
|
+
await queue.consume("payment-workers", async (msg, envelope) => {
|
|
956
|
+
const result = await paymentGateway.charge(msg.paymentId, msg.amount, msg.currency);
|
|
957
|
+
|
|
958
|
+
const completed = completedPub.create({
|
|
959
|
+
paymentId: msg.paymentId,
|
|
960
|
+
success: result.ok,
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
await completedPub.publish(completed);
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
// --- Notify on completion ---
|
|
967
|
+
|
|
968
|
+
const completedBus = source.messageBus(ChargeCompleted);
|
|
969
|
+
|
|
970
|
+
await completedBus.subscribe({
|
|
971
|
+
topic: "ChargeCompleted",
|
|
972
|
+
queue: "notification-service",
|
|
973
|
+
callback: async (msg) => {
|
|
974
|
+
if (msg.success) {
|
|
975
|
+
await emailService.sendReceipt(msg.paymentId);
|
|
976
|
+
}
|
|
977
|
+
},
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
// --- Shutdown ---
|
|
981
|
+
|
|
982
|
+
process.on("SIGTERM", async () => {
|
|
983
|
+
await source.drain();
|
|
984
|
+
await source.disconnect();
|
|
985
|
+
});
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
## License
|
|
989
|
+
|
|
990
|
+
AGPL-3.0-or-later
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lindorm/iris",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"license": "AGPL-3.0-or-later",
|
|
5
5
|
"author": "Jonn Nilsson",
|
|
6
6
|
"repository": {
|
|
@@ -26,6 +26,13 @@
|
|
|
26
26
|
"default": "./dist/mocks/index.js"
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
|
+
"typesVersions": {
|
|
30
|
+
"*": {
|
|
31
|
+
"mocks": [
|
|
32
|
+
"dist/mocks/index.d.ts"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
29
36
|
"imports": {
|
|
30
37
|
"#internal/*": "./dist/internal/*.js"
|
|
31
38
|
},
|
|
@@ -44,20 +51,20 @@
|
|
|
44
51
|
"verify": "npm run typecheck; npm run build; npm test"
|
|
45
52
|
},
|
|
46
53
|
"dependencies": {
|
|
47
|
-
"@lindorm/aes": "^0.6.
|
|
48
|
-
"@lindorm/amphora": "^0.3.
|
|
49
|
-
"@lindorm/errors": "^0.1.
|
|
50
|
-
"@lindorm/is": "^0.1.
|
|
51
|
-
"@lindorm/json-kit": "^0.5.
|
|
52
|
-
"@lindorm/logger": "^0.5.
|
|
54
|
+
"@lindorm/aes": "^0.6.3",
|
|
55
|
+
"@lindorm/amphora": "^0.3.4",
|
|
56
|
+
"@lindorm/errors": "^0.1.16",
|
|
57
|
+
"@lindorm/is": "^0.1.14",
|
|
58
|
+
"@lindorm/json-kit": "^0.5.6",
|
|
59
|
+
"@lindorm/logger": "^0.5.2",
|
|
53
60
|
"@lindorm/random": "^0.2.2",
|
|
54
|
-
"@lindorm/retry": "^0.2.
|
|
55
|
-
"@lindorm/scanner": "^0.4.
|
|
61
|
+
"@lindorm/retry": "^0.2.1",
|
|
62
|
+
"@lindorm/scanner": "^0.4.2",
|
|
56
63
|
"zod": "^4.3.6"
|
|
57
64
|
},
|
|
58
65
|
"devDependencies": {
|
|
59
|
-
"@lindorm/kryptos": "^0.5.
|
|
60
|
-
"@lindorm/types": "^0.4.
|
|
66
|
+
"@lindorm/kryptos": "^0.5.3",
|
|
67
|
+
"@lindorm/types": "^0.4.1",
|
|
61
68
|
"@types/amqplib": "^0.10.8",
|
|
62
69
|
"amqplib": "^0.10.9",
|
|
63
70
|
"ioredis": "^5.10.0",
|
|
@@ -84,5 +91,5 @@
|
|
|
84
91
|
"optional": true
|
|
85
92
|
}
|
|
86
93
|
},
|
|
87
|
-
"gitHead": "
|
|
94
|
+
"gitHead": "a771f3669e540fb78fecf0ffc0e58e0f417f086c"
|
|
88
95
|
}
|