@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 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 publishing messages to Kafka topics.
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
- - [Batch Publishing](#batch-publishing)
13
- - [Managing Connection](#managing-connection)
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
- - **Simple Producer API** - Easy-to-use interface for publishing messages
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
- - **Type Safety** - Full TypeScript support
38
- - **ESM & CommonJS Compatible** - Works in both ESM and CommonJS projects via dynamic imports
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 AfKafka from '@autofleet/kafka';
45
-
46
- // Initialize the producer (async factory)
47
- const kafka = await AfKafka.create({
48
- brokers: ['localhost:9092'],
49
- clientId: 'my-service',
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 a message
53
- await kafka.publish('user-events', {
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 multiple messages in a batch
60
- await kafka.publishBatch({
61
- topic: 'user-events',
62
- messages: [
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
- // Disconnect when done
69
- await kafka.disconnect();
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
- ### Publishing Messages
96
+ ### Setup with Multiple Producers
75
97
 
76
- #### Basic Publishing
98
+ Create a `kafka.ts` file in your service:
77
99
 
78
100
  ```typescript
79
- const kafka = await AfKafka.create({
80
- brokers: ['localhost:9092', 'localhost:9093'],
81
- clientId: 'user-service',
82
- });
83
-
84
- // Publish a simple message
85
- await kafka.publish('user-events', {
86
- userId: '123',
87
- event: 'profile_updated',
88
- data: {
89
- name: 'John Doe',
90
- email: 'john@example.com',
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
- #### Publishing with Message Key
134
+ ### Publishing Messages
96
135
 
97
- Message keys determine which partition a message goes to, ensuring messages with the same key are always sent to the same partition (maintaining order).
136
+ Now you can use it directly throughout your service with full type safety:
98
137
 
99
138
  ```typescript
100
- await kafka.publish(
101
- 'user-events',
102
- { userId: '123', action: 'update' },
103
- {
104
- key: 'user-123', // All messages with this key go to the same partition
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
- ```typescript
149
+ // With options - autocomplete works!
112
150
  await kafka.publish(
113
- 'user-events',
114
- { userId: '123', action: 'login' },
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
- #### Publishing to Specific Partition
169
+ ### Batch Publishing
126
170
 
127
171
  ```typescript
128
- await kafka.publish(
129
- 'user-events',
130
- { userId: '123', action: 'update' },
131
- {
132
- partition: 2, // Send directly to partition 2
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
- ### Batch Publishing
182
+ ### Mock Mode (Disable Kafka)
138
183
 
139
- Batch publishing is more efficient for sending multiple messages:
184
+ When `enabled: false`, all producers become mocks automatically:
140
185
 
141
186
  ```typescript
142
- await kafka.publishBatch({
143
- topic: 'user-events',
144
- messages: [
145
- {
146
- value: { userId: '123', action: 'login' },
147
- key: 'user-123',
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
- ### Managing Connection
203
+ ### Health Checks & Readiness Probes
204
+
205
+ Use the built-in health check for Kubernetes readiness probes:
163
206
 
164
207
  ```typescript
165
- // The producer connects automatically on first publish
166
- await kafka.publish('my-topic', { data: 'value' });
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
- // Disconnect when you're done
169
- await kafka.disconnect();
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
- // Graceful shutdown is automatic on SIGTERM/SIGINT
172
- // unless dontGracefulShutdown is set to true
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
- ### KafkaOptions
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 KafkaOptions {
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
- // Custom logger instance (optional)
188
- logger?: LoggerInstanceManager;
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
- // Skip automatic graceful shutdown (default: false)
191
- dontGracefulShutdown?: boolean;
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
- ### `AfKafka.create(options): Promise<AfKafka>`
405
+ ### `KafkaManager.create(options): KafkaManager<ProducerNames>`
265
406
 
266
- Creates a new Kafka producer instance using an async factory pattern. This method dynamically imports the ESM-only `@platformatic/kafka` library, making it compatible with both ESM and CommonJS environments.
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 Promise that resolves to an `AfKafka` instance
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 = await AfKafka.create({
276
- brokers: ['localhost:9092'],
277
- clientId: 'my-service',
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
- const metadata = await kafka.publish('events', { id: 1, data: 'test' }, {
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
- console.log(`Published to partition ${metadata[0].partition} at offset ${metadata[0].offset}`);
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 to a topic.
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
- const metadata = await kafka.publishBatch({
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 the producer and cleans up resources.
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 Topic Definitions
550
+ ### 1. Use Centralized Kafka Configuration
335
551
 
336
- Define your topics in `src/topics.ts`:
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('user-events', event, {
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': serviceName,
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. Handle Errors Gracefully
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
- ### 6. Use Multiple Brokers for High Availability
633
+ ### 7. Use Multiple Producers for Different Clusters
413
634
 
414
635
  ```typescript
415
- const kafka = await AfKafka.create({
416
- brokers: ['kafka1:9092', 'kafka2:9092', 'kafka3:9092'],
417
- clientId: 'my-service',
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
- ### 7. Monitor Production Metrics
653
+ // Critical events go to main cluster
654
+ await kafka.publish('main', 'orders', orderData);
422
655
 
423
- ```typescript
424
- const metadata = await kafka.publish('events', data);
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 AfKafka from '@autofleet/kafka';
454
-
455
- describe('MyKafkaService', () => {
456
- let kafka: AfKafka;
457
-
458
- beforeEach(async () => {
459
- kafka = await AfKafka.create({
460
- brokers: ['localhost:9092'],
461
- clientId: 'test-client',
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('partition');
473
- expect(result[0]).toHaveProperty('offset');
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 brokers
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 = await AfKafka.create({
496
- brokers: process.env.KAFKA_BROKERS?.split(',') || ['localhost:9092'],
497
- clientId: process.env.KAFKA_CLIENT_ID || 'default-client',
498
- });
499
- ```
500
-
501
- ## Common Patterns
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