@riaskov/nevo-messaging 1.0.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/LICENSE +21 -0
- package/README.md +831 -0
- package/dist/common/base.client.d.ts +12 -0
- package/dist/common/base.client.js +50 -0
- package/dist/common/base.controller.d.ts +28 -0
- package/dist/common/base.controller.js +159 -0
- package/dist/common/bigint.utils.d.ts +7 -0
- package/dist/common/bigint.utils.js +50 -0
- package/dist/common/error-code.d.ts +3 -0
- package/dist/common/error-code.js +7 -0
- package/dist/common/error-messages.d.ts +1 -0
- package/dist/common/error-messages.js +7 -0
- package/dist/common/errors.d.ts +9 -0
- package/dist/common/errors.js +41 -0
- package/dist/common/index.d.ts +8 -0
- package/dist/common/index.js +24 -0
- package/dist/common/service-utils.d.ts +21 -0
- package/dist/common/service-utils.js +54 -0
- package/dist/common/signal-router.decorator.d.ts +9 -0
- package/dist/common/signal-router.decorator.js +67 -0
- package/dist/common/types.d.ts +72 -0
- package/dist/common/types.js +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +20 -0
- package/dist/signal-router.utils.d.ts +28 -0
- package/dist/signal-router.utils.js +131 -0
- package/dist/signal.decorator.d.ts +15 -0
- package/dist/signal.decorator.js +67 -0
- package/dist/transports/index.d.ts +1 -0
- package/dist/transports/index.js +17 -0
- package/dist/transports/kafka/index.d.ts +5 -0
- package/dist/transports/kafka/index.js +21 -0
- package/dist/transports/kafka/kafka.client-base.d.ts +8 -0
- package/dist/transports/kafka/kafka.client-base.js +18 -0
- package/dist/transports/kafka/kafka.config.d.ts +16 -0
- package/dist/transports/kafka/kafka.config.js +210 -0
- package/dist/transports/kafka/kafka.signal-router.decorator.d.ts +3 -0
- package/dist/transports/kafka/kafka.signal-router.decorator.js +78 -0
- package/dist/transports/kafka/microservice.config.d.ts +10 -0
- package/dist/transports/kafka/microservice.config.js +46 -0
- package/dist/transports/kafka/nevo-kafka.client.d.ts +16 -0
- package/dist/transports/kafka/nevo-kafka.client.js +87 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
# Nevo Messaging
|
|
2
|
+
|
|
3
|
+
A powerful microservices messaging framework for NestJS 11+ with Kafka 4+ transport, designed for building scalable distributed systems with type-safe inter-service communication.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🚀 **Type-safe messaging** - Full TypeScript support with auto-completion
|
|
8
|
+
- 🔄 **Dual communication patterns** - Both request-response (query) and fire-and-forget (emit)
|
|
9
|
+
- 🎯 **Signal-based routing** - Declarative method mapping with `@Signal` decorator
|
|
10
|
+
- 📡 **Kafka transport** - Production-ready Apache Kafka integration
|
|
11
|
+
- 🔧 **Auto-configuration** - Automatic topic creation and client setup
|
|
12
|
+
- 🛡️ **Error handling** - Comprehensive error propagation and timeout management
|
|
13
|
+
- 📊 **BigInt support** - Native handling of large integers across services
|
|
14
|
+
- 🪝 **Lifecycle hooks** - Before/after message processing hooks
|
|
15
|
+
- 🔍 **Debug mode** - Built-in logging for development and troubleshooting
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @riaskov/nevo-messaging
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Peer Dependencies
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @nestjs/common @nestjs/core @nestjs/microservices @nestjs/config @nestjs/platform-fastify kafkajs rxjs reflect-metadata
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
### 1. Basic Service Setup
|
|
32
|
+
|
|
33
|
+
Create a simple microservice that responds to messages:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// user.service.ts
|
|
37
|
+
import { Injectable, Inject } from "@nestjs/common"
|
|
38
|
+
import { KafkaClientBase, NevoKafkaClient } from "@riaskov/nevo-messaging"
|
|
39
|
+
|
|
40
|
+
@Injectable()
|
|
41
|
+
export class UserService extends KafkaClientBase {
|
|
42
|
+
constructor(@Inject("NEVO_KAFKA_CLIENT") nevoClient: NevoKafkaClient) {
|
|
43
|
+
super(nevoClient)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async getById(id: bigint) {
|
|
47
|
+
return { id, name: "John Doe", email: "john@example.com" }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async create(userData: { name: string; email: string }) {
|
|
51
|
+
const newUser = { id: 123n, ...userData }
|
|
52
|
+
return newUser
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 2. Signal Router Controller
|
|
58
|
+
|
|
59
|
+
Map service methods to external signals using the `@Signal` decorator:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// user.controller.ts
|
|
63
|
+
import { Controller, Inject } from "@nestjs/common"
|
|
64
|
+
import { KafkaSignalRouter, Signal } from "@riaskov/nevo-messaging"
|
|
65
|
+
import { UserService } from "./user.service"
|
|
66
|
+
|
|
67
|
+
@Controller()
|
|
68
|
+
@KafkaSignalRouter([UserService])
|
|
69
|
+
export class UserController {
|
|
70
|
+
constructor(@Inject(UserService) private readonly userService: UserService) {}
|
|
71
|
+
|
|
72
|
+
@Signal("user.getById", "getById", (data: any) => [data.id])
|
|
73
|
+
getUserById() {}
|
|
74
|
+
|
|
75
|
+
@Signal("user.create", "create", (data: any) => [data])
|
|
76
|
+
createUser() {}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 3. Module Configuration
|
|
81
|
+
|
|
82
|
+
Configure the module with Kafka client:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// user.module.ts
|
|
86
|
+
import { Module } from "@nestjs/common"
|
|
87
|
+
import { ConfigModule } from "@nestjs/config"
|
|
88
|
+
import { createNevoKafkaClient } from "@riaskov/nevo-messaging"
|
|
89
|
+
import { UserController } from "./user.controller"
|
|
90
|
+
import { UserService } from "./user.service"
|
|
91
|
+
|
|
92
|
+
@Module({
|
|
93
|
+
imports: [ConfigModule],
|
|
94
|
+
controllers: [UserController],
|
|
95
|
+
providers: [
|
|
96
|
+
UserService,
|
|
97
|
+
createNevoKafkaClient(["COORDINATOR"], {
|
|
98
|
+
clientIdPrefix: "user"
|
|
99
|
+
})
|
|
100
|
+
]
|
|
101
|
+
})
|
|
102
|
+
export class UserModule {}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 4. Application Bootstrap
|
|
106
|
+
|
|
107
|
+
Start your microservice:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// main.ts
|
|
111
|
+
import { createKafkaMicroservice } from "@riaskov/nevo-messaging"
|
|
112
|
+
import { AppModule } from "./app.module"
|
|
113
|
+
|
|
114
|
+
createKafkaMicroservice({
|
|
115
|
+
microserviceName: "user",
|
|
116
|
+
module: AppModule,
|
|
117
|
+
port: 8086
|
|
118
|
+
}).then()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Core Concepts
|
|
122
|
+
|
|
123
|
+
### Signal Routing
|
|
124
|
+
|
|
125
|
+
The Signal Router pattern allows you to declaratively map external message patterns to internal service methods:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
@Signal("external.signal.name", "internalMethodName", parameterTransformer?, resultTransformer?)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Communication Patterns
|
|
132
|
+
|
|
133
|
+
#### Query Pattern (Request-Response)
|
|
134
|
+
Use for operations that need a response:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
const user = await this.query("user", "user.getById", { id: 123n })
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### Emit Pattern (Fire-and-Forget)
|
|
141
|
+
Use for events and notifications:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
await this.emit("notifications", "user.created", { userId: 123n, email: "user@example.com" })
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Advanced Usage
|
|
148
|
+
|
|
149
|
+
### Parameter Transformation
|
|
150
|
+
|
|
151
|
+
Transform incoming parameters before passing to service methods:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
@Signal("user.update", "updateUser", (data: any) => [data.id, data.changes])
|
|
155
|
+
updateUser() {}
|
|
156
|
+
|
|
157
|
+
// Service method signature:
|
|
158
|
+
async updateUser(id: bigint, changes: Partial<User>) {
|
|
159
|
+
// Implementation
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Result Transformation
|
|
164
|
+
|
|
165
|
+
Transform service method results before sending response:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
@Signal(
|
|
169
|
+
"user.getProfile",
|
|
170
|
+
"getById",
|
|
171
|
+
(data: any) => [data.id],
|
|
172
|
+
(user: User) => ({ ...user, password: undefined }) // Remove sensitive data
|
|
173
|
+
)
|
|
174
|
+
getProfile() {}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Multiple Service Dependencies
|
|
178
|
+
|
|
179
|
+
Route signals to different services within the same controller:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
@Controller()
|
|
183
|
+
@KafkaSignalRouter([UserService, ProfileService, NotificationService])
|
|
184
|
+
export class UserController {
|
|
185
|
+
constructor(
|
|
186
|
+
@Inject(UserService) private readonly userService: UserService,
|
|
187
|
+
@Inject(ProfileService) private readonly profileService: ProfileService,
|
|
188
|
+
@Inject(NotificationService) private readonly notificationService: NotificationService
|
|
189
|
+
) {}
|
|
190
|
+
|
|
191
|
+
@Signal("user.create", "createUser", (data: any) => [data])
|
|
192
|
+
createUser() {}
|
|
193
|
+
|
|
194
|
+
@Signal("profile.update", "updateProfile", (data: any) => [data.userId, data.profile])
|
|
195
|
+
updateProfile() {}
|
|
196
|
+
|
|
197
|
+
@Signal("notification.send", "sendNotification", (data: any) => [data.userId, data.message])
|
|
198
|
+
sendNotification() {}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Cross-Service Communication
|
|
203
|
+
|
|
204
|
+
Services can communicate with each other through the messaging layer:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
@Injectable()
|
|
208
|
+
export class OrderService extends KafkaClientBase {
|
|
209
|
+
constructor(@Inject("NEVO_KAFKA_CLIENT") nevoClient: NevoKafkaClient) {
|
|
210
|
+
super(nevoClient)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async createOrder(orderData: CreateOrderDto) {
|
|
214
|
+
// Create the order
|
|
215
|
+
const order = await this.saveOrder(orderData)
|
|
216
|
+
|
|
217
|
+
// Query user service for user details
|
|
218
|
+
const user = await this.query("user", "user.getById", { id: orderData.userId })
|
|
219
|
+
|
|
220
|
+
// Query inventory service to reserve items
|
|
221
|
+
const reservation = await this.query("inventory", "item.reserve", {
|
|
222
|
+
items: orderData.items,
|
|
223
|
+
orderId: order.id
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
// Emit event to notification service
|
|
227
|
+
await this.emit("notifications", "order.created", {
|
|
228
|
+
orderId: order.id,
|
|
229
|
+
userId: user.id,
|
|
230
|
+
userEmail: user.email
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
return order
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Lifecycle Hooks
|
|
239
|
+
|
|
240
|
+
Add custom logic before and after message processing:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
@KafkaSignalRouter([UserService], {
|
|
244
|
+
before: async (context) => {
|
|
245
|
+
console.log(`Processing ${context.method} for ${context.uuid}`)
|
|
246
|
+
// Validate request, log metrics, etc.
|
|
247
|
+
return context.params // Can modify parameters
|
|
248
|
+
},
|
|
249
|
+
after: async (context) => {
|
|
250
|
+
console.log(`Completed ${context.method} with result:`, context.result)
|
|
251
|
+
// Log metrics, audit trail, etc.
|
|
252
|
+
return context.response // Can modify response
|
|
253
|
+
},
|
|
254
|
+
debug: true
|
|
255
|
+
})
|
|
256
|
+
export class UserController {
|
|
257
|
+
// ...
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Error Handling
|
|
262
|
+
|
|
263
|
+
The framework provides comprehensive error handling:
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import { MessagingError, ErrorCode } from "@riaskov/nevo-messaging"
|
|
267
|
+
|
|
268
|
+
@Injectable()
|
|
269
|
+
export class UserService extends KafkaClientBase {
|
|
270
|
+
async getById(id: bigint) {
|
|
271
|
+
const user = await this.findUser(id)
|
|
272
|
+
|
|
273
|
+
if (!user) {
|
|
274
|
+
throw new MessagingError(ErrorCode.UNKNOWN, {
|
|
275
|
+
message: "User not found",
|
|
276
|
+
userId: id
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return user
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Configuration
|
|
286
|
+
|
|
287
|
+
### Environment Variables
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
# Kafka Configuration
|
|
291
|
+
KAFKA_HOST=localhost
|
|
292
|
+
KAFKA_PORT=9092
|
|
293
|
+
NODE_ENV=production
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Kafka Client Options
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
createNevoKafkaClient(["USER", "INVENTORY", "NOTIFICATIONS"], {
|
|
300
|
+
clientIdPrefix: "order-service",
|
|
301
|
+
groupIdPrefix: "order-consumer",
|
|
302
|
+
sessionTimeout: 30000,
|
|
303
|
+
allowAutoTopicCreation: true,
|
|
304
|
+
retryAttempts: 5,
|
|
305
|
+
brokerRetryTimeout: 2000,
|
|
306
|
+
timeoutMs: 25000,
|
|
307
|
+
debug: false
|
|
308
|
+
})
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Microservice Startup Options
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
createKafkaMicroservice({
|
|
315
|
+
microserviceName: "user",
|
|
316
|
+
module: AppModule,
|
|
317
|
+
port: 8086,
|
|
318
|
+
host: "0.0.0.0",
|
|
319
|
+
debug: true,
|
|
320
|
+
onInit: async (app) => {
|
|
321
|
+
// Custom initialization logic
|
|
322
|
+
await app.get(DatabaseService).runMigrations()
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## BigInt Support
|
|
328
|
+
|
|
329
|
+
The framework automatically handles BigInt serialization across service boundaries:
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
// Service returns BigInt
|
|
333
|
+
async getUserId(): Promise<bigint> {
|
|
334
|
+
return 9007199254740991n // Large integer
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Automatically serialized as "9007199254740991n"
|
|
338
|
+
// Automatically deserialized back to BigInt on the receiving end
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Architecture Patterns
|
|
342
|
+
|
|
343
|
+
### Event Sourcing Pattern
|
|
344
|
+
|
|
345
|
+
Use events to maintain state consistency across services:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
@Injectable()
|
|
349
|
+
export class OrderService extends KafkaClientBase {
|
|
350
|
+
async createOrder(orderData: CreateOrderDto) {
|
|
351
|
+
const order = await this.saveOrder(orderData)
|
|
352
|
+
|
|
353
|
+
// Emit domain events
|
|
354
|
+
await Promise.all([
|
|
355
|
+
this.emit("events", "order.created", {
|
|
356
|
+
orderId: order.id,
|
|
357
|
+
userId: order.userId,
|
|
358
|
+
timestamp: new Date(),
|
|
359
|
+
aggregateVersion: 1
|
|
360
|
+
}),
|
|
361
|
+
this.emit("events", "inventory.reserved", {
|
|
362
|
+
orderId: order.id,
|
|
363
|
+
items: order.items,
|
|
364
|
+
timestamp: new Date()
|
|
365
|
+
})
|
|
366
|
+
])
|
|
367
|
+
|
|
368
|
+
return order
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### CQRS Pattern
|
|
374
|
+
|
|
375
|
+
Separate command and query responsibilities:
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
// Command Service
|
|
379
|
+
@Injectable()
|
|
380
|
+
export class UserCommandService extends KafkaClientBase {
|
|
381
|
+
async createUser(userData: CreateUserDto) {
|
|
382
|
+
const user = await this.repository.save(userData)
|
|
383
|
+
|
|
384
|
+
// Emit event for read model updates
|
|
385
|
+
await this.emit("events", "user.created", {
|
|
386
|
+
userId: user.id,
|
|
387
|
+
email: user.email,
|
|
388
|
+
timestamp: new Date()
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
return user
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Query Service
|
|
396
|
+
@Injectable()
|
|
397
|
+
export class UserQueryService extends KafkaClientBase {
|
|
398
|
+
async getUserProfile(userId: bigint) {
|
|
399
|
+
// Optimized read model
|
|
400
|
+
return this.readRepository.findUserProfile(userId)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Advanced Configuration
|
|
406
|
+
|
|
407
|
+
### Custom Message Extractors
|
|
408
|
+
|
|
409
|
+
For complex message formats:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
export function createCustomSignalRouter(serviceType: Type<any>[], options?: SignalRouterOptions) {
|
|
413
|
+
return createSignalRouterDecorator(
|
|
414
|
+
serviceType,
|
|
415
|
+
options,
|
|
416
|
+
(data) => {
|
|
417
|
+
// Custom message extraction logic
|
|
418
|
+
const envelope = parseWithBigInt(data.value)
|
|
419
|
+
return {
|
|
420
|
+
method: envelope.command.action,
|
|
421
|
+
params: envelope.payload,
|
|
422
|
+
uuid: envelope.metadata.correlationId
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
(target, eventPattern, handlerName) => {
|
|
426
|
+
// Custom handler registration
|
|
427
|
+
MessagePattern(eventPattern)(target.prototype, handlerName,
|
|
428
|
+
Object.getOwnPropertyDescriptor(target.prototype, handlerName)!)
|
|
429
|
+
}
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Distributed Tracing
|
|
435
|
+
|
|
436
|
+
Implement correlation IDs for request tracing:
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
@KafkaSignalRouter([UserService], {
|
|
440
|
+
before: async (context) => {
|
|
441
|
+
// Inject correlation ID
|
|
442
|
+
const correlationId = context.uuid
|
|
443
|
+
context.params.correlationId = correlationId
|
|
444
|
+
|
|
445
|
+
console.log(`[${correlationId}] Starting ${context.method}`)
|
|
446
|
+
return context.params
|
|
447
|
+
},
|
|
448
|
+
after: async (context) => {
|
|
449
|
+
const correlationId = context.params.correlationId
|
|
450
|
+
console.log(`[${correlationId}] Completed ${context.method}`)
|
|
451
|
+
return context.response
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
export class UserController {
|
|
455
|
+
// ...
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Retry Policies
|
|
460
|
+
|
|
461
|
+
Configure retry behavior for failed operations:
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
@Injectable()
|
|
465
|
+
export class ResilientService extends KafkaClientBase {
|
|
466
|
+
async performOperation(data: any) {
|
|
467
|
+
const maxRetries = 3
|
|
468
|
+
let attempt = 0
|
|
469
|
+
|
|
470
|
+
while (attempt < maxRetries) {
|
|
471
|
+
try {
|
|
472
|
+
return await this.query("external", "risky.operation", data)
|
|
473
|
+
} catch (error) {
|
|
474
|
+
attempt++
|
|
475
|
+
if (attempt >= maxRetries) throw error
|
|
476
|
+
|
|
477
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * attempt))
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
## Performance Optimization
|
|
485
|
+
|
|
486
|
+
### Batch Operations
|
|
487
|
+
|
|
488
|
+
Process multiple operations efficiently:
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
@Injectable()
|
|
492
|
+
export class BatchUserService extends KafkaClientBase {
|
|
493
|
+
async processBatch(userIds: bigint[]) {
|
|
494
|
+
// Process in chunks to avoid overwhelming downstream services
|
|
495
|
+
const chunkSize = 10
|
|
496
|
+
const results = []
|
|
497
|
+
|
|
498
|
+
for (let i = 0; i < userIds.length; i += chunkSize) {
|
|
499
|
+
const chunk = userIds.slice(i, i + chunkSize)
|
|
500
|
+
const chunkResults = await Promise.all(
|
|
501
|
+
chunk.map(id => this.query("user", "user.getById", { id }))
|
|
502
|
+
)
|
|
503
|
+
results.push(...chunkResults)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return results
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### Caching Layer
|
|
512
|
+
|
|
513
|
+
Implement service-level caching:
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
@Injectable()
|
|
517
|
+
export class CachedUserService extends KafkaClientBase {
|
|
518
|
+
private cache = new Map<string, any>()
|
|
519
|
+
private readonly cacheTimeout = 300000 // 5 minutes
|
|
520
|
+
|
|
521
|
+
async getCachedUser(id: bigint) {
|
|
522
|
+
const cacheKey = `user:${id}`
|
|
523
|
+
const cached = this.cache.get(cacheKey)
|
|
524
|
+
|
|
525
|
+
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
|
|
526
|
+
return cached.data
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const user = await this.query("user", "user.getById", { id })
|
|
530
|
+
this.cache.set(cacheKey, { data: user, timestamp: Date.now() })
|
|
531
|
+
|
|
532
|
+
return user
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
## Monitoring and Observability
|
|
538
|
+
|
|
539
|
+
### Health Checks
|
|
540
|
+
|
|
541
|
+
Implement service health monitoring:
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
@Injectable()
|
|
545
|
+
export class HealthService extends KafkaClientBase {
|
|
546
|
+
async checkServiceHealth() {
|
|
547
|
+
const services = this.getAvailableServices()
|
|
548
|
+
const healthChecks = await Promise.allSettled(
|
|
549
|
+
services.map(async (service) => {
|
|
550
|
+
try {
|
|
551
|
+
await this.query(service, "health.check", {})
|
|
552
|
+
return { service, status: "healthy" }
|
|
553
|
+
} catch (error) {
|
|
554
|
+
return { service, status: "unhealthy", error: error.message }
|
|
555
|
+
}
|
|
556
|
+
})
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
return healthChecks.map(result =>
|
|
560
|
+
result.status === "fulfilled" ? result.value : result.reason
|
|
561
|
+
)
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### Metrics Collection
|
|
567
|
+
|
|
568
|
+
Track message processing metrics:
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
@KafkaSignalRouter([MetricsService], {
|
|
572
|
+
before: async (context) => {
|
|
573
|
+
context.startTime = Date.now()
|
|
574
|
+
return context.params
|
|
575
|
+
},
|
|
576
|
+
after: async (context) => {
|
|
577
|
+
const duration = Date.now() - context.startTime
|
|
578
|
+
|
|
579
|
+
await this.emit("metrics", "message.processed", {
|
|
580
|
+
service: context.serviceName,
|
|
581
|
+
method: context.method,
|
|
582
|
+
duration,
|
|
583
|
+
success: context.response.params.result !== "error"
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
return context.response
|
|
587
|
+
}
|
|
588
|
+
})
|
|
589
|
+
export class MetricsController {
|
|
590
|
+
// ...
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
## Security
|
|
595
|
+
|
|
596
|
+
### Message Validation
|
|
597
|
+
|
|
598
|
+
Implement input validation:
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
import { IsString, IsEmail, validate } from "class-validator"
|
|
602
|
+
|
|
603
|
+
class CreateUserDto {
|
|
604
|
+
@IsString()
|
|
605
|
+
name: string
|
|
606
|
+
|
|
607
|
+
@IsEmail()
|
|
608
|
+
email: string
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
@Injectable()
|
|
612
|
+
export class SecureUserService extends KafkaClientBase {
|
|
613
|
+
async createUser(userData: any) {
|
|
614
|
+
const dto = Object.assign(new CreateUserDto(), userData)
|
|
615
|
+
const errors = await validate(dto)
|
|
616
|
+
|
|
617
|
+
if (errors.length > 0) {
|
|
618
|
+
throw new MessagingError(ErrorCode.UNKNOWN, {
|
|
619
|
+
message: "Validation failed",
|
|
620
|
+
errors: errors.map(e => e.constraints)
|
|
621
|
+
})
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return this.repository.save(dto)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### Authentication Context
|
|
630
|
+
|
|
631
|
+
Pass authentication context between services:
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
@KafkaSignalRouter([UserService], {
|
|
635
|
+
before: async (context) => {
|
|
636
|
+
// Extract and validate auth token
|
|
637
|
+
const authHeader = context.rawData.headers?.authorization
|
|
638
|
+
const user = await this.validateToken(authHeader)
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
...context.params,
|
|
642
|
+
authContext: { userId: user.id, roles: user.roles }
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
})
|
|
646
|
+
export class SecureUserController {
|
|
647
|
+
// ...
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
## Production Deployment
|
|
652
|
+
|
|
653
|
+
### Docker Compose Setup
|
|
654
|
+
|
|
655
|
+
```yaml
|
|
656
|
+
services:
|
|
657
|
+
kafka:
|
|
658
|
+
image: apache/kafka:4.0.0
|
|
659
|
+
environment:
|
|
660
|
+
- KAFKA_PROCESS_ROLES=broker,controller
|
|
661
|
+
- KAFKA_NODE_ID=1
|
|
662
|
+
- KAFKA_CONTROLLER_QUORUM_VOTERS=1@kafka:9093
|
|
663
|
+
- KAFKA_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093
|
|
664
|
+
- KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092
|
|
665
|
+
- KAFKA_AUTO_CREATE_TOPICS_ENABLE=true
|
|
666
|
+
|
|
667
|
+
user-service:
|
|
668
|
+
build: ./user-service
|
|
669
|
+
environment:
|
|
670
|
+
- KAFKA_HOST=kafka
|
|
671
|
+
- KAFKA_PORT=9092
|
|
672
|
+
depends_on:
|
|
673
|
+
- kafka
|
|
674
|
+
|
|
675
|
+
order-service:
|
|
676
|
+
build: ./order-service
|
|
677
|
+
environment:
|
|
678
|
+
- KAFKA_HOST=kafka
|
|
679
|
+
- KAFKA_PORT=9092
|
|
680
|
+
depends_on:
|
|
681
|
+
- kafka
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### Scaling Considerations
|
|
685
|
+
|
|
686
|
+
Configure partition strategy for high throughput:
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
createNevoKafkaClient(["HIGH_VOLUME_SERVICE"], {
|
|
690
|
+
clientIdPrefix: "processor",
|
|
691
|
+
partitionStrategy: "round-robin",
|
|
692
|
+
maxInFlightRequests: 5,
|
|
693
|
+
batchSize: 100,
|
|
694
|
+
lingerMs: 10
|
|
695
|
+
})
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
## API Reference
|
|
699
|
+
|
|
700
|
+
### Decorators
|
|
701
|
+
|
|
702
|
+
#### `@Signal(signalName, methodName?, paramTransformer?, resultTransformer?)`
|
|
703
|
+
|
|
704
|
+
Maps external signals to service methods.
|
|
705
|
+
|
|
706
|
+
**Parameters:**
|
|
707
|
+
- `signalName` (string): External signal identifier
|
|
708
|
+
- `methodName` (string, optional): Service method name (defaults to signalName)
|
|
709
|
+
- `paramTransformer` (function, optional): Transform incoming parameters
|
|
710
|
+
- `resultTransformer` (function, optional): Transform outgoing results
|
|
711
|
+
|
|
712
|
+
#### `@KafkaSignalRouter(serviceTypes, options?)`
|
|
713
|
+
|
|
714
|
+
Class decorator for signal routing setup.
|
|
715
|
+
|
|
716
|
+
**Parameters:**
|
|
717
|
+
- `serviceTypes` (Type<any> | Type<any>[]): Service classes to route to
|
|
718
|
+
- `options` (object, optional): Configuration options
|
|
719
|
+
|
|
720
|
+
### Classes
|
|
721
|
+
|
|
722
|
+
#### `KafkaClientBase`
|
|
723
|
+
|
|
724
|
+
Base class for services that need to communicate with other microservices.
|
|
725
|
+
|
|
726
|
+
**Methods:**
|
|
727
|
+
- `query<T>(serviceName, method, params): Promise<T>` - Request-response communication
|
|
728
|
+
- `emit(serviceName, method, params): Promise<void>` - Fire-and-forget communication
|
|
729
|
+
- `getAvailableServices(): string[]` - List registered services
|
|
730
|
+
|
|
731
|
+
#### `NevoKafkaClient`
|
|
732
|
+
|
|
733
|
+
Universal Kafka client for multi-service communication.
|
|
734
|
+
|
|
735
|
+
**Methods:**
|
|
736
|
+
- `query<T>(serviceName, method, params): Promise<T>` - Send query to service
|
|
737
|
+
- `emit(serviceName, method, params): Promise<void>` - Emit event to service
|
|
738
|
+
- `getAvailableServices(): string[]` - Get list of available services
|
|
739
|
+
|
|
740
|
+
### Functions
|
|
741
|
+
|
|
742
|
+
#### `createNevoKafkaClient(serviceNames, options)`
|
|
743
|
+
|
|
744
|
+
Factory function for creating Kafka client providers.
|
|
745
|
+
|
|
746
|
+
#### `createKafkaMicroservice(options)`
|
|
747
|
+
|
|
748
|
+
Bootstrap function for starting NestJS microservices with Kafka transport.
|
|
749
|
+
|
|
750
|
+
## Troubleshooting
|
|
751
|
+
|
|
752
|
+
### Common Issues
|
|
753
|
+
|
|
754
|
+
**Topic Creation Failures**
|
|
755
|
+
```bash
|
|
756
|
+
# Ensure Kafka is running and accessible
|
|
757
|
+
docker-compose up kafka
|
|
758
|
+
|
|
759
|
+
# Check topic creation logs
|
|
760
|
+
docker-compose logs kafka
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
**Connection Timeouts**
|
|
764
|
+
```typescript
|
|
765
|
+
// Increase timeouts for slow networks
|
|
766
|
+
createNevoKafkaClient(["USER"], {
|
|
767
|
+
clientIdPrefix: "app",
|
|
768
|
+
timeoutMs: 30000,
|
|
769
|
+
sessionTimeout: 45000
|
|
770
|
+
})
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
**Serialization Errors**
|
|
774
|
+
```typescript
|
|
775
|
+
// Enable debug mode to see message payloads
|
|
776
|
+
@KafkaSignalRouter([UserService], { debug: true })
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
### Debug Mode
|
|
780
|
+
|
|
781
|
+
Enable comprehensive logging:
|
|
782
|
+
|
|
783
|
+
```bash
|
|
784
|
+
NODE_ENV=development
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
```typescript
|
|
788
|
+
createKafkaMicroservice({
|
|
789
|
+
microserviceName: "user",
|
|
790
|
+
module: AppModule,
|
|
791
|
+
debug: true
|
|
792
|
+
})
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
## Migration Guide
|
|
796
|
+
|
|
797
|
+
### From Other Messaging Libraries
|
|
798
|
+
|
|
799
|
+
If migrating from other microservice messaging solutions:
|
|
800
|
+
|
|
801
|
+
1. **Replace message handlers** with `@Signal` decorators
|
|
802
|
+
2. **Update service injection** to use `KafkaClientBase`
|
|
803
|
+
3. **Configure Kafka clients** with `createNevoKafkaClient`
|
|
804
|
+
4. **Update message patterns** to use signal names
|
|
805
|
+
|
|
806
|
+
### Version Compatibility
|
|
807
|
+
|
|
808
|
+
- **Node.js**: ≥24.0.0
|
|
809
|
+
- **NestJS**: ≥11.1.0
|
|
810
|
+
- **Kafka**: ≥2.8.0 (≥4.0.0 is recommended)
|
|
811
|
+
|
|
812
|
+
## Contributing
|
|
813
|
+
|
|
814
|
+
1. Fork the repository
|
|
815
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
816
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
817
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
818
|
+
5. Open a Pull Request
|
|
819
|
+
|
|
820
|
+
## License
|
|
821
|
+
|
|
822
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
823
|
+
|
|
824
|
+
## Support
|
|
825
|
+
|
|
826
|
+
- GitHub Issues: [Report bugs and request features](https://github.com/ARyaskov/nevo-messaging/issues)
|
|
827
|
+
- Documentation: This README and inline code documentation
|
|
828
|
+
- Examples: Check the `examples/` directory for complete working examples
|
|
829
|
+
|
|
830
|
+
## Aux
|
|
831
|
+
There are many anys in core code - the simple temporary solution for changeable Nest.js microservices API.
|