@autofleet/kafka 0.0.5 → 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 +421 -256
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -33
- package/dist/index.d.ts +70 -33
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +14 -11
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @autofleet/kafka
|
|
2
2
|
|
|
3
|
-
Internal wrapper for Apache Kafka producer using [@platformatic/kafka](https://www.npmjs.com/package/@platformatic/kafka), providing a production-ready interface for
|
|
3
|
+
Internal wrapper for Apache Kafka producer using [@platformatic/kafka](https://www.npmjs.com/package/@platformatic/kafka), providing a production-ready interface for managing multiple Kafka producers.
|
|
4
4
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
@@ -8,9 +8,11 @@ Internal wrapper for Apache Kafka producer using [@platformatic/kafka](https://w
|
|
|
8
8
|
- [Features](#features)
|
|
9
9
|
- [Quick Start](#quick-start)
|
|
10
10
|
- [Usage](#usage)
|
|
11
|
+
- [Setup with Multiple Producers](#setup-with-multiple-producers)
|
|
11
12
|
- [Publishing Messages](#publishing-messages)
|
|
12
|
-
- [
|
|
13
|
-
- [
|
|
13
|
+
- [Mock Mode (Disable Kafka)](#mock-mode-disable-kafka)
|
|
14
|
+
- [Health Checks & Readiness Probes](#health-checks--readiness-probes)
|
|
15
|
+
- [Migration from getKafka() Pattern](#migration-from-getkafka-pattern)
|
|
14
16
|
- [Configuration](#configuration)
|
|
15
17
|
- [Advanced Features](#advanced-features)
|
|
16
18
|
- [Message Keys for Partitioning](#message-keys-for-partitioning)
|
|
@@ -28,172 +30,314 @@ pnpm add @autofleet/kafka
|
|
|
28
30
|
|
|
29
31
|
## Features
|
|
30
32
|
|
|
31
|
-
- **
|
|
33
|
+
- **Multi-Producer Management** - Manage multiple named producers with different broker configurations
|
|
34
|
+
- **Type-Safe Producer Names** - Producer names are typed based on your configuration with full autocomplete
|
|
35
|
+
- **Built-in Mock Mode** - Disable Kafka entirely with automatic mock implementations
|
|
36
|
+
- **Direct API Access** - No need for `getKafka()` wrappers, access producers directly
|
|
37
|
+
- **Centralized Health Checks** - Single readiness check for all producers
|
|
32
38
|
- **Automatic Connection Management** - Handles connection lifecycle automatically
|
|
33
39
|
- **Batch Publishing** - Efficient batch message sending
|
|
34
40
|
- **Message Partitioning** - Control message distribution across partitions
|
|
35
41
|
- **Custom Headers** - Attach metadata to messages
|
|
36
42
|
- **Graceful Shutdown** - Automatic cleanup on process termination
|
|
37
|
-
- **
|
|
38
|
-
- **ESM & CommonJS Compatible** - Works in both ESM and CommonJS projects
|
|
43
|
+
- **Full TypeScript Support** - Type-safe APIs with IntelliSense autocomplete
|
|
44
|
+
- **ESM & CommonJS Compatible** - Works in both ESM and CommonJS projects
|
|
39
45
|
- **Production Ready** - Built with reliability and performance in mind
|
|
40
46
|
|
|
41
47
|
## Quick Start
|
|
42
48
|
|
|
43
49
|
```typescript
|
|
44
|
-
import
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
import { KafkaManager } from '@autofleet/kafka';
|
|
51
|
+
import logger from './logger';
|
|
52
|
+
|
|
53
|
+
// Initialize with multiple named producers (synchronous!)
|
|
54
|
+
const kafka = KafkaManager.create({
|
|
55
|
+
enabled: process.env.ENABLE_KAFKA === 'true', // Built-in mock mode
|
|
56
|
+
logger,
|
|
57
|
+
producers: {
|
|
58
|
+
main: {
|
|
59
|
+
brokers: ['kafka-main.svc.cluster.local:9092'],
|
|
60
|
+
clientId: 'my-service-main',
|
|
61
|
+
},
|
|
62
|
+
analytics: {
|
|
63
|
+
brokers: ['kafka-analytics.svc.cluster.local:9092'],
|
|
64
|
+
clientId: 'my-service-analytics',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
50
67
|
});
|
|
51
68
|
|
|
52
|
-
// Publish
|
|
53
|
-
|
|
69
|
+
// Publish directly - no getKafka() needed!
|
|
70
|
+
// Producers initialize automatically on first publish
|
|
71
|
+
// TypeScript knows 'main' and 'analytics' are the only valid producer names!
|
|
72
|
+
await kafka.publish('main', 'user-events', {
|
|
54
73
|
userId: '123',
|
|
55
74
|
action: 'user.created',
|
|
56
75
|
timestamp: Date.now(),
|
|
57
76
|
});
|
|
58
77
|
|
|
59
|
-
// Publish
|
|
60
|
-
await kafka.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{ value: { userId: '123', action: 'login' } },
|
|
64
|
-
{ value: { userId: '456', action: 'logout' } },
|
|
65
|
-
],
|
|
78
|
+
// Publish to different producer - autocomplete suggests 'main' | 'analytics'
|
|
79
|
+
await kafka.publish('analytics', 'metrics', {
|
|
80
|
+
metric: 'user.signup',
|
|
81
|
+
value: 1,
|
|
66
82
|
});
|
|
67
83
|
|
|
68
|
-
//
|
|
69
|
-
await kafka.
|
|
84
|
+
// TypeScript error: Producer 'invalid' doesn't exist!
|
|
85
|
+
// await kafka.publish('invalid', 'topic', {}); // ❌ Type error
|
|
86
|
+
|
|
87
|
+
// Use in health checks
|
|
88
|
+
app.get('/health/ready', async (req, res) => {
|
|
89
|
+
const ready = await kafka.isReady();
|
|
90
|
+
res.status(ready ? 200 : 503).json({ ready });
|
|
91
|
+
});
|
|
70
92
|
```
|
|
71
93
|
|
|
72
94
|
## Usage
|
|
73
95
|
|
|
74
|
-
###
|
|
96
|
+
### Setup with Multiple Producers
|
|
75
97
|
|
|
76
|
-
|
|
98
|
+
Create a `kafka.ts` file in your service:
|
|
77
99
|
|
|
78
100
|
```typescript
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
import { KafkaManager } from '@autofleet/kafka';
|
|
102
|
+
import logger from './logger';
|
|
103
|
+
|
|
104
|
+
const ENABLE_KAFKA = process.env.ENABLE_KAFKA === 'true';
|
|
105
|
+
|
|
106
|
+
// No await needed - initialization is synchronous!
|
|
107
|
+
// Producers connect lazily on first publish
|
|
108
|
+
export const kafka = KafkaManager.create({
|
|
109
|
+
enabled: ENABLE_KAFKA,
|
|
110
|
+
logger,
|
|
111
|
+
producers: {
|
|
112
|
+
// Primary producer for main events
|
|
113
|
+
main: {
|
|
114
|
+
brokers: ['kafka-main.svc.cluster.local:9092'],
|
|
115
|
+
clientId: 'driver-ms-main',
|
|
116
|
+
autoCreateTopics: true,
|
|
117
|
+
},
|
|
118
|
+
// Secondary producer for analytics
|
|
119
|
+
analytics: {
|
|
120
|
+
brokers: ['kafka-analytics.svc.cluster.local:9092'],
|
|
121
|
+
clientId: 'driver-ms-analytics',
|
|
122
|
+
},
|
|
91
123
|
},
|
|
92
124
|
});
|
|
125
|
+
|
|
126
|
+
// Define your topics
|
|
127
|
+
export const TOPICS = {
|
|
128
|
+
DRIVER_CONSENT_V1: 'backend.driver.consent.v1',
|
|
129
|
+
DRIVER_ASSIGNED_V1: 'backend.driver.vehicle.assigned.v1',
|
|
130
|
+
DRIVER_UNASSIGNED_V1: 'backend.driver.vehicle.unassigned.v1',
|
|
131
|
+
} as const;
|
|
93
132
|
```
|
|
94
133
|
|
|
95
|
-
|
|
134
|
+
### Publishing Messages
|
|
96
135
|
|
|
97
|
-
|
|
136
|
+
Now you can use it directly throughout your service with full type safety:
|
|
98
137
|
|
|
99
138
|
```typescript
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
#### Publishing with Custom Headers
|
|
139
|
+
import { kafka, TOPICS } from './kafka';
|
|
140
|
+
|
|
141
|
+
// Publish directly - no getKafka() overhead!
|
|
142
|
+
// TypeScript validates producer names: 'main' | 'analytics'
|
|
143
|
+
await kafka.publish('main', TOPICS.DRIVER_CONSENT_V1, {
|
|
144
|
+
state: 'accepted',
|
|
145
|
+
driverId: '123',
|
|
146
|
+
fleetId: '456',
|
|
147
|
+
});
|
|
110
148
|
|
|
111
|
-
|
|
149
|
+
// With options - autocomplete works!
|
|
112
150
|
await kafka.publish(
|
|
113
|
-
'
|
|
114
|
-
|
|
151
|
+
'main', // TypeScript autocompletes 'main' | 'analytics'
|
|
152
|
+
TOPICS.DRIVER_ASSIGNED_V1,
|
|
153
|
+
{
|
|
154
|
+
driverId: '123',
|
|
155
|
+
vehicleId: '789',
|
|
156
|
+
},
|
|
115
157
|
{
|
|
158
|
+
key: `driver-123`, // Ensures ordering per driver
|
|
116
159
|
headers: {
|
|
117
160
|
'correlation-id': requestId,
|
|
118
|
-
'source': 'api-gateway',
|
|
119
|
-
'version': '1.0.0',
|
|
120
161
|
},
|
|
121
162
|
}
|
|
122
163
|
);
|
|
164
|
+
|
|
165
|
+
// TypeScript will error if you use a non-existent producer
|
|
166
|
+
// await kafka.publish('wrong', TOPICS.DRIVER_CONSENT_V1, {}); // ❌ Error!
|
|
123
167
|
```
|
|
124
168
|
|
|
125
|
-
|
|
169
|
+
### Batch Publishing
|
|
126
170
|
|
|
127
171
|
```typescript
|
|
128
|
-
await kafka.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
172
|
+
await kafka.publishBatch('main', {
|
|
173
|
+
topic: TOPICS.DRIVER_CONSENT_V1,
|
|
174
|
+
messages: [
|
|
175
|
+
{ value: { driverId: '123', state: 'accepted' } },
|
|
176
|
+
{ value: { driverId: '456', state: 'rejected' } },
|
|
177
|
+
{ value: { driverId: '789', state: 'pending' } },
|
|
178
|
+
],
|
|
179
|
+
});
|
|
135
180
|
```
|
|
136
181
|
|
|
137
|
-
###
|
|
182
|
+
### Mock Mode (Disable Kafka)
|
|
138
183
|
|
|
139
|
-
|
|
184
|
+
When `enabled: false`, all producers become mocks automatically:
|
|
140
185
|
|
|
141
186
|
```typescript
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
{
|
|
150
|
-
value: { userId: '456', action: 'logout' },
|
|
151
|
-
key: 'user-456',
|
|
187
|
+
const kafka = KafkaManager.create({
|
|
188
|
+
enabled: false, // or process.env.ENABLE_KAFKA !== 'true'
|
|
189
|
+
logger,
|
|
190
|
+
producers: {
|
|
191
|
+
main: {
|
|
192
|
+
brokers: ['kafka:9092'],
|
|
193
|
+
clientId: 'my-service',
|
|
152
194
|
},
|
|
153
|
-
|
|
154
|
-
value: { userId: '789', action: 'update' },
|
|
155
|
-
key: 'user-789',
|
|
156
|
-
headers: { priority: 'high' },
|
|
157
|
-
},
|
|
158
|
-
],
|
|
195
|
+
},
|
|
159
196
|
});
|
|
197
|
+
|
|
198
|
+
// This will log a debug message but not actually publish
|
|
199
|
+
await kafka.publish('main', 'topic', { data: 'test' });
|
|
200
|
+
// Output: "Kafka: [main] Mock mode - skipping publish to topic: topic"
|
|
160
201
|
```
|
|
161
202
|
|
|
162
|
-
###
|
|
203
|
+
### Health Checks & Readiness Probes
|
|
204
|
+
|
|
205
|
+
Use the built-in health check for Kubernetes readiness probes:
|
|
163
206
|
|
|
164
207
|
```typescript
|
|
165
|
-
|
|
166
|
-
|
|
208
|
+
import { kafka } from './kafka';
|
|
209
|
+
|
|
210
|
+
// Express/Fastify/etc
|
|
211
|
+
app.get('/health/ready', async (req, res) => {
|
|
212
|
+
const ready = await kafka.isReady();
|
|
213
|
+
res.status(ready ? 200 : 503).json({
|
|
214
|
+
ready,
|
|
215
|
+
kafka: kafka.getConnectionStatus(),
|
|
216
|
+
});
|
|
217
|
+
});
|
|
167
218
|
|
|
168
|
-
//
|
|
169
|
-
|
|
219
|
+
// Or manually check each producer
|
|
220
|
+
app.get('/health/detailed', async (req, res) => {
|
|
221
|
+
const status = kafka.getConnectionStatus();
|
|
222
|
+
const allConnected = Object.values(status).every(s => s);
|
|
170
223
|
|
|
171
|
-
|
|
172
|
-
|
|
224
|
+
res.status(allConnected ? 200 : 503).json({
|
|
225
|
+
producers: status,
|
|
226
|
+
enabled: kafka.isEnabled,
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Migration from getKafka() Pattern
|
|
232
|
+
|
|
233
|
+
**Before (problematic pattern):**
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
// kafka.ts
|
|
237
|
+
let kafkaInstance: AfKafka | null = null;
|
|
238
|
+
const ENABLE_KAFKA = process.env.ENABLE_KAFKA === 'true';
|
|
239
|
+
|
|
240
|
+
const disabledKafkaMock: Partial<AfKafka> = {
|
|
241
|
+
ping: async () => { logger.info('Kafka disabled'); },
|
|
242
|
+
publish: async (topic, message) => {
|
|
243
|
+
logger.debug('Skipping publish', { topic, message });
|
|
244
|
+
return [];
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
async function getKafka(): Promise<AfKafka> {
|
|
249
|
+
if (!ENABLE_KAFKA) {
|
|
250
|
+
return disabledKafkaMock as AfKafka;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (!kafkaInstance) {
|
|
254
|
+
const { default: Kafka } = await import('@autofleet/kafka');
|
|
255
|
+
kafkaInstance = await Kafka.create({
|
|
256
|
+
brokers: ['kafka:9092'],
|
|
257
|
+
clientId: 'my-service',
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return kafkaInstance;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Usage - awkward!
|
|
264
|
+
async function publishEvent() {
|
|
265
|
+
const kafka = await getKafka(); // Overhead on every call
|
|
266
|
+
await kafka.publish('topic', data);
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**After (clean pattern):**
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// kafka.ts
|
|
274
|
+
import { KafkaManager } from '@autofleet/kafka';
|
|
275
|
+
import logger from './logger';
|
|
276
|
+
|
|
277
|
+
// Synchronous initialization - no await!
|
|
278
|
+
export const kafka = KafkaManager.create({
|
|
279
|
+
enabled: process.env.ENABLE_KAFKA === 'true', // Built-in mock mode!
|
|
280
|
+
logger,
|
|
281
|
+
producers: {
|
|
282
|
+
main: {
|
|
283
|
+
brokers: ['kafka:9092'],
|
|
284
|
+
clientId: 'my-service',
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
export const TOPICS = {
|
|
290
|
+
MY_TOPIC: 'my.topic',
|
|
291
|
+
} as const;
|
|
292
|
+
|
|
293
|
+
// Usage - clean and direct!
|
|
294
|
+
async function publishEvent() {
|
|
295
|
+
await kafka.publish('main', TOPICS.MY_TOPIC, data); // Direct access!
|
|
296
|
+
}
|
|
173
297
|
```
|
|
174
298
|
|
|
175
299
|
## Configuration
|
|
176
300
|
|
|
177
|
-
###
|
|
301
|
+
### KafkaManagerOptions
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
interface KafkaManagerOptions {
|
|
305
|
+
// Enable/disable Kafka - when false, returns mock implementations
|
|
306
|
+
enabled?: boolean;
|
|
307
|
+
|
|
308
|
+
// Custom logger instance
|
|
309
|
+
logger?: LoggerInstanceManager;
|
|
310
|
+
|
|
311
|
+
// Skip automatic graceful shutdown (default: false)
|
|
312
|
+
dontGracefulShutdown?: boolean;
|
|
313
|
+
|
|
314
|
+
// Named producers configuration
|
|
315
|
+
producers: Record<string, ProducerConfig>;
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### ProducerConfig
|
|
178
320
|
|
|
179
321
|
```typescript
|
|
180
|
-
interface
|
|
322
|
+
interface ProducerConfig {
|
|
181
323
|
// Array of Kafka broker addresses (required)
|
|
182
324
|
brokers: string[];
|
|
183
325
|
|
|
184
326
|
// Client ID for this producer (optional)
|
|
185
327
|
clientId?: string;
|
|
186
328
|
|
|
187
|
-
//
|
|
188
|
-
|
|
329
|
+
// SASL authentication options (optional)
|
|
330
|
+
sasl?: {
|
|
331
|
+
mechanism: 'plain' | 'scram-sha-256' | 'scram-sha-512';
|
|
332
|
+
username: string;
|
|
333
|
+
password: string;
|
|
334
|
+
};
|
|
189
335
|
|
|
190
|
-
//
|
|
191
|
-
|
|
336
|
+
// Automatically create topics if they don't exist (default: false)
|
|
337
|
+
autoCreateTopics?: boolean;
|
|
192
338
|
}
|
|
193
339
|
```
|
|
194
340
|
|
|
195
|
-
The package uses [@platformatic/kafka](https://www.npmjs.com/package/@platformatic/kafka) with `stringSerializers` for automatic serialization. The ESM-only `@platformatic/kafka` library is loaded via dynamic imports, ensuring compatibility with both ESM and CommonJS environments.
|
|
196
|
-
|
|
197
341
|
## Advanced Features
|
|
198
342
|
|
|
199
343
|
### Message Keys for Partitioning
|
|
@@ -202,12 +346,12 @@ Use message keys to control partitioning and maintain order:
|
|
|
202
346
|
|
|
203
347
|
```typescript
|
|
204
348
|
// All messages for the same user go to the same partition
|
|
205
|
-
await kafka.publish('user-events', event, {
|
|
349
|
+
await kafka.publish('main', 'user-events', event, {
|
|
206
350
|
key: `user-${userId}`,
|
|
207
351
|
});
|
|
208
352
|
|
|
209
353
|
// All messages for the same order go to the same partition
|
|
210
|
-
await kafka.publish('order-events', event, {
|
|
354
|
+
await kafka.publish('main', 'order-events', event, {
|
|
211
355
|
key: `order-${orderId}`,
|
|
212
356
|
});
|
|
213
357
|
```
|
|
@@ -222,7 +366,7 @@ await kafka.publish('order-events', event, {
|
|
|
222
366
|
Headers are useful for metadata and message routing:
|
|
223
367
|
|
|
224
368
|
```typescript
|
|
225
|
-
await kafka.publish('events', data, {
|
|
369
|
+
await kafka.publish('main', 'events', data, {
|
|
226
370
|
headers: {
|
|
227
371
|
// Correlation ID for distributed tracing
|
|
228
372
|
'correlation-id': correlationId,
|
|
@@ -233,9 +377,6 @@ await kafka.publish('events', data, {
|
|
|
233
377
|
// Message schema version
|
|
234
378
|
'schema-version': '2.0',
|
|
235
379
|
|
|
236
|
-
// Content encoding
|
|
237
|
-
'encoding': 'json',
|
|
238
|
-
|
|
239
380
|
// Custom application headers
|
|
240
381
|
'tenant-id': 'tenant-123',
|
|
241
382
|
},
|
|
@@ -248,41 +389,60 @@ Direct partition control for advanced use cases:
|
|
|
248
389
|
|
|
249
390
|
```typescript
|
|
250
391
|
// Send to specific partition
|
|
251
|
-
await kafka.publish('events', data, {
|
|
392
|
+
await kafka.publish('main', 'events', data, {
|
|
252
393
|
partition: 0, // Always send to partition 0
|
|
253
394
|
});
|
|
254
395
|
|
|
255
396
|
// Balance across partitions using keys
|
|
256
397
|
const partitionKey = `${customerId % 10}`;
|
|
257
|
-
await kafka.publish('events', data, {
|
|
398
|
+
await kafka.publish('main', 'events', data, {
|
|
258
399
|
key: partitionKey,
|
|
259
400
|
});
|
|
260
401
|
```
|
|
261
402
|
|
|
262
403
|
## API Reference
|
|
263
404
|
|
|
264
|
-
### `
|
|
405
|
+
### `KafkaManager.create(options): KafkaManager<ProducerNames>`
|
|
265
406
|
|
|
266
|
-
Creates a new
|
|
407
|
+
Creates a new KafkaManager instance with multiple named producers. Producers are initialized lazily on first use. **Producer names are type-safe** - TypeScript will autocomplete and validate them based on your configuration.
|
|
267
408
|
|
|
268
409
|
**Parameters:**
|
|
269
410
|
- `options` - Configuration options (see [Configuration](#configuration))
|
|
270
411
|
|
|
271
|
-
**Returns:** A
|
|
412
|
+
**Returns:** A `KafkaManager<ProducerNames>` instance where `ProducerNames` are the keys from your producers config
|
|
272
413
|
|
|
273
414
|
**Example:**
|
|
274
415
|
```typescript
|
|
275
|
-
const kafka =
|
|
276
|
-
|
|
277
|
-
|
|
416
|
+
const kafka = KafkaManager.create({
|
|
417
|
+
enabled: true,
|
|
418
|
+
logger,
|
|
419
|
+
producers: {
|
|
420
|
+
main: {
|
|
421
|
+
brokers: ['kafka:9092'],
|
|
422
|
+
clientId: 'my-service',
|
|
423
|
+
},
|
|
424
|
+
analytics: {
|
|
425
|
+
brokers: ['kafka-analytics:9092'],
|
|
426
|
+
clientId: 'my-service-analytics',
|
|
427
|
+
},
|
|
428
|
+
},
|
|
278
429
|
});
|
|
430
|
+
|
|
431
|
+
// Type: KafkaManager<'main' | 'analytics'>
|
|
432
|
+
// TypeScript knows the valid producer names!
|
|
433
|
+
|
|
434
|
+
// Producers connect automatically on first publish
|
|
435
|
+
await kafka.publish('main', 'my-topic', { data: 'test' }); // ✅ Valid
|
|
436
|
+
await kafka.publish('analytics', 'metrics', { value: 1 }); // ✅ Valid
|
|
437
|
+
// await kafka.publish('wrong', 'topic', {}); // ❌ TypeScript error!
|
|
279
438
|
```
|
|
280
439
|
|
|
281
|
-
### `publish<T>(topic, value, options?): Promise<RecordMetadata[]>`
|
|
440
|
+
### `publish<T>(producerName, topic, value, options?): Promise<RecordMetadata[]>`
|
|
282
441
|
|
|
283
|
-
Publishes a single message to a topic.
|
|
442
|
+
Publishes a single message to a topic using a named producer. Producer name is **type-safe** - must match one of the configured producers.
|
|
284
443
|
|
|
285
444
|
**Parameters:**
|
|
445
|
+
- `producerName` - Name of the producer to use (type-safe: only accepts configured producer names)
|
|
286
446
|
- `topic` - Topic name
|
|
287
447
|
- `value` - Message value (will be JSON stringified)
|
|
288
448
|
- `options` (optional) - Publish options
|
|
@@ -291,19 +451,22 @@ Publishes a single message to a topic.
|
|
|
291
451
|
|
|
292
452
|
**Example:**
|
|
293
453
|
```typescript
|
|
294
|
-
|
|
454
|
+
// TypeScript autocompletes and validates producer names
|
|
455
|
+
const metadata = await kafka.publish('main', 'events', { id: 1, data: 'test' }, {
|
|
295
456
|
key: 'key-1',
|
|
296
457
|
headers: { type: 'create' },
|
|
297
458
|
});
|
|
298
459
|
|
|
299
|
-
|
|
460
|
+
// Type error if using non-existent producer
|
|
461
|
+
// await kafka.publish('nonexistent', 'topic', {}); // ❌ Error!
|
|
300
462
|
```
|
|
301
463
|
|
|
302
|
-
### `publishBatch(options): Promise<RecordMetadata[]>`
|
|
464
|
+
### `publishBatch(producerName, options): Promise<RecordMetadata[]>`
|
|
303
465
|
|
|
304
|
-
Publishes multiple messages in a batch
|
|
466
|
+
Publishes multiple messages in a batch using a named producer. Producer name is **type-safe**.
|
|
305
467
|
|
|
306
468
|
**Parameters:**
|
|
469
|
+
- `producerName` - Name of the producer to use (type-safe: only accepts configured producer names)
|
|
307
470
|
- `options.topic` - Topic name
|
|
308
471
|
- `options.messages` - Array of messages with values, keys, headers, etc.
|
|
309
472
|
|
|
@@ -311,7 +474,8 @@ Publishes multiple messages in a batch to a topic.
|
|
|
311
474
|
|
|
312
475
|
**Example:**
|
|
313
476
|
```typescript
|
|
314
|
-
|
|
477
|
+
// TypeScript validates 'main' is a configured producer
|
|
478
|
+
const metadata = await kafka.publishBatch('main', {
|
|
315
479
|
topic: 'events',
|
|
316
480
|
messages: [
|
|
317
481
|
{ value: { id: 1 }, key: 'key-1' },
|
|
@@ -320,62 +484,113 @@ const metadata = await kafka.publishBatch({
|
|
|
320
484
|
});
|
|
321
485
|
```
|
|
322
486
|
|
|
487
|
+
### `isReady(): Promise<boolean>`
|
|
488
|
+
|
|
489
|
+
Check if all producers are connected and ready. Useful for health checks.
|
|
490
|
+
|
|
491
|
+
**Example:**
|
|
492
|
+
```typescript
|
|
493
|
+
const ready = await kafka.isReady();
|
|
494
|
+
if (!ready) {
|
|
495
|
+
throw new Error('Kafka not ready');
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### `getConnectionStatus(): Record<ProducerNames, boolean>`
|
|
500
|
+
|
|
501
|
+
Get connection status for all producers. Return type is **type-safe** based on your configuration.
|
|
502
|
+
|
|
503
|
+
**Example:**
|
|
504
|
+
```typescript
|
|
505
|
+
const status = kafka.getConnectionStatus();
|
|
506
|
+
// Type: { main: boolean; analytics: boolean }
|
|
507
|
+
// { main: true, analytics: false }
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### `ping(): Promise<void>`
|
|
511
|
+
|
|
512
|
+
Ping all producers to verify connectivity.
|
|
513
|
+
|
|
514
|
+
**Example:**
|
|
515
|
+
```typescript
|
|
516
|
+
await kafka.ping(); // Throws if any producer fails to connect
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### `pingProducer(name): Promise<void>`
|
|
520
|
+
|
|
521
|
+
Ping a specific producer to verify connectivity. Producer name is **type-safe**.
|
|
522
|
+
|
|
523
|
+
**Example:**
|
|
524
|
+
```typescript
|
|
525
|
+
await kafka.pingProducer('main'); // ✅ Valid
|
|
526
|
+
// await kafka.pingProducer('invalid'); // ❌ Type error!
|
|
527
|
+
```
|
|
528
|
+
|
|
323
529
|
### `disconnect(): Promise<void>`
|
|
324
530
|
|
|
325
|
-
Disconnects
|
|
531
|
+
Disconnects all producers and cleans up resources.
|
|
326
532
|
|
|
327
533
|
**Example:**
|
|
328
534
|
```typescript
|
|
329
535
|
await kafka.disconnect();
|
|
330
536
|
```
|
|
331
537
|
|
|
538
|
+
### `disconnectProducer(name): Promise<void>`
|
|
539
|
+
|
|
540
|
+
Disconnects a specific producer and cleans up resources. Producer name is **type-safe**.
|
|
541
|
+
|
|
542
|
+
**Example:**
|
|
543
|
+
```typescript
|
|
544
|
+
await kafka.disconnectProducer('main'); // ✅ Valid
|
|
545
|
+
// await kafka.disconnectProducer('invalid'); // ❌ Type error!
|
|
546
|
+
```
|
|
547
|
+
|
|
332
548
|
## Best Practices
|
|
333
549
|
|
|
334
|
-
### 1. Use Centralized
|
|
550
|
+
### 1. Use Centralized Kafka Configuration
|
|
335
551
|
|
|
336
|
-
Define your topics in
|
|
552
|
+
Define your Kafka instance and topics in one place:
|
|
337
553
|
|
|
338
554
|
```typescript
|
|
555
|
+
// src/kafka.ts
|
|
556
|
+
import { KafkaManager } from '@autofleet/kafka';
|
|
557
|
+
import logger from './logger';
|
|
558
|
+
|
|
559
|
+
export const kafka = KafkaManager.create({
|
|
560
|
+
enabled: process.env.ENABLE_KAFKA === 'true',
|
|
561
|
+
logger,
|
|
562
|
+
producers: {
|
|
563
|
+
main: {
|
|
564
|
+
brokers: process.env.KAFKA_BROKERS?.split(',') || ['localhost:9092'],
|
|
565
|
+
clientId: 'my-service',
|
|
566
|
+
autoCreateTopics: process.env.NODE_ENV === 'development',
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
|
|
339
571
|
export const TOPICS = {
|
|
340
572
|
USER_EVENTS: 'user-events',
|
|
341
573
|
ORDER_EVENTS: 'order-events',
|
|
342
|
-
PAYMENT_EVENTS: 'payment-events',
|
|
343
574
|
} as const;
|
|
344
|
-
|
|
345
|
-
export type TopicName = typeof TOPICS[keyof typeof TOPICS];
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
Usage:
|
|
349
|
-
|
|
350
|
-
```typescript
|
|
351
|
-
import { TOPICS } from './topics';
|
|
352
|
-
|
|
353
|
-
await kafka.publish(TOPICS.USER_EVENTS, data);
|
|
354
575
|
```
|
|
355
576
|
|
|
356
577
|
### 2. Use Message Keys for Ordering
|
|
357
578
|
|
|
358
579
|
```typescript
|
|
359
580
|
// Ensure all events for a user are processed in order
|
|
360
|
-
await kafka.publish('
|
|
581
|
+
await kafka.publish('main', TOPICS.USER_EVENTS, event, {
|
|
361
582
|
key: `user-${userId}`,
|
|
362
583
|
});
|
|
363
|
-
|
|
364
|
-
// Ensure all events for a session are processed in order
|
|
365
|
-
await kafka.publish('session-events', event, {
|
|
366
|
-
key: `session-${sessionId}`,
|
|
367
|
-
});
|
|
368
584
|
```
|
|
369
585
|
|
|
370
586
|
### 3. Add Metadata with Headers
|
|
371
587
|
|
|
372
588
|
```typescript
|
|
373
|
-
await kafka.publish('events', data, {
|
|
589
|
+
await kafka.publish('main', 'events', data, {
|
|
374
590
|
headers: {
|
|
375
591
|
'correlation-id': requestId,
|
|
376
|
-
'source-service':
|
|
592
|
+
'source-service': 'user-service',
|
|
377
593
|
'event-type': eventType,
|
|
378
|
-
'schema-version': '1.0',
|
|
379
594
|
},
|
|
380
595
|
});
|
|
381
596
|
```
|
|
@@ -385,51 +600,61 @@ await kafka.publish('events', data, {
|
|
|
385
600
|
```typescript
|
|
386
601
|
// Instead of multiple publish calls
|
|
387
602
|
for (const event of events) {
|
|
388
|
-
await kafka.publish('events', event); // ❌ Slow
|
|
603
|
+
await kafka.publish('main', 'events', event); // ❌ Slow
|
|
389
604
|
}
|
|
390
605
|
|
|
391
606
|
// Use batch publishing
|
|
392
|
-
await kafka.publishBatch({
|
|
607
|
+
await kafka.publishBatch('main', {
|
|
393
608
|
topic: 'events',
|
|
394
609
|
messages: events.map(e => ({ value: e })), // ✅ Fast
|
|
395
610
|
});
|
|
396
611
|
```
|
|
397
612
|
|
|
398
|
-
### 5.
|
|
613
|
+
### 5. Integrate with Health Checks
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
app.get('/health/ready', async (req, res) => {
|
|
617
|
+
const ready = await kafka.isReady();
|
|
618
|
+
res.status(ready ? 200 : 503).json({ ready });
|
|
619
|
+
});
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### 6. Handle Errors Gracefully
|
|
399
623
|
|
|
400
624
|
```typescript
|
|
401
625
|
try {
|
|
402
|
-
await kafka.publish('events', data);
|
|
626
|
+
await kafka.publish('main', 'events', data);
|
|
403
627
|
} catch (error) {
|
|
404
628
|
logger.error('Failed to publish to Kafka', { error, data });
|
|
405
|
-
// Consider:
|
|
406
|
-
// - Retry logic
|
|
407
|
-
// - Fallback to queue
|
|
408
|
-
// - Alert monitoring
|
|
629
|
+
// Consider: retry logic, fallback queue, monitoring alerts
|
|
409
630
|
}
|
|
410
631
|
```
|
|
411
632
|
|
|
412
|
-
###
|
|
633
|
+
### 7. Use Multiple Producers for Different Clusters
|
|
413
634
|
|
|
414
635
|
```typescript
|
|
415
|
-
const kafka =
|
|
416
|
-
|
|
417
|
-
|
|
636
|
+
const kafka = KafkaManager.create({
|
|
637
|
+
enabled: true,
|
|
638
|
+
logger,
|
|
639
|
+
producers: {
|
|
640
|
+
// Production events
|
|
641
|
+
main: {
|
|
642
|
+
brokers: ['kafka-prod.svc.cluster.local:9092'],
|
|
643
|
+
clientId: 'my-service-main',
|
|
644
|
+
},
|
|
645
|
+
// Analytics (separate cluster)
|
|
646
|
+
analytics: {
|
|
647
|
+
brokers: ['kafka-analytics.svc.cluster.local:9092'],
|
|
648
|
+
clientId: 'my-service-analytics',
|
|
649
|
+
},
|
|
650
|
+
},
|
|
418
651
|
});
|
|
419
|
-
```
|
|
420
652
|
|
|
421
|
-
|
|
653
|
+
// Critical events go to main cluster
|
|
654
|
+
await kafka.publish('main', 'orders', orderData);
|
|
422
655
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
// Log metrics
|
|
427
|
-
logger.info('Message published', {
|
|
428
|
-
topic: 'events',
|
|
429
|
-
partition: metadata[0].partition,
|
|
430
|
-
offset: metadata[0].offset,
|
|
431
|
-
latency: Date.now() - startTime,
|
|
432
|
-
});
|
|
656
|
+
// Analytics go to analytics cluster
|
|
657
|
+
await kafka.publish('analytics', 'metrics', metricsData);
|
|
433
658
|
```
|
|
434
659
|
|
|
435
660
|
## Testing
|
|
@@ -450,27 +675,36 @@ pnpm run coverage
|
|
|
450
675
|
|
|
451
676
|
```typescript
|
|
452
677
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
453
|
-
import
|
|
454
|
-
|
|
455
|
-
describe('
|
|
456
|
-
let kafka:
|
|
457
|
-
|
|
458
|
-
beforeEach(
|
|
459
|
-
kafka =
|
|
460
|
-
|
|
461
|
-
|
|
678
|
+
import { KafkaManager } from '@autofleet/kafka';
|
|
679
|
+
|
|
680
|
+
describe('KafkaService', () => {
|
|
681
|
+
let kafka: KafkaManager;
|
|
682
|
+
|
|
683
|
+
beforeEach(() => {
|
|
684
|
+
kafka = KafkaManager.create({
|
|
685
|
+
enabled: false, // Use mock mode for tests
|
|
686
|
+
producers: {
|
|
687
|
+
main: {
|
|
688
|
+
brokers: ['localhost:9092'],
|
|
689
|
+
clientId: 'test-client',
|
|
690
|
+
},
|
|
691
|
+
},
|
|
462
692
|
});
|
|
463
693
|
});
|
|
464
694
|
|
|
465
|
-
it('should publish event successfully', async () => {
|
|
466
|
-
const result = await kafka.publish('test-topic', {
|
|
695
|
+
it('should publish event successfully in mock mode', async () => {
|
|
696
|
+
const result = await kafka.publish('main', 'test-topic', {
|
|
467
697
|
userId: '123',
|
|
468
698
|
action: 'test',
|
|
469
699
|
});
|
|
470
700
|
|
|
471
701
|
expect(result).toBeDefined();
|
|
472
|
-
expect(result[0]).toHaveProperty('
|
|
473
|
-
|
|
702
|
+
expect(result[0]).toHaveProperty('topic', 'test-topic');
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it('should report ready status', async () => {
|
|
706
|
+
const ready = await kafka.isReady();
|
|
707
|
+
expect(ready).toBe(true);
|
|
474
708
|
});
|
|
475
709
|
});
|
|
476
710
|
```
|
|
@@ -480,99 +714,30 @@ describe('MyKafkaService', () => {
|
|
|
480
714
|
Common environment variable patterns:
|
|
481
715
|
|
|
482
716
|
```bash
|
|
483
|
-
# Kafka
|
|
717
|
+
# Enable/disable Kafka
|
|
718
|
+
ENABLE_KAFKA=true
|
|
719
|
+
|
|
720
|
+
# Kafka brokers (comma-separated)
|
|
484
721
|
KAFKA_BROKERS=kafka1:9092,kafka2:9092,kafka3:9092
|
|
485
722
|
|
|
486
723
|
# Client configuration
|
|
487
724
|
KAFKA_CLIENT_ID=my-service
|
|
488
|
-
KAFKA_SASL_USERNAME=user
|
|
489
|
-
KAFKA_SASL_PASSWORD=pass
|
|
490
725
|
```
|
|
491
726
|
|
|
492
727
|
Usage:
|
|
493
728
|
|
|
494
729
|
```typescript
|
|
495
|
-
const kafka =
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
### Event Sourcing
|
|
504
|
-
|
|
505
|
-
```typescript
|
|
506
|
-
await kafka.publish('user-events', {
|
|
507
|
-
eventType: 'UserCreated',
|
|
508
|
-
aggregateId: userId,
|
|
509
|
-
timestamp: Date.now(),
|
|
510
|
-
data: userData,
|
|
511
|
-
}, {
|
|
512
|
-
key: userId,
|
|
513
|
-
headers: {
|
|
514
|
-
'event-type': 'UserCreated',
|
|
515
|
-
'aggregate-type': 'User',
|
|
516
|
-
},
|
|
517
|
-
});
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
### Change Data Capture (CDC)
|
|
521
|
-
|
|
522
|
-
```typescript
|
|
523
|
-
await kafka.publish('database-changes', {
|
|
524
|
-
operation: 'INSERT',
|
|
525
|
-
table: 'users',
|
|
526
|
-
before: null,
|
|
527
|
-
after: newUserRecord,
|
|
528
|
-
}, {
|
|
529
|
-
key: newUserRecord.id,
|
|
530
|
-
headers: {
|
|
531
|
-
'schema': 'public',
|
|
532
|
-
'table': 'users',
|
|
730
|
+
const kafka = KafkaManager.create({
|
|
731
|
+
enabled: process.env.ENABLE_KAFKA === 'true',
|
|
732
|
+
producers: {
|
|
733
|
+
main: {
|
|
734
|
+
brokers: process.env.KAFKA_BROKERS?.split(',') || ['localhost:9092'],
|
|
735
|
+
clientId: process.env.KAFKA_CLIENT_ID || 'default-client',
|
|
736
|
+
},
|
|
533
737
|
},
|
|
534
738
|
});
|
|
535
739
|
```
|
|
536
740
|
|
|
537
|
-
### Dead Letter Queue Pattern
|
|
538
|
-
|
|
539
|
-
```typescript
|
|
540
|
-
try {
|
|
541
|
-
await processMessage(message);
|
|
542
|
-
} catch (error) {
|
|
543
|
-
// Send to DLQ for manual review
|
|
544
|
-
await kafka.publish('events-dlq', {
|
|
545
|
-
originalTopic: 'events',
|
|
546
|
-
error: error.message,
|
|
547
|
-
message: message,
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
## Troubleshooting
|
|
553
|
-
|
|
554
|
-
### Connection Issues
|
|
555
|
-
|
|
556
|
-
```typescript
|
|
557
|
-
// Use multiple brokers for redundancy
|
|
558
|
-
const kafka = await AfKafka.create({
|
|
559
|
-
brokers: ['kafka1:9092', 'kafka2:9092', 'kafka3:9092'],
|
|
560
|
-
clientId: 'my-service',
|
|
561
|
-
});
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
### Performance Tuning
|
|
565
|
-
|
|
566
|
-
For high-throughput scenarios, use batch publishing:
|
|
567
|
-
|
|
568
|
-
```typescript
|
|
569
|
-
// Batch multiple messages together
|
|
570
|
-
await kafka.publishBatch({
|
|
571
|
-
topic: 'events',
|
|
572
|
-
messages: events.map(e => ({ value: e })),
|
|
573
|
-
});
|
|
574
|
-
```
|
|
575
|
-
|
|
576
741
|
## License
|
|
577
742
|
|
|
578
743
|
Proprietary - Autofleet
|