@horizon-republic/nestjs-jetstream 2.3.6 → 2.5.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 +21 -996
- package/dist/index.cjs +294 -65
- package/dist/index.d.cts +116 -31
- package/dist/index.d.ts +116 -31
- package/dist/index.js +299 -63
- package/package.json +6 -4
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,1036 +1,61 @@
|
|
|
1
1
|
# @horizon-republic/nestjs-jetstream
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Ship reliable microservices with NATS JetStream and NestJS. Events, broadcast, ordered delivery, and RPC — with two lines of config.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@horizon-republic/nestjs-jetstream)
|
|
6
6
|
[](https://codecov.io/github/HorizonRepublic/nestjs-jetstream)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
[](https://socket.dev/npm/package/@horizon-republic/nestjs-jetstream)
|
|
9
9
|
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
- [Features](#features)
|
|
13
|
-
- [Installation](#installation)
|
|
14
|
-
- [Quick Start](#quick-start)
|
|
15
|
-
- [Module Configuration](#module-configuration)
|
|
16
|
-
- [forRoot / forRootAsync](#forroot--forrootasync)
|
|
17
|
-
- [forFeature](#forfeature)
|
|
18
|
-
- [Full Options Reference](#full-options-reference)
|
|
19
|
-
- [Messaging Patterns](#messaging-patterns)
|
|
20
|
-
- [RPC (Request/Reply)](#rpc-requestreply)
|
|
21
|
-
- [Events](#events)
|
|
22
|
-
- [JetstreamRecord Builder](#jetstreamrecord-builder)
|
|
23
|
-
- [Handler Context & Serialization](#handler-context--serialization)
|
|
24
|
-
- [RpcContext](#rpccontext)
|
|
25
|
-
- [Custom Codec](#custom-codec)
|
|
26
|
-
- [Operations](#operations)
|
|
27
|
-
- [Lifecycle Hooks](#lifecycle-hooks)
|
|
28
|
-
- [Health Checks](#health-checks)
|
|
29
|
-
- [Graceful Shutdown](#graceful-shutdown)
|
|
30
|
-
- [Reference](#reference)
|
|
31
|
-
- [Edge Cases & Important Notes](#edge-cases--important-notes)
|
|
32
|
-
- [NATS Naming Conventions](#nats-naming-conventions)
|
|
33
|
-
- [Default Stream & Consumer Configs](#default-stream--consumer-configs)
|
|
34
|
-
- [API Reference](#api-reference)
|
|
35
|
-
- [Development](#development)
|
|
36
|
-
- [Testing](#testing)
|
|
37
|
-
- [Contributing](#contributing)
|
|
38
|
-
- [License](#license)
|
|
39
|
-
- [Links](#links)
|
|
40
|
-
|
|
41
|
-
## Features
|
|
42
|
-
|
|
43
|
-
- **Two RPC modes** — NATS Core request/reply (lowest latency) or JetStream-persisted commands
|
|
44
|
-
- **At-least-once event delivery** — messages acked after handler success, redelivered on failure
|
|
45
|
-
- **Broadcast events** — fan-out to all subscribing services with per-service durable consumers
|
|
46
|
-
- **Pluggable codec** — JSON by default, swap in MessagePack, Protobuf, or any custom format
|
|
47
|
-
- **Progressive configuration** — two lines to start, full NATS overrides for power users
|
|
48
|
-
- **Lifecycle hooks** — observable events for connect, disconnect, errors, timeouts, shutdown
|
|
49
|
-
- **Graceful shutdown** — drain in-flight messages before closing the connection
|
|
50
|
-
- **Publisher-only mode** — set `consumer: false` for API gateways that only send messages
|
|
51
|
-
- **Per-feature codec override** — different serialization per target service
|
|
52
|
-
|
|
53
|
-
## Installation
|
|
10
|
+
## Quick Start
|
|
54
11
|
|
|
55
12
|
```bash
|
|
56
13
|
npm install @horizon-republic/nestjs-jetstream
|
|
57
|
-
# or
|
|
58
|
-
pnpm add @horizon-republic/nestjs-jetstream
|
|
59
|
-
# or
|
|
60
|
-
yarn add @horizon-republic/nestjs-jetstream
|
|
61
14
|
```
|
|
62
15
|
|
|
63
|
-
**Peer dependencies:**
|
|
64
|
-
|
|
65
|
-
```
|
|
66
|
-
@nestjs/common ^10.2.0 || ^11.0.0
|
|
67
|
-
@nestjs/core ^10.2.0 || ^11.0.0
|
|
68
|
-
@nestjs/microservices ^10.2.0 || ^11.0.0
|
|
69
|
-
nats ^2.0.0
|
|
70
|
-
reflect-metadata ^0.2.0
|
|
71
|
-
rxjs ^7.8.0
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Quick Start
|
|
75
|
-
|
|
76
|
-
### 1. Register the module
|
|
77
|
-
|
|
78
16
|
```typescript
|
|
79
17
|
// app.module.ts
|
|
80
|
-
import { Module } from '@nestjs/common';
|
|
81
|
-
import { JetstreamModule } from '@horizon-republic/nestjs-jetstream';
|
|
82
|
-
|
|
83
18
|
@Module({
|
|
84
19
|
imports: [
|
|
85
|
-
|
|
86
|
-
JetstreamModule.forRoot({
|
|
87
|
-
name: 'orders',
|
|
88
|
-
servers: ['nats://localhost:4222'],
|
|
89
|
-
}),
|
|
90
|
-
|
|
91
|
-
// Client for sending messages to the "orders" service
|
|
20
|
+
JetstreamModule.forRoot({ name: 'orders', servers: ['nats://localhost:4222'] }),
|
|
92
21
|
JetstreamModule.forFeature({ name: 'orders' }),
|
|
93
22
|
],
|
|
94
23
|
})
|
|
95
24
|
export class AppModule {}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### 2. Connect the transport
|
|
99
|
-
|
|
100
|
-
```typescript
|
|
101
|
-
// main.ts
|
|
102
|
-
import { NestFactory } from '@nestjs/core';
|
|
103
|
-
import { JetstreamStrategy } from '@horizon-republic/nestjs-jetstream';
|
|
104
|
-
import { AppModule } from './app.module';
|
|
105
|
-
|
|
106
|
-
const bootstrap = async () => {
|
|
107
|
-
const app = await NestFactory.create(AppModule);
|
|
108
|
-
|
|
109
|
-
app.connectMicroservice(
|
|
110
|
-
{ strategy: app.get(JetstreamStrategy) },
|
|
111
|
-
{ inheritAppConfig: true },
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
await app.startAllMicroservices();
|
|
115
|
-
await app.listen(3000);
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
void bootstrap();
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### 3. Define handlers
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
import { Controller } from '@nestjs/common';
|
|
125
|
-
import { EventPattern, MessagePattern, Payload } from '@nestjs/microservices';
|
|
126
25
|
|
|
26
|
+
// orders.controller.ts
|
|
127
27
|
@Controller()
|
|
128
28
|
export class OrdersController {
|
|
29
|
+
constructor(@Inject('orders') private client: ClientProxy) {}
|
|
30
|
+
|
|
129
31
|
@EventPattern('order.created')
|
|
130
|
-
|
|
32
|
+
handle(@Payload() data: { orderId: number }) {
|
|
131
33
|
console.log('Order created:', data.orderId);
|
|
132
34
|
}
|
|
133
35
|
|
|
134
|
-
@
|
|
135
|
-
|
|
136
|
-
return { id: data.id, status: 'shipped' };
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
### 4. Send messages
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
import { Controller, Get, Inject } from '@nestjs/common';
|
|
145
|
-
import { ClientProxy } from '@nestjs/microservices';
|
|
146
|
-
|
|
147
|
-
@Controller()
|
|
148
|
-
export class AppController {
|
|
149
|
-
constructor(@Inject('orders') private client: ClientProxy) {}
|
|
150
|
-
|
|
151
|
-
@Get('create')
|
|
152
|
-
createOrder() {
|
|
36
|
+
@Get('emit')
|
|
37
|
+
emit() {
|
|
153
38
|
return this.client.emit('order.created', { orderId: 42 });
|
|
154
39
|
}
|
|
155
|
-
|
|
156
|
-
@Get('get')
|
|
157
|
-
getOrder() {
|
|
158
|
-
return this.client.send('order.get', { id: 42 });
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
## Module Configuration
|
|
164
|
-
|
|
165
|
-
### forRoot / forRootAsync
|
|
166
|
-
|
|
167
|
-
`forRoot()` registers the transport globally. Call it once in your root `AppModule`.
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
JetstreamModule.forRoot({
|
|
171
|
-
name: 'orders',
|
|
172
|
-
servers: ['nats://localhost:4222'],
|
|
173
|
-
rpc: { mode: 'core', timeout: 10_000 },
|
|
174
|
-
shutdownTimeout: 15_000,
|
|
175
|
-
hooks: {
|
|
176
|
-
[TransportEvent.Error]: (err, ctx) => sentry.captureException(err),
|
|
177
|
-
},
|
|
178
|
-
})
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
For async configuration (e.g., loading from `ConfigService`):
|
|
182
|
-
|
|
183
|
-
```typescript
|
|
184
|
-
JetstreamModule.forRootAsync({
|
|
185
|
-
name: 'orders',
|
|
186
|
-
imports: [ConfigModule],
|
|
187
|
-
inject: [ConfigService],
|
|
188
|
-
useFactory: (config: ConfigService) => ({
|
|
189
|
-
servers: [config.get('NATS_URL')],
|
|
190
|
-
rpc: { mode: config.get('RPC_MODE') as 'core' | 'jetstream' },
|
|
191
|
-
}),
|
|
192
|
-
})
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
Also supports `useExisting` and `useClass` patterns.
|
|
196
|
-
|
|
197
|
-
### forFeature
|
|
198
|
-
|
|
199
|
-
`forFeature()` creates a lightweight client for a target service. Import in each feature module.
|
|
200
|
-
|
|
201
|
-
```typescript
|
|
202
|
-
// The client reuses the NATS connection from forRoot().
|
|
203
|
-
// No separate connection is created.
|
|
204
|
-
JetstreamModule.forFeature({ name: 'users' })
|
|
205
|
-
JetstreamModule.forFeature({ name: 'payments' })
|
|
206
|
-
|
|
207
|
-
// Optionally override the codec for a specific client
|
|
208
|
-
JetstreamModule.forFeature({ name: 'legacy-service', codec: new MsgPackCodec() })
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
Inject clients by the service name:
|
|
212
|
-
|
|
213
|
-
```typescript
|
|
214
|
-
constructor(
|
|
215
|
-
@Inject('users') private usersClient: ClientProxy,
|
|
216
|
-
@Inject('payments') private paymentsClient: ClientProxy,
|
|
217
|
-
) {}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### Full Options Reference
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
interface JetstreamModuleOptions {
|
|
224
|
-
/** Service name. Used for stream/consumer/subject naming. */
|
|
225
|
-
name: string;
|
|
226
|
-
|
|
227
|
-
/** NATS server URLs. */
|
|
228
|
-
servers: string[];
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Global message codec.
|
|
232
|
-
* @default JsonCodec
|
|
233
|
-
*/
|
|
234
|
-
codec?: Codec;
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* RPC transport mode.
|
|
238
|
-
* @default { mode: 'core' }
|
|
239
|
-
*/
|
|
240
|
-
rpc?: RpcConfig;
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Enable consumer infrastructure (streams, consumers, message routing).
|
|
244
|
-
* Set to false for publisher-only services (e.g., API gateways).
|
|
245
|
-
* @default true
|
|
246
|
-
*/
|
|
247
|
-
consumer?: boolean;
|
|
248
|
-
|
|
249
|
-
/** Workqueue event stream/consumer overrides. */
|
|
250
|
-
events?: { stream?: Partial<StreamConfig>; consumer?: Partial<ConsumerConfig> };
|
|
251
|
-
|
|
252
|
-
/** Broadcast event stream/consumer overrides. */
|
|
253
|
-
broadcast?: { stream?: Partial<StreamConfig>; consumer?: Partial<ConsumerConfig> };
|
|
254
|
-
|
|
255
|
-
/** Transport lifecycle hook handlers. Unset hooks are silently ignored. */
|
|
256
|
-
hooks?: Partial<TransportHooks>;
|
|
257
|
-
|
|
258
|
-
/** Async callback for dead letter handling. See Dead Letter Queue section below. */
|
|
259
|
-
onDeadLetter?: (info: DeadLetterInfo) => Promise<void>;
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Graceful shutdown timeout in ms.
|
|
263
|
-
* @default 10_000
|
|
264
|
-
*/
|
|
265
|
-
shutdownTimeout?: number;
|
|
266
|
-
|
|
267
|
-
/** Raw NATS ConnectionOptions pass-through (tls, auth, reconnect, etc.). */
|
|
268
|
-
connectionOptions?: Partial<ConnectionOptions>;
|
|
269
|
-
}
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
#### Connection Options
|
|
273
|
-
|
|
274
|
-
Pass raw NATS `ConnectionOptions` for TLS, authentication, and reconnection:
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
JetstreamModule.forRoot({
|
|
278
|
-
name: 'orders',
|
|
279
|
-
servers: ['nats://nats.prod.internal:4222'],
|
|
280
|
-
connectionOptions: {
|
|
281
|
-
// TLS
|
|
282
|
-
tls: {
|
|
283
|
-
certFile: '/certs/client.crt',
|
|
284
|
-
keyFile: '/certs/client.key',
|
|
285
|
-
caFile: '/certs/ca.crt',
|
|
286
|
-
},
|
|
287
|
-
// Token auth
|
|
288
|
-
token: process.env.NATS_TOKEN,
|
|
289
|
-
// Or user/pass
|
|
290
|
-
user: process.env.NATS_USER,
|
|
291
|
-
pass: process.env.NATS_PASS,
|
|
292
|
-
// Reconnection
|
|
293
|
-
maxReconnectAttempts: -1, // unlimited
|
|
294
|
-
reconnectTimeWait: 2000, // 2s between attempts
|
|
295
|
-
},
|
|
296
|
-
})
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
#### RpcConfig
|
|
300
|
-
|
|
301
|
-
Discriminated union on `mode`:
|
|
302
|
-
|
|
303
|
-
| Mode | Timeout Default | Persistence | Use Case |
|
|
304
|
-
|---------------|-----------------|------------------|----------------------------------------|
|
|
305
|
-
| `'core'` | 30s | None | Low-latency, simple RPC |
|
|
306
|
-
| `'jetstream'` | 3 min | JetStream stream | Commands must survive handler downtime |
|
|
307
|
-
|
|
308
|
-
> **Note:** `timeout` controls both the **client-side wait** (how long the caller waits for a response) and the **server-side handler limit** (how long the handler is allowed to run before being terminated). Both sides use the same value from their own `forRoot()` config.
|
|
309
|
-
|
|
310
|
-
```typescript
|
|
311
|
-
// Core mode (default)
|
|
312
|
-
rpc: { mode: 'core', timeout: 10_000 }
|
|
313
|
-
|
|
314
|
-
// JetStream mode with custom stream/consumer config
|
|
315
|
-
rpc: {
|
|
316
|
-
mode: 'jetstream',
|
|
317
|
-
timeout: 60_000,
|
|
318
|
-
stream: { max_age: nanos(60_000) },
|
|
319
|
-
consumer: { max_deliver: 3 },
|
|
320
|
-
}
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
## Messaging Patterns
|
|
324
|
-
|
|
325
|
-
### RPC (Request/Reply)
|
|
326
|
-
|
|
327
|
-
#### Core Mode (Default)
|
|
328
|
-
|
|
329
|
-
Uses NATS native `request/reply` for the lowest possible latency.
|
|
330
|
-
|
|
331
|
-
```typescript
|
|
332
|
-
// Configuration
|
|
333
|
-
JetstreamModule.forRoot({
|
|
334
|
-
name: 'orders',
|
|
335
|
-
servers: ['nats://localhost:4222'],
|
|
336
|
-
// rpc: { mode: 'core' } ← default, can be omitted
|
|
337
|
-
})
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
**How it works:**
|
|
341
|
-
|
|
342
|
-
1. Client calls `nc.request()` with a timeout
|
|
343
|
-
2. Server receives on a queue-group subscription (load-balanced across instances)
|
|
344
|
-
3. Handler executes and responds via `msg.respond()`
|
|
345
|
-
4. Client receives the response
|
|
346
|
-
|
|
347
|
-
**Error behavior:**
|
|
348
|
-
|
|
349
|
-
| Scenario | Result |
|
|
350
|
-
|----------|--------|
|
|
351
|
-
| Handler success | Response returned to caller |
|
|
352
|
-
| Handler throws | Error response returned to caller |
|
|
353
|
-
| No handler registered | Error response returned to caller |
|
|
354
|
-
| Server not running | Client times out |
|
|
355
|
-
| Decode error | Error response returned to caller |
|
|
356
|
-
|
|
357
|
-
#### JetStream Mode
|
|
358
|
-
|
|
359
|
-
Commands are persisted in a JetStream stream. Responses flow back via NATS Core inbox.
|
|
360
|
-
|
|
361
|
-
```typescript
|
|
362
|
-
JetstreamModule.forRoot({
|
|
363
|
-
name: 'orders',
|
|
364
|
-
servers: ['nats://localhost:4222'],
|
|
365
|
-
rpc: { mode: 'jetstream', timeout: 120_000 },
|
|
366
|
-
})
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
**How it works:**
|
|
370
|
-
|
|
371
|
-
1. Client publishes command to JetStream with `replyTo` and `correlationId` headers
|
|
372
|
-
2. Server pulls from consumer, executes handler
|
|
373
|
-
3. Server publishes response to the client's inbox via Core NATS
|
|
374
|
-
4. Server acks/terms the JetStream message
|
|
375
|
-
|
|
376
|
-
**Error behavior:**
|
|
377
|
-
|
|
378
|
-
| Scenario | JetStream Action | Client Result |
|
|
379
|
-
|-----------------|------------------------|-------------------|
|
|
380
|
-
| Handler success | `ack` | Response returned |
|
|
381
|
-
| Handler throws | `term` (no redelivery) | Error response |
|
|
382
|
-
| Handler timeout | `term` | Client times out |
|
|
383
|
-
| Decode error | `term` | No response |
|
|
384
|
-
| No handler | `term` | No response |
|
|
385
|
-
|
|
386
|
-
> **Why `term` instead of `nak` for RPC errors?** Redelivering a failed command could cause duplicate side effects. The caller is responsible for retrying.
|
|
387
|
-
|
|
388
|
-
### Events
|
|
389
|
-
|
|
390
|
-
#### Workqueue Events
|
|
391
|
-
|
|
392
|
-
Each event is delivered to **one** handler instance (load-balanced). Messages are acked **after** the handler completes successfully.
|
|
393
|
-
|
|
394
|
-
```typescript
|
|
395
|
-
// Sending
|
|
396
|
-
this.client.emit('order.created', { orderId: 42 });
|
|
397
|
-
|
|
398
|
-
// Handling
|
|
399
|
-
@EventPattern('order.created')
|
|
400
|
-
handleOrderCreated(@Payload() data: OrderCreatedDto) {
|
|
401
|
-
// If this throws, the message is nak'd and redelivered (up to max_deliver times)
|
|
402
|
-
await this.ordersService.process(data);
|
|
403
|
-
}
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
**Delivery semantics (at-least-once):**
|
|
407
|
-
|
|
408
|
-
| Scenario | Action | Redelivery? |
|
|
409
|
-
|------------------|--------|---------------------------------------|
|
|
410
|
-
| Handler success | `ack` | No |
|
|
411
|
-
| Handler throws | `nak` | Yes, up to `max_deliver` (default: 3) |
|
|
412
|
-
| Decode error | `term` | No (malformed payload) |
|
|
413
|
-
| No handler found | `term` | No (configuration error) |
|
|
414
|
-
|
|
415
|
-
> Handlers **must be idempotent** — NATS may redeliver on failure or timeout.
|
|
416
|
-
|
|
417
|
-
**Custom stream/consumer configuration:**
|
|
418
|
-
|
|
419
|
-
```typescript
|
|
420
|
-
import { nanos } from '@horizon-republic/nestjs-jetstream';
|
|
421
|
-
|
|
422
|
-
JetstreamModule.forRoot({
|
|
423
|
-
name: 'orders',
|
|
424
|
-
servers: ['nats://localhost:4222'],
|
|
425
|
-
events: {
|
|
426
|
-
stream: {
|
|
427
|
-
max_age: nanos(3 * 24 * 60 * 60 * 1000), // 3 days instead of default 7
|
|
428
|
-
max_bytes: 1024 * 1024 * 512, // 512 MB instead of default 5 GB
|
|
429
|
-
},
|
|
430
|
-
consumer: {
|
|
431
|
-
max_deliver: 5, // retry up to 5 times instead of default 3
|
|
432
|
-
ack_wait: nanos(30_000), // 30s ack timeout instead of default 10s
|
|
433
|
-
},
|
|
434
|
-
},
|
|
435
|
-
})
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
#### Broadcast Events
|
|
439
|
-
|
|
440
|
-
Broadcast events are delivered to **all** subscribing services. Each service gets its own durable consumer on a shared `broadcast-stream`.
|
|
441
|
-
|
|
442
|
-
```typescript
|
|
443
|
-
// Sending — use the 'broadcast:' prefix
|
|
444
|
-
this.client.emit('broadcast:config.updated', { key: 'theme', value: 'dark' });
|
|
445
|
-
|
|
446
|
-
// Handling — use { broadcast: true } in extras
|
|
447
|
-
@EventPattern('config.updated', { broadcast: true })
|
|
448
|
-
handleConfigUpdated(@Payload() data: ConfigDto) {
|
|
449
|
-
this.configCache.invalidate(data.key);
|
|
450
|
-
}
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
Every service with this handler receives the message independently.
|
|
454
|
-
|
|
455
|
-
**Delivery guarantees:** Each service has its own durable consumer on the broadcast stream. Delivery tracking is fully isolated — if service A fails to process a message, only service A retries it (`nak`). Services B, C, D are not affected. This means broadcast provides **at-least-once delivery per consumer**, not at-most-once.
|
|
456
|
-
|
|
457
|
-
**Custom broadcast configuration:**
|
|
458
|
-
|
|
459
|
-
```typescript
|
|
460
|
-
import { nanos } from '@horizon-republic/nestjs-jetstream';
|
|
461
|
-
|
|
462
|
-
JetstreamModule.forRoot({
|
|
463
|
-
name: 'orders',
|
|
464
|
-
servers: ['nats://localhost:4222'],
|
|
465
|
-
broadcast: {
|
|
466
|
-
stream: {
|
|
467
|
-
max_age: nanos(7 * 24 * 60 * 60 * 1000), // keep messages for 7 days
|
|
468
|
-
},
|
|
469
|
-
consumer: {
|
|
470
|
-
max_deliver: 5,
|
|
471
|
-
},
|
|
472
|
-
},
|
|
473
|
-
})
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
> **Note:** The broadcast stream is shared across all services — stream-level settings (e.g., `max_age`, `max_bytes`) affect everyone. Consumer-level settings are per-service.
|
|
477
|
-
|
|
478
|
-
### JetstreamRecord Builder
|
|
479
|
-
|
|
480
|
-
Attach custom headers and per-request timeouts using the builder pattern:
|
|
481
|
-
|
|
482
|
-
```typescript
|
|
483
|
-
import { JetstreamRecordBuilder } from '@horizon-republic/nestjs-jetstream';
|
|
484
|
-
|
|
485
|
-
const record = new JetstreamRecordBuilder({ id: 1 })
|
|
486
|
-
.setHeader('x-trace-id', 'abc-123')
|
|
487
|
-
.setHeader('x-tenant', 'acme')
|
|
488
|
-
.setTimeout(5000)
|
|
489
|
-
.build();
|
|
490
|
-
|
|
491
|
-
// Works with both send() and emit()
|
|
492
|
-
this.client.send('user.get', record);
|
|
493
|
-
this.client.emit('user.created', record);
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
**Reserved headers** (set automatically by the transport, cannot be overridden):
|
|
497
|
-
|
|
498
|
-
| Header | Purpose |
|
|
499
|
-
|--------------------|-------------------------------|
|
|
500
|
-
| `x-correlation-id` | RPC request/response matching |
|
|
501
|
-
| `x-reply-to` | JetStream RPC response inbox |
|
|
502
|
-
| `x-error` | RPC error response flag |
|
|
503
|
-
|
|
504
|
-
Attempting to set a reserved header throws an error at build time.
|
|
505
|
-
|
|
506
|
-
**Additional transport headers** (set automatically, available in handlers):
|
|
507
|
-
|
|
508
|
-
| Header | Purpose |
|
|
509
|
-
|-----------------|---------------------------------------------|
|
|
510
|
-
| `x-subject` | Original NATS subject |
|
|
511
|
-
| `x-caller-name` | Sending service name |
|
|
512
|
-
| `x-request-id` | Available for user-defined request tracking |
|
|
513
|
-
| `x-trace-id` | Available for distributed tracing |
|
|
514
|
-
| `x-span-id` | Available for distributed tracing |
|
|
515
|
-
|
|
516
|
-
## Handler Context & Serialization
|
|
517
|
-
|
|
518
|
-
### RpcContext
|
|
519
|
-
|
|
520
|
-
Execution context available in all handlers via `@Ctx()`:
|
|
521
|
-
|
|
522
|
-
```typescript
|
|
523
|
-
import { Ctx, Payload, MessagePattern } from '@nestjs/microservices';
|
|
524
|
-
import { RpcContext } from '@horizon-republic/nestjs-jetstream';
|
|
525
|
-
|
|
526
|
-
@MessagePattern('user.get')
|
|
527
|
-
getUser(@Payload() data: GetUserDto, @Ctx() ctx: RpcContext) {
|
|
528
|
-
const subject = ctx.getSubject(); // Full NATS subject
|
|
529
|
-
const traceId = ctx.getHeader('x-trace-id'); // Single header value
|
|
530
|
-
const headers = ctx.getHeaders(); // All headers (MsgHdrs)
|
|
531
|
-
const isJs = ctx.isJetStream(); // true for JetStream messages
|
|
532
|
-
const msg = ctx.getMessage(); // Raw JsMsg | Msg (escape hatch)
|
|
533
|
-
|
|
534
|
-
return this.userService.findOne(data.id);
|
|
535
|
-
}
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
**Available methods:**
|
|
539
|
-
|
|
540
|
-
| Method | Returns | Description |
|
|
541
|
-
|------------------|------------------------|-------------------------------------------|
|
|
542
|
-
| `getSubject()` | `string` | NATS subject the message was published to |
|
|
543
|
-
| `getHeader(key)` | `string \| undefined` | Single header value by key |
|
|
544
|
-
| `getHeaders()` | `MsgHdrs \| undefined` | All NATS message headers |
|
|
545
|
-
| `isJetStream()` | `boolean` | Whether the message supports ack/nak/term |
|
|
546
|
-
| `getMessage()` | `JsMsg \| Msg` | Raw NATS message (escape hatch) |
|
|
547
|
-
|
|
548
|
-
Available on both `@EventPattern` and `@MessagePattern` handlers.
|
|
549
|
-
|
|
550
|
-
### Custom Codec
|
|
551
|
-
|
|
552
|
-
The library uses JSON by default. Implement the `Codec` interface for any serialization format:
|
|
553
|
-
|
|
554
|
-
```typescript
|
|
555
|
-
import { Codec } from '@horizon-republic/nestjs-jetstream';
|
|
556
|
-
import { encode, decode } from '@msgpack/msgpack';
|
|
557
|
-
|
|
558
|
-
class MsgPackCodec implements Codec {
|
|
559
|
-
encode(data: unknown): Uint8Array {
|
|
560
|
-
return encode(data);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
decode(data: Uint8Array): unknown {
|
|
564
|
-
return decode(data);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
```typescript
|
|
570
|
-
// Global codec
|
|
571
|
-
JetstreamModule.forRoot({
|
|
572
|
-
name: 'orders',
|
|
573
|
-
servers: ['nats://localhost:4222'],
|
|
574
|
-
codec: new MsgPackCodec(),
|
|
575
|
-
})
|
|
576
|
-
|
|
577
|
-
// Per-client override (falls back to global codec when omitted)
|
|
578
|
-
JetstreamModule.forFeature({
|
|
579
|
-
name: 'legacy-service',
|
|
580
|
-
codec: new JsonCodec(),
|
|
581
|
-
})
|
|
582
|
-
```
|
|
583
|
-
|
|
584
|
-
> All services communicating with each other **must use the same codec**. A codec mismatch results in decode errors (`term`, no redelivery).
|
|
585
|
-
|
|
586
|
-
## Operations
|
|
587
|
-
|
|
588
|
-
### Lifecycle Hooks
|
|
589
|
-
|
|
590
|
-
Subscribe to transport events for monitoring, alerting, or custom logic. Events without a registered hook are silently ignored — no default logging:
|
|
591
|
-
|
|
592
|
-
```typescript
|
|
593
|
-
import { JetstreamModule, TransportEvent } from '@horizon-republic/nestjs-jetstream';
|
|
594
|
-
|
|
595
|
-
JetstreamModule.forRoot({
|
|
596
|
-
name: 'orders',
|
|
597
|
-
servers: ['nats://localhost:4222'],
|
|
598
|
-
hooks: {
|
|
599
|
-
[TransportEvent.Connect]: (server) => {
|
|
600
|
-
console.log(`Connected to ${server}`);
|
|
601
|
-
},
|
|
602
|
-
[TransportEvent.Disconnect]: () => {
|
|
603
|
-
metrics.increment('nats.disconnect');
|
|
604
|
-
},
|
|
605
|
-
[TransportEvent.Error]: (error, context) => {
|
|
606
|
-
sentry.captureException(error, { extra: { context } });
|
|
607
|
-
},
|
|
608
|
-
[TransportEvent.RpcTimeout]: (subject, correlationId) => {
|
|
609
|
-
metrics.increment('rpc.timeout', { subject });
|
|
610
|
-
},
|
|
611
|
-
},
|
|
612
|
-
})
|
|
613
|
-
```
|
|
614
|
-
|
|
615
|
-
**Available events:**
|
|
616
|
-
|
|
617
|
-
| Event | Arguments |
|
|
618
|
-
|--------------------|---------------------------------------------|
|
|
619
|
-
| `connect` | `(server: string)` |
|
|
620
|
-
| `disconnect` | `()` |
|
|
621
|
-
| `reconnect` | `(server: string)` |
|
|
622
|
-
| `error` | `(error: Error, context?: string)` |
|
|
623
|
-
| `rpcTimeout` | `(subject: string, correlationId: string)` |
|
|
624
|
-
| `messageRouted` | `(subject: string, kind: 'rpc' \| 'event')` |
|
|
625
|
-
| `shutdownStart` | `()` |
|
|
626
|
-
| `shutdownComplete` | `()` |
|
|
627
|
-
| `deadLetter` | `(info: DeadLetterInfo)` |
|
|
628
|
-
|
|
629
|
-
#### Dead Letter Queue (DLQ)
|
|
630
|
-
|
|
631
|
-
When an event handler fails on every delivery attempt (`max_deliver`), the message becomes a "dead letter." By default, NATS terminates it silently. Configure `onDeadLetter` to intercept these messages:
|
|
632
|
-
|
|
633
|
-
```typescript
|
|
634
|
-
JetstreamModule.forRoot({
|
|
635
|
-
name: 'my-service',
|
|
636
|
-
servers: ['nats://localhost:4222'],
|
|
637
|
-
onDeadLetter: async (info) => {
|
|
638
|
-
// Persist to your DLQ store (database, S3, another queue, etc.)
|
|
639
|
-
await dlqRepository.save({
|
|
640
|
-
subject: info.subject,
|
|
641
|
-
data: info.data,
|
|
642
|
-
error: String(info.error),
|
|
643
|
-
deliveryCount: info.deliveryCount,
|
|
644
|
-
stream: info.stream,
|
|
645
|
-
timestamp: info.timestamp,
|
|
646
|
-
});
|
|
647
|
-
},
|
|
648
|
-
});
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
**Behavior:**
|
|
652
|
-
- Hook is awaited before `msg.term()` — if it succeeds, the message is terminated
|
|
653
|
-
- If the hook throws, the message is `nak()`'d for retry (NATS redelivers it)
|
|
654
|
-
- `TransportEvent.DeadLetter` is emitted for observability regardless of the hook
|
|
655
|
-
- Applies only to events (workqueue + broadcast), not RPC
|
|
656
|
-
|
|
657
|
-
**With dependency injection (`forRootAsync`):**
|
|
658
|
-
|
|
659
|
-
```typescript
|
|
660
|
-
import { JETSTREAM_CONNECTION } from '@horizon-republic/nestjs-jetstream';
|
|
661
|
-
|
|
662
|
-
JetstreamModule.forRootAsync({
|
|
663
|
-
name: 'my-service',
|
|
664
|
-
imports: [DlqModule],
|
|
665
|
-
inject: [DlqService, JETSTREAM_CONNECTION],
|
|
666
|
-
useFactory: (dlqService: DlqService, connection: ConnectionProvider) => ({
|
|
667
|
-
servers: ['nats://localhost:4222'],
|
|
668
|
-
onDeadLetter: async (info) => {
|
|
669
|
-
await dlqService.persist(info);
|
|
670
|
-
},
|
|
671
|
-
}),
|
|
672
|
-
});
|
|
673
|
-
```
|
|
674
|
-
|
|
675
|
-
### Health Checks
|
|
676
|
-
|
|
677
|
-
`JetstreamHealthIndicator` is automatically registered and exported by `forRoot()`. It checks NATS connection status and measures round-trip latency. `@nestjs/terminus` is **not required** — the indicator follows the Terminus API convention and can be used standalone.
|
|
678
|
-
|
|
679
|
-
> **Note:** `isHealthy()` throws a plain `Error` with attached status details rather than Terminus's `HealthCheckError`. Terminus will report the service as unhealthy, but the structured `{ status: 'down', server, latency }` details may not appear in the response body. For full Terminus formatting, use the `check()` method in a custom wrapper.
|
|
680
|
-
|
|
681
|
-
**With [@nestjs/terminus](https://docs.nestjs.com/recipes/terminus) (zero boilerplate):**
|
|
682
|
-
|
|
683
|
-
```typescript
|
|
684
|
-
import { Controller, Get } from '@nestjs/common';
|
|
685
|
-
import { HealthCheck, HealthCheckService } from '@nestjs/terminus';
|
|
686
|
-
import { JetstreamHealthIndicator } from '@horizon-republic/nestjs-jetstream';
|
|
687
|
-
|
|
688
|
-
@Controller('health')
|
|
689
|
-
export class HealthController {
|
|
690
|
-
constructor(
|
|
691
|
-
private health: HealthCheckService,
|
|
692
|
-
private jetstream: JetstreamHealthIndicator,
|
|
693
|
-
) {}
|
|
694
|
-
|
|
695
|
-
@Get()
|
|
696
|
-
@HealthCheck()
|
|
697
|
-
check() {
|
|
698
|
-
return this.health.check([
|
|
699
|
-
() => this.jetstream.isHealthy(),
|
|
700
|
-
]);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
**Standalone (without Terminus):**
|
|
706
|
-
|
|
707
|
-
```typescript
|
|
708
|
-
const status = await this.jetstream.check();
|
|
709
|
-
// { connected: true, server: 'nats://localhost:4222', latency: 2 }
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
| Method | Returns | Throws |
|
|
713
|
-
|-------------------|------------------------------------|------------------------------------|
|
|
714
|
-
| `check()` | `JetstreamHealthStatus` | Never |
|
|
715
|
-
| `isHealthy(key?)` | `{ [key]: { status: 'up', ... } }` | On unhealthy (Terminus convention) |
|
|
716
|
-
|
|
717
|
-
### Graceful Shutdown
|
|
718
|
-
|
|
719
|
-
The transport shuts down automatically via NestJS `onApplicationShutdown()`:
|
|
720
|
-
|
|
721
|
-
1. Stop accepting new messages (close subscriptions, stop consumers)
|
|
722
|
-
2. Drain and close NATS connection (waits for in-flight messages)
|
|
723
|
-
3. Safety timeout if drain takes too long
|
|
724
|
-
|
|
725
|
-
```typescript
|
|
726
|
-
JetstreamModule.forRoot({
|
|
727
|
-
name: 'orders',
|
|
728
|
-
servers: ['nats://localhost:4222'],
|
|
729
|
-
shutdownTimeout: 15_000, // default: 10_000 ms
|
|
730
|
-
})
|
|
731
|
-
```
|
|
732
|
-
|
|
733
|
-
No manual shutdown code needed.
|
|
734
|
-
|
|
735
|
-
## Reference
|
|
736
|
-
|
|
737
|
-
### Edge Cases & Important Notes
|
|
738
|
-
|
|
739
|
-
#### Event handlers must be idempotent
|
|
740
|
-
|
|
741
|
-
Events use at-least-once delivery. If your handler throws, the message is `nak`'d and NATS redelivers it (up to `max_deliver` times, default 3). Design handlers to be safe for repeated execution.
|
|
742
|
-
|
|
743
|
-
#### RPC error handling
|
|
744
|
-
|
|
745
|
-
The transport fully supports NestJS `RpcException` and custom exception filters. Throw `RpcException` with any payload — it will be delivered to the caller as-is:
|
|
746
|
-
|
|
747
|
-
```typescript
|
|
748
|
-
import { RpcException } from '@nestjs/microservices';
|
|
749
|
-
|
|
750
|
-
@MessagePattern('user.update')
|
|
751
|
-
updateUser(@Payload() data: UpdateUserDto) {
|
|
752
|
-
throw new RpcException({
|
|
753
|
-
statusCode: 400,
|
|
754
|
-
errors: [{ field: 'email', message: 'Already taken' }],
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
// Caller receives the full error object:
|
|
759
|
-
this.client.send('user.update', data).subscribe({
|
|
760
|
-
error: (err) => {
|
|
761
|
-
// err = { statusCode: 400, errors: [{ field: 'email', message: 'Already taken' }] }
|
|
762
|
-
},
|
|
763
|
-
});
|
|
764
|
-
```
|
|
765
|
-
|
|
766
|
-
In JetStream mode, failed RPC messages are `term`'d (not `nak`'d) to prevent duplicate side effects. The caller is responsible for implementing retry logic.
|
|
767
|
-
|
|
768
|
-
#### Fire-and-forget events
|
|
769
|
-
|
|
770
|
-
This library focuses on **reliable, persistent** event delivery via JetStream. If you need fire-and-forget (no persistence, no ack) for high-throughput scenarios, use the standard [NestJS NATS transport](https://docs.nestjs.com/microservices/nats) — it works perfectly alongside this library on the same NATS server.
|
|
771
|
-
|
|
772
|
-
#### Publisher-only mode
|
|
773
|
-
|
|
774
|
-
For services that only send messages (e.g., API gateways), disable consumer infrastructure:
|
|
775
|
-
|
|
776
|
-
```typescript
|
|
777
|
-
JetstreamModule.forRoot({
|
|
778
|
-
name: 'api-gateway',
|
|
779
|
-
servers: ['nats://localhost:4222'],
|
|
780
|
-
consumer: false, // no streams, consumers, or routers created
|
|
781
|
-
})
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
#### Broadcast stream is shared
|
|
785
|
-
|
|
786
|
-
All services share a single `broadcast-stream`. Each service creates its own durable consumer with `filter_subjects` matching only its registered broadcast patterns. Stream-level configuration (`broadcast.stream`) affects all services.
|
|
787
|
-
|
|
788
|
-
Broadcast consumers use the same ack/nak semantics as workqueue consumers. Because each service has an **isolated durable consumer**, a `nak` (retry) from one service only causes redelivery to that specific service — other consumers are unaffected. This gives broadcast **at-least-once delivery per consumer** with independent retry.
|
|
789
|
-
|
|
790
|
-
#### Connection failure behavior
|
|
791
|
-
|
|
792
|
-
If the initial NATS connection is refused, the module throws an `Error` immediately (fail fast). For transient disconnects after startup, NATS handles reconnection automatically and the `reconnect` hook fires.
|
|
793
|
-
|
|
794
|
-
#### Observable return values
|
|
795
|
-
|
|
796
|
-
Handlers can return Observables. The transport resolves on the **first emitted value** (or on completion if the Observable emits nothing):
|
|
797
|
-
|
|
798
|
-
```typescript
|
|
799
|
-
@MessagePattern('user.get')
|
|
800
|
-
getUser(@Payload() data: { id: number }): Observable<UserDto> {
|
|
801
|
-
return this.userService.findById(data.id); // first value used as response
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
@EventPattern('order.created')
|
|
805
|
-
handleOrder(@Payload() data: OrderDto): Observable<void> {
|
|
806
|
-
return this.pipeline.process(data); // ack after first emission or completion
|
|
807
40
|
}
|
|
808
41
|
```
|
|
809
42
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
If a JetStream consumer's message iterator ends unexpectedly (e.g., NATS restart), the transport automatically re-establishes consumption with exponential backoff (100ms up to 30s). This is logged as a warning.
|
|
813
|
-
|
|
814
|
-
#### NATS header size
|
|
815
|
-
|
|
816
|
-
Custom headers are transmitted as NATS message headers. NATS has a default header size limit. If you're attaching large metadata, consider putting it in the message body instead.
|
|
817
|
-
|
|
818
|
-
### NATS Naming Conventions
|
|
819
|
-
|
|
820
|
-
The transport generates NATS subjects, streams, and consumers based on the service `name`:
|
|
821
|
-
|
|
822
|
-
| Resource | Format | Example (`name: 'orders'`) |
|
|
823
|
-
|--------------------|---------------------------------|-------------------------------------------|
|
|
824
|
-
| Internal name | `{name}__microservice` | `orders__microservice` |
|
|
825
|
-
| RPC subject | `{internal}.cmd.{pattern}` | `orders__microservice.cmd.get.order` |
|
|
826
|
-
| Event subject | `{internal}.ev.{pattern}` | `orders__microservice.ev.order.created` |
|
|
827
|
-
| Broadcast subject | `broadcast.{pattern}` | `broadcast.config.updated` |
|
|
828
|
-
| Event stream | `{internal}_ev-stream` | `orders__microservice_ev-stream` |
|
|
829
|
-
| Command stream | `{internal}_cmd-stream` | `orders__microservice_cmd-stream` |
|
|
830
|
-
| Broadcast stream | `broadcast-stream` | `broadcast-stream` |
|
|
831
|
-
| Event consumer | `{internal}_ev-consumer` | `orders__microservice_ev-consumer` |
|
|
832
|
-
| Command consumer | `{internal}_cmd-consumer` | `orders__microservice_cmd-consumer` |
|
|
833
|
-
| Broadcast consumer | `{internal}_broadcast-consumer` | `orders__microservice_broadcast-consumer` |
|
|
834
|
-
|
|
835
|
-
### Default Stream & Consumer Configs
|
|
836
|
-
|
|
837
|
-
All defaults can be overridden via `events`, `broadcast`, or `rpc` options.
|
|
838
|
-
|
|
839
|
-
<details>
|
|
840
|
-
<summary><strong>Event Stream</strong></summary>
|
|
841
|
-
|
|
842
|
-
| Property | Value |
|
|
843
|
-
|----------------------|------------|
|
|
844
|
-
| Retention | Workqueue |
|
|
845
|
-
| Storage | File |
|
|
846
|
-
| Replicas | 1 |
|
|
847
|
-
| Max consumers | 100 |
|
|
848
|
-
| Max message size | 10 MB |
|
|
849
|
-
| Max messages/subject | 5,000,000 |
|
|
850
|
-
| Max messages | 50,000,000 |
|
|
851
|
-
| Max bytes | 5 GB |
|
|
852
|
-
| Max age | 7 days |
|
|
853
|
-
| Duplicate window | 2 minutes |
|
|
854
|
-
|
|
855
|
-
</details>
|
|
856
|
-
|
|
857
|
-
<details>
|
|
858
|
-
<summary><strong>Command Stream (JetStream RPC only)</strong></summary>
|
|
859
|
-
|
|
860
|
-
| Property | Value |
|
|
861
|
-
|----------------------|------------|
|
|
862
|
-
| Retention | Workqueue |
|
|
863
|
-
| Storage | File |
|
|
864
|
-
| Replicas | 1 |
|
|
865
|
-
| Max consumers | 50 |
|
|
866
|
-
| Max message size | 5 MB |
|
|
867
|
-
| Max messages/subject | 100,000 |
|
|
868
|
-
| Max messages | 1,000,000 |
|
|
869
|
-
| Max bytes | 100 MB |
|
|
870
|
-
| Max age | 3 minutes |
|
|
871
|
-
| Duplicate window | 30 seconds |
|
|
872
|
-
|
|
873
|
-
</details>
|
|
874
|
-
|
|
875
|
-
<details>
|
|
876
|
-
<summary><strong>Broadcast Stream</strong></summary>
|
|
877
|
-
|
|
878
|
-
| Property | Value |
|
|
879
|
-
|----------------------|------------|
|
|
880
|
-
| Retention | Limits |
|
|
881
|
-
| Storage | File |
|
|
882
|
-
| Replicas | 1 |
|
|
883
|
-
| Max consumers | 200 |
|
|
884
|
-
| Max message size | 10 MB |
|
|
885
|
-
| Max messages/subject | 1,000,000 |
|
|
886
|
-
| Max messages | 10,000,000 |
|
|
887
|
-
| Max bytes | 2 GB |
|
|
888
|
-
| Max age | 1 day |
|
|
889
|
-
| Duplicate window | 2 minutes |
|
|
890
|
-
|
|
891
|
-
</details>
|
|
892
|
-
|
|
893
|
-
<details>
|
|
894
|
-
<summary><strong>Consumer Configs</strong></summary>
|
|
895
|
-
|
|
896
|
-
**Event consumer:**
|
|
897
|
-
|
|
898
|
-
| Property | Value |
|
|
899
|
-
|-----------------|------------|
|
|
900
|
-
| Ack wait | 10 seconds |
|
|
901
|
-
| Max deliver | 3 |
|
|
902
|
-
| Max ack pending | 100 |
|
|
903
|
-
|
|
904
|
-
**Command consumer (JetStream RPC):**
|
|
905
|
-
|
|
906
|
-
| Property | Value |
|
|
907
|
-
|-----------------|-----------|
|
|
908
|
-
| Ack wait | 5 minutes |
|
|
909
|
-
| Max deliver | 1 |
|
|
910
|
-
| Max ack pending | 100 |
|
|
43
|
+
## Documentation
|
|
911
44
|
|
|
912
|
-
**
|
|
45
|
+
**[Read the full documentation →](https://horizonrepublic.github.io/nestjs-jetstream/)**
|
|
913
46
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
| Max deliver | 3 |
|
|
918
|
-
| Max ack pending | 100 |
|
|
47
|
+
- [Getting Started](https://horizonrepublic.github.io/nestjs-jetstream/docs/getting-started) — installation, module setup, first handler
|
|
48
|
+
- [Guides](https://horizonrepublic.github.io/nestjs-jetstream/docs/guides/health-checks) — health checks, graceful shutdown, lifecycle hooks
|
|
49
|
+
- [API Reference](https://horizonrepublic.github.io/nestjs-jetstream/docs/reference/api/) — full TypeDoc-generated API
|
|
919
50
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
### API Reference
|
|
923
|
-
|
|
924
|
-
#### Exports
|
|
925
|
-
|
|
926
|
-
```typescript
|
|
927
|
-
// Module
|
|
928
|
-
JetstreamModule
|
|
929
|
-
|
|
930
|
-
// Client
|
|
931
|
-
JetstreamClient
|
|
932
|
-
JetstreamRecord
|
|
933
|
-
JetstreamRecordBuilder
|
|
934
|
-
|
|
935
|
-
// Server
|
|
936
|
-
JetstreamStrategy
|
|
937
|
-
|
|
938
|
-
// Codec
|
|
939
|
-
JsonCodec
|
|
940
|
-
|
|
941
|
-
// Context
|
|
942
|
-
RpcContext
|
|
943
|
-
|
|
944
|
-
// Hooks
|
|
945
|
-
EventBus
|
|
946
|
-
TransportEvent
|
|
947
|
-
|
|
948
|
-
// Constants
|
|
949
|
-
JETSTREAM_OPTIONS
|
|
950
|
-
JETSTREAM_CONNECTION
|
|
951
|
-
JETSTREAM_CODEC
|
|
952
|
-
JETSTREAM_EVENT_BUS
|
|
953
|
-
JetstreamHeader
|
|
954
|
-
getClientToken
|
|
955
|
-
nanos
|
|
956
|
-
|
|
957
|
-
// Types
|
|
958
|
-
Codec
|
|
959
|
-
DeadLetterInfo
|
|
960
|
-
JetstreamModuleOptions
|
|
961
|
-
JetstreamModuleAsyncOptions
|
|
962
|
-
JetstreamFeatureOptions
|
|
963
|
-
RpcConfig
|
|
964
|
-
StreamConsumerOverrides
|
|
965
|
-
TransportHooks
|
|
966
|
-
```
|
|
967
|
-
|
|
968
|
-
#### Helper: `nanos(ms)`
|
|
969
|
-
|
|
970
|
-
Convert milliseconds to nanoseconds (required by NATS JetStream config):
|
|
971
|
-
|
|
972
|
-
```typescript
|
|
973
|
-
import { nanos } from '@horizon-republic/nestjs-jetstream';
|
|
974
|
-
|
|
975
|
-
// Use in stream/consumer overrides
|
|
976
|
-
events: {
|
|
977
|
-
stream: { max_age: nanos(3 * 24 * 60 * 60 * 1000) }, // 3 days
|
|
978
|
-
consumer: { ack_wait: nanos(30_000) }, // 30s
|
|
979
|
-
}
|
|
980
|
-
```
|
|
981
|
-
|
|
982
|
-
## Development
|
|
983
|
-
|
|
984
|
-
### Testing
|
|
985
|
-
|
|
986
|
-
The project uses [Vitest](https://vitest.dev/) with two test suites configured as [projects](https://vitest.dev/guide/workspace):
|
|
987
|
-
|
|
988
|
-
```bash
|
|
989
|
-
# Unit tests (no external dependencies)
|
|
990
|
-
pnpm test
|
|
991
|
-
|
|
992
|
-
# Integration tests (requires a running NATS server with JetStream)
|
|
993
|
-
pnpm test:integration
|
|
994
|
-
|
|
995
|
-
# Both suites sequentially
|
|
996
|
-
pnpm test:all
|
|
997
|
-
|
|
998
|
-
# Unit tests in watch mode
|
|
999
|
-
pnpm test:watch
|
|
1000
|
-
|
|
1001
|
-
# Unit tests with coverage report
|
|
1002
|
-
pnpm test:cov
|
|
1003
|
-
```
|
|
1004
|
-
|
|
1005
|
-
#### Running NATS locally
|
|
1006
|
-
|
|
1007
|
-
Integration tests require a NATS server with JetStream enabled:
|
|
1008
|
-
|
|
1009
|
-
```bash
|
|
1010
|
-
docker run -d --name nats -p 4222:4222 nats:latest -js
|
|
1011
|
-
```
|
|
1012
|
-
|
|
1013
|
-
#### Writing tests
|
|
1014
|
-
|
|
1015
|
-
- Use `sut` (system under test) for the main instance
|
|
1016
|
-
- Use `createMock<T>()` from `@golevelup/ts-vitest` for mocking
|
|
1017
|
-
- Follow Given-When-Then structure with comments
|
|
1018
|
-
- Order: happy path → edge cases → error cases
|
|
1019
|
-
- Always include `afterEach(vi.resetAllMocks)`
|
|
1020
|
-
|
|
1021
|
-
### Contributing
|
|
51
|
+
## Links
|
|
1022
52
|
|
|
1023
|
-
|
|
53
|
+
- [npm](https://www.npmjs.com/package/@horizon-republic/nestjs-jetstream)
|
|
54
|
+
- [GitHub](https://github.com/HorizonRepublic/nestjs-jetstream)
|
|
55
|
+
- [Documentation](https://horizonrepublic.github.io/nestjs-jetstream/)
|
|
56
|
+
- [Issues](https://github.com/HorizonRepublic/nestjs-jetstream/issues)
|
|
57
|
+
- [Discussions](https://github.com/HorizonRepublic/nestjs-jetstream/discussions)
|
|
1024
58
|
|
|
1025
59
|
## License
|
|
1026
60
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
## Links
|
|
1030
|
-
|
|
1031
|
-
- [NATS JetStream Documentation](https://docs.nats.io/nats-concepts/jetstream)
|
|
1032
|
-
- [NestJS Microservices](https://docs.nestjs.com/microservices/basics)
|
|
1033
|
-
- [GitHub Repository](https://github.com/HorizonRepublic/nestjs-jetstream)
|
|
1034
|
-
- [npm Package](https://www.npmjs.com/package/@horizon-republic/nestjs-jetstream)
|
|
1035
|
-
- [Report bugs](https://github.com/HorizonRepublic/nestjs-jetstream/issues)
|
|
1036
|
-
- [Discussions](https://github.com/HorizonRepublic/nestjs-jetstream/discussions)
|
|
61
|
+
MIT
|