@autofleet/kafka 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,17 @@
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 managing multiple Kafka producers.
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 with type-safe topic definitions.
4
4
 
5
5
  ## Table of Contents
6
6
 
7
7
  - [Installation](#installation)
8
8
  - [Features](#features)
9
9
  - [Quick Start](#quick-start)
10
+ - [Typed Topics](#typed-topics)
11
+ - [Benefits](#benefits)
12
+ - [Basic Usage](#basic-usage)
13
+ - [Complex Schemas](#complex-schemas)
14
+ - [Skipping Validation](#skipping-validation)
10
15
  - [Initialization & Bootstrap](#initialization--bootstrap)
11
16
  - [Lifecycle Overview](#lifecycle-overview)
12
17
  - [Production Bootstrap Pattern](#production-bootstrap-pattern)
@@ -35,17 +40,16 @@ npm add @autofleet/kafka
35
40
 
36
41
  ## Features
37
42
 
43
+ - **Typed Topics with Zod** - Define topics with Zod schemas for compile-time type safety and runtime validation
38
44
  - **Multi-Producer Management** - Manage multiple named producers with different broker configurations
39
- - **Type-Safe Producer Names** - Producer names are typed based on your configuration with full autocomplete
45
+ - **Type-Safe APIs** - Full TypeScript support with inferred types for topics and payloads
40
46
  - **Built-in Mock Mode** - Disable Kafka entirely with automatic mock implementations
41
- - **Direct API Access** - Access producers directly with a clean, simple interface
42
47
  - **Centralized Health Checks** - Single readiness check for all producers
43
48
  - **Automatic Connection Management** - Handles connection lifecycle automatically
44
- - **Batch Publishing** - Efficient batch message sending
49
+ - **Batch Publishing** - Efficient batch message sending with type safety
45
50
  - **Message Partitioning** - Control message distribution across partitions
46
51
  - **Custom Headers** - Attach metadata to messages
47
52
  - **Graceful Shutdown** - Automatic cleanup on process termination
48
- - **Full TypeScript Support** - Type-safe APIs with IntelliSense autocomplete
49
53
  - **ESM & CommonJS Compatible** - Works in both ESM and CommonJS projects
50
54
  - **Production Ready** - Built with reliability and performance in mind
51
55
 
@@ -53,9 +57,10 @@ npm add @autofleet/kafka
53
57
 
54
58
  ```typescript
55
59
  import { KafkaManager } from '@autofleet/kafka';
60
+ import { z } from 'zod';
56
61
  import logger from './logger';
57
62
 
58
- // Initialize with multiple named producers (synchronous!)
63
+ // Define typed topics with Zod schemas
59
64
  const kafka = KafkaManager.create({
60
65
  enabled: process.env.ENABLE_KAFKA === 'true', // Built-in mock mode
61
66
  logger,
@@ -69,25 +74,55 @@ const kafka = KafkaManager.create({
69
74
  clientId: 'my-service-analytics',
70
75
  },
71
76
  },
77
+ topics: {
78
+ 'order.created': {
79
+ schema: z.object({
80
+ orderId: z.string(),
81
+ customerId: z.string(),
82
+ amount: z.number(),
83
+ items: z.array(z.object({
84
+ sku: z.string(),
85
+ quantity: z.number(),
86
+ })),
87
+ }),
88
+ producer: 'main',
89
+ },
90
+ 'user.signup': {
91
+ schema: z.object({
92
+ userId: z.string(),
93
+ email: z.string().email(),
94
+ timestamp: z.number(),
95
+ }),
96
+ producer: 'analytics',
97
+ },
98
+ },
72
99
  });
73
100
 
74
- // Publish messages directly
75
- // Producers initialize automatically on first publish
76
- // TypeScript knows 'main' and 'analytics' are the only valid producer names!
77
- await kafka.publish('main', 'user-events', {
78
- userId: '123',
79
- action: 'user.created',
80
- timestamp: Date.now(),
101
+ // Publish with full type safety and runtime validation
102
+ // TypeScript infers payload type from Zod schema!
103
+ await kafka.publish('order.created', {
104
+ orderId: 'ORD-123',
105
+ customerId: 'CUST-456',
106
+ amount: 99.99,
107
+ items: [
108
+ { sku: 'WIDGET-1', quantity: 2 },
109
+ { sku: 'GADGET-2', quantity: 1 },
110
+ ],
81
111
  });
82
112
 
83
- // Publish to different producer - autocomplete suggests 'main' | 'analytics'
84
- await kafka.publish('analytics', 'metrics', {
85
- metric: 'user.signup',
86
- value: 1,
87
- });
113
+ // Type error! TypeScript knows the exact shape expected
114
+ // await kafka.publish('order.created', {
115
+ // orderId: 123, // ❌ Error: Expected string
116
+ // // ❌ Error: Missing required fields
117
+ // });
88
118
 
89
- // TypeScript error: Producer 'invalid' doesn't exist!
90
- // await kafka.publish('invalid', 'topic', {}); // ❌ Type error
119
+ // Batch publish with type safety
120
+ await kafka.publishBatch('order.created', {
121
+ messages: [
122
+ { value: { orderId: 'ORD-1', customerId: 'CUST-1', amount: 50, items: [] } },
123
+ { value: { orderId: 'ORD-2', customerId: 'CUST-2', amount: 75, items: [] } },
124
+ ],
125
+ });
91
126
 
92
127
  // Use in health checks
93
128
  app.get('/health/ready', async (req, res) => {
@@ -96,6 +131,139 @@ app.get('/health/ready', async (req, res) => {
96
131
  });
97
132
  ```
98
133
 
134
+ ## Typed Topics
135
+
136
+ The typed topics feature provides compile-time type safety and runtime validation for Kafka messages using Zod schemas.
137
+
138
+ ### Benefits
139
+
140
+ - **Type Safety**: Payload types are automatically inferred from Zod schemas
141
+ - **Runtime Validation**: Messages are validated before publishing
142
+ - **DX**: Full autocomplete for topic names and payloads in your IDE
143
+ - **Centralized**: Topic definitions live alongside your Kafka configuration
144
+ - **Producer Routing**: Topics are automatically routed to the correct producer
145
+
146
+ ### Basic Usage
147
+
148
+ ```typescript
149
+ import { z } from 'zod';
150
+
151
+ const kafka = KafkaManager.create({
152
+ logger,
153
+ producers: {
154
+ main: { brokers: ['localhost:9092'] },
155
+ analytics: { brokers: ['localhost:9093'] },
156
+ },
157
+ topics: {
158
+ 'order.created': {
159
+ schema: z.object({
160
+ orderId: z.string().uuid(),
161
+ customerId: z.string().uuid(),
162
+ amount: z.number().positive(),
163
+ currency: z.enum(['USD', 'EUR', 'GBP']),
164
+ }),
165
+ producer: 'main', // Routes to 'main' producer
166
+ },
167
+ 'user.activity': {
168
+ schema: z.object({
169
+ userId: z.string(),
170
+ action: z.string(),
171
+ metadata: z.record(z.unknown()).optional(),
172
+ }),
173
+ producer: 'analytics', // Routes to 'analytics' producer
174
+ },
175
+ },
176
+ });
177
+
178
+ // ✅ Fully typed - TypeScript knows the exact shape
179
+ await kafka.publish('order.created', {
180
+ orderId: '550e8400-e29b-41d4-a716-446655440000',
181
+ customerId: '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
182
+ amount: 99.99,
183
+ currency: 'USD',
184
+ });
185
+
186
+ // ❌ Type error: Wrong type for 'amount'
187
+ // await kafka.publish('order.created', {
188
+ // orderId: '550e8400-e29b-41d4-a716-446655440000',
189
+ // customerId: '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
190
+ // amount: '99.99', // Error: Expected number, got string
191
+ // currency: 'USD',
192
+ // });
193
+
194
+ // ❌ Runtime validation error: Invalid enum value
195
+ // await kafka.publish('order.created', {
196
+ // orderId: '550e8400-e29b-41d4-a716-446655440000',
197
+ // customerId: '6ba7b810-9dad-11d1-80b4-00c04fd430c8',
198
+ // amount: 99.99,
199
+ // currency: 'JPY', // Error: Invalid enum value (not in ['USD', 'EUR', 'GBP'])
200
+ // });
201
+ ```
202
+
203
+ ### Complex Schemas
204
+
205
+ Support for nested objects, arrays, and advanced Zod features:
206
+
207
+ ```typescript
208
+ const kafka = KafkaManager.create({
209
+ logger,
210
+ producers: {
211
+ main: { brokers: ['localhost:9092'] },
212
+ },
213
+ topics: {
214
+ 'order.created': {
215
+ schema: z.object({
216
+ orderId: z.string(),
217
+ customer: z.object({
218
+ id: z.string(),
219
+ email: z.string().email(),
220
+ name: z.string().min(1),
221
+ }),
222
+ items: z.array(z.object({
223
+ sku: z.string(),
224
+ quantity: z.number().positive(),
225
+ price: z.number().positive(),
226
+ })).min(1),
227
+ total: z.number().positive(),
228
+ metadata: z.record(z.unknown()).optional(),
229
+ }),
230
+ producer: 'main',
231
+ },
232
+ },
233
+ });
234
+
235
+ await kafka.publish('order.created', {
236
+ orderId: '123',
237
+ customer: {
238
+ id: '456',
239
+ email: 'test@example.com',
240
+ name: 'John Doe',
241
+ },
242
+ items: [
243
+ { sku: 'ABC', quantity: 2, price: 10.00 },
244
+ { sku: 'DEF', quantity: 1, price: 20.00 },
245
+ ],
246
+ total: 40.00,
247
+ metadata: { source: 'web' },
248
+ });
249
+ ```
250
+
251
+ ### Skipping Validation
252
+
253
+ For advanced use cases, you can skip runtime validation:
254
+
255
+ ```typescript
256
+ await kafka.publish('order.created', payload, {
257
+ skipValidation: true, // Skip Zod validation
258
+ });
259
+
260
+ // Also works for batch publishing
261
+ await kafka.publishBatch('order.created', {
262
+ messages: [{ value: payload1 }, { value: payload2 }],
263
+ skipValidation: true,
264
+ });
265
+ ```
266
+
99
267
  ## Initialization & Bootstrap
100
268
 
101
269
  ### Lifecycle Overview
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
1
  let e=require(`node:timers/promises`);var t=class extends Error{constructor(...e){super(...e),this.name=`KafkaError`}};const n=`x-timestamp`,r=`autofleet-kafka-producer`;var i=class{constructor(e,t,n){this.name=e,this.config=t,this.logger=n,this.producer=null,this._isConnected=!1,this.initPromise=null,this._lastPingAt=null,this._lastPingSucceededAt=null,this._lastError=null,this._clusterId=null,this._brokerCount=0}async#e(){let{Producer:e,stringSerializers:t}=await import(`@platformatic/kafka`);this.producer=new e({bootstrapBrokers:this.config.brokers,clientId:this.config.clientId||`${r}-${this.name}`,serializers:t,autocreateTopics:this.config.autoCreateTopics??!1,sasl:this.config.sasl,tls:this.config.tls}),this.logger.info(`Kafka: [${this.name}] Initialized producer`,{clientId:this.config.clientId||`${r}-${this.name}`,brokers:this.config.brokers}),this.initPromise=null}async initialize(){this.producer||(this.initPromise??=this.#e(),await this.initPromise)}get isConnected(){return this._isConnected}getHealth(){return{name:this.name,enabled:!0,isConnected:this._isConnected,lastPingAt:this._lastPingAt,lastPingSucceededAt:this._lastPingSucceededAt,lastError:this._lastError,clusterId:this._clusterId,brokerCount:this._brokerCount,brokers:this.config.brokers}}buildConnectionErrorMessage(e){let t=this.config.brokers.join(`, `),n=this.config.brokers[0],[i,a]=n?n.split(`:`):[``,``];return`[${this.name}] Failed to connect to Kafka brokers: ${e.message}\n\nPossible causes:\n 1. Brokers are unreachable: ${t}\n 2. Network connectivity issues\n 3. Firewall blocking ports\n 4. ${this.config.sasl?`SASL authentication failed`:`Authentication not configured but required`}\n\nTroubleshooting:\n - Verify brokers are running and accessible\n`+(i&&a?` - Test connectivity: nc -zv ${i} ${a}\n`:``)+` - Check network policies and firewall rules\n ${this.config.sasl?`- Verify SASL credentials are correct
2
- `:``}\nCurrent configuration:\n Brokers: ${t}\n Client ID: ${this.config.clientId||`${r}-${this.name}`}\n SASL: ${this.config.sasl?`enabled (${this.config.sasl.mechanism})`:`disabled`}\n SASL Password: ${this.config.sasl?this.config.sasl.password?`****`:`not set`:`N/A`}\n Auto-create topics: ${this.config.autoCreateTopics??!1}`}async ping(n){if(this._lastPingAt=Date.now(),!(this._isConnected&&!n?.force)){await this.initialize();try{let t=n?.timeout??5e3,r=Error(`Connection timeout after ${t}ms`),i=(0,e.setTimeout)(t).then(()=>{throw r}),a=await Promise.race([this.producer.metadata({topics:[]}),i]);this._isConnected=!0,this._lastPingSucceededAt=Date.now(),this._clusterId=a.id,this._brokerCount=a.brokers.size,this._lastError=null,this.logger.info(`Kafka: [${this.name}] Successfully connected to cluster`,{clusterId:a.id,brokers:a.brokers.size,duration:Date.now()-this._lastPingAt})}catch(e){let n=e;throw this._isConnected=!1,this._lastError={message:n.message,timestamp:Date.now(),stack:n.stack},this.logger.error(`Kafka: [${this.name}] Failed to connect to brokers`,{error:n.message,duration:Date.now()-this._lastPingAt}),new t(this.buildConnectionErrorMessage(n),{cause:n})}}}async publish(e,r,i){if(!e)throw new t(`[${this.name}] Topic name is required`);await this.ping();try{let t={[n]:Date.now().toString(),...i?.headers},a=await this.producer.send({messages:[{topic:e,value:JSON.stringify(r),key:i?.key,partition:i?.partition,headers:t}]});return this.logger.debug(`Kafka: [${this.name}] Published message to topic ${e}`,{topic:e}),[{topic:e,partition:i?.partition??0,offset:a?.offsets?.[0]?.offset?.toString()??`0`}]}catch(n){let i=n;throw this.logger.error(`Kafka: [${this.name}] Error publishing to topic ${e}`,{error:n,value:r}),new t(`[${this.name}] Failed to publish to topic ${e}: ${i.message||`Unknown error`}`,{cause:n})}}async publishBatch(e){if(!e.topic)throw new t(`[${this.name}] Topic name is required`);if(!e.messages||e.messages.length===0)throw new t(`[${this.name}] At least one message is required`);await this.ping();try{let t=e.messages.map(t=>({topic:e.topic,value:JSON.stringify(t.value),key:t.key,partition:t.partition,headers:{[n]:Date.now().toString(),...t.headers}})),r=await this.producer.send({messages:t});return this.logger.debug(`Kafka: [${this.name}] Published ${t.length} messages to topic ${e.topic}`,{topic:e.topic,count:t.length}),t.map((t,n)=>({topic:e.topic,partition:t.partition??0,offset:r?.offsets?.[n]?.offset?.toString()??n.toString()}))}catch(n){let r=n;throw this.logger.error(`Kafka: [${this.name}] Error publishing batch to topic ${e.topic}`,{error:n}),new t(`[${this.name}] Failed to publish batch to topic ${e.topic}: ${r.message||`Unknown error`}`,{cause:n})}}async disconnect(){if(!this.producer||!this._isConnected){this.logger.debug(`Kafka: [${this.name}] Producer already disconnected`);return}this.logger.info(`Kafka: [${this.name}] Disconnecting producer`);try{await this.producer.close(),this._isConnected=!1,this.logger.info(`Kafka: [${this.name}] Producer disconnected successfully`)}catch(e){throw this.logger.error(`Kafka: [${this.name}] Error disconnecting producer`,{error:e}),new t(`[${this.name}] Failed to disconnect producer: ${e.message}`,{cause:e})}}},a=class{constructor(e,t,n){this.name=e,this.brokers=t,this.logger=n,this.isConnected=!0}getHealth(){return{name:this.name,enabled:!1,isConnected:!0,lastPingAt:null,lastPingSucceededAt:null,lastError:null,clusterId:`mock-cluster`,brokerCount:0,brokers:this.brokers}}async ping(){return this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping ping`),Promise.resolve()}async publish(e,t,n){return this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping publish to topic: ${e}`,{value:t}),Promise.resolve([{topic:e,partition:0,offset:`0`}])}async publishBatch(e){return this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping batch publish to topic: ${e.topic}`,{messageCount:e.messages.length}),Promise.resolve(e.messages.map(()=>({topic:e.topic,partition:0,offset:`0`})))}async disconnect(){return this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping disconnect`),Promise.resolve()}},o=class e{constructor(e){if(this.producers=new Map,this.gracefulShutdownStarted=!1,this.fatalError=null,this.lastReadinessCheck=null,this.logger=e.logger,this.enabled=e.enabled??!0,this.healthCheckTimeoutMs=e.healthCheckTimeoutMs??5e3,this.healthCheckCacheMs=e.healthCheckCacheMs??1e3,this.bootstrapTimeoutMs=e.bootstrapTimeoutMs??3e4,this.strictBootstrap=e.strictBootstrap??!0,this.logger.info(`Kafka: Initialized KafkaManager`,{enabled:this.enabled,producerCount:Object.keys(e.producers).length,producers:Object.keys(e.producers)}),this.enabled)for(let[n,r]of Object.entries(e.producers)){if(!r.brokers||r.brokers.length===0)throw new t(`[${n}] At least one broker is required`);this.producers.set(n,new i(n,r,this.logger))}else{for(let[t,n]of Object.entries(e.producers))this.producers.set(t,new a(t,n.brokers,this.logger));this.logger.info(`Kafka: Created mock producers (Kafka disabled)`)}e.dontGracefulShutdown||this.setupGracefulShutdown()}static create(t){return new e(t)}setupGracefulShutdown(){this.logger.info(`Kafka: [graceful-shutdown] adding graceful shutdown for process.pid ${process.pid}`),process.on(`SIGTERM`,async()=>{await this.gracefulShutdown(`SIGTERM`)}),process.on(`SIGINT`,async()=>{await this.gracefulShutdown(`SIGINT`)})}async gracefulShutdown(e){if(!this.gracefulShutdownStarted){this.gracefulShutdownStarted=!0,this.logger.info(`Kafka: [graceful-shutdown] received ${e}! Disconnecting all producers...`);try{await this.disconnect(),this.logger.info(`Kafka: [graceful-shutdown] all producers disconnected successfully`)}catch(e){throw this.logger.error(`Kafka: [graceful-shutdown] error during shutdown`,{error:e}),e}}}get isEnabled(){return this.enabled}get producerNames(){return Array.from(this.producers.keys())}hasProducer(e){return this.producers.has(e)}getProducer(e){let n=this.producers.get(e);if(!n)throw new t(`Producer '${e}' not found. Available producers: ${this.producerNames.join(`, `)}`);return n}async bootstrap(e){let n=Date.now(),r=e?.timeoutMs??this.bootstrapTimeoutMs,i=e?.strict??this.strictBootstrap,a=e?.producers??this.producerNames;this.logger.info(`Kafka: Starting bootstrap`,{producers:a,timeout:r,strict:i});let o={},s=[];for(let e of a){let t=this.producers.get(e);if(!t){o[e]={success:!1,duration:0,error:`Producer '${e}' not found`},s.push(`Producer '${e}' not found`);continue}let n=Date.now();try{await t.ping({timeout:r,force:!0});let i=t.getHealth();o[e]={success:!0,duration:Date.now()-n,clusterId:i.clusterId??void 0,brokerCount:i.brokerCount},this.logger.info(`Kafka: [${e}] Bootstrap successful`,{duration:Date.now()-n,clusterId:i.clusterId,brokerCount:i.brokerCount})}catch(t){let r=t;o[e]={success:!1,duration:Date.now()-n,error:r.message},s.push(`[${e}] ${r.message}`),this.logger.error(`Kafka: [${e}] Bootstrap failed`,{duration:Date.now()-n,error:r.message})}}let c=Date.now()-n,l=Object.values(o).filter(e=>e.success).length,u=Object.keys(o).length,d=i?l===u:l>0,f={success:d,duration:c,results:o};if(d)this.logger.info(`Kafka: Bootstrap completed successfully`,{duration:c,successCount:l,totalCount:u});else{let e=`Kafka bootstrap failed (${l}/${u} producers succeeded)\n\nFailures:\n${s.map(e=>` - ${e}`).join(`
3
- `)}\n\nConfiguration:\n Strict mode: ${i}\n Timeout: ${r}ms\n Producers attempted: ${a.join(`, `)}`;throw this.logger.error(`Kafka: Bootstrap failed`,{duration:c,successCount:l,totalCount:u,errors:s}),new t(e)}return f}getHealth(){let e={};for(let[t,n]of this.producers.entries())e[t]=n.getHealth();return e}getProducerHealth(e){return this.getProducer(e).getHealth()}isLive(){return!this.fatalError&&!this.gracefulShutdownStarted}async ping(){let e=Array.from(this.producers.values(),e=>e.ping());await Promise.all(e)}async pingProducer(e){await this.getProducer(e).ping()}async isReady(e){if(!this.enabled)return!0;let t=Date.now();if(!(e?.force??!1)&&this.lastReadinessCheck){let e=t-this.lastReadinessCheck.timestamp;if(e<this.healthCheckCacheMs)return this.logger.debug(`Kafka: Using cached readiness result`,{result:this.lastReadinessCheck.result,age:e}),this.lastReadinessCheck.result}try{let n=e?.timeout??this.healthCheckTimeoutMs,r=Array.from(this.producers.values()).map(e=>e.ping({timeout:n,force:!0}));return await Promise.all(r),this.lastReadinessCheck={result:!0,timestamp:t},!0}catch(e){return this.lastReadinessCheck={result:!1,timestamp:t},this.logger.warn(`Kafka: Readiness check failed`,{error:e.message}),!1}}getConnectionStatus(){let e={};for(let[t,n]of this.producers.entries()){let r=n.getHealth();e[t]={connected:r.isConnected,lastSuccessAt:r.lastPingSucceededAt,lastError:r.lastError?.message??null}}return e}async publish(e,t,n,r){return this.getProducer(e).publish(t,n,r)}async publishBatch(e,t){return this.getProducer(e).publishBatch(t)}async disconnect(){let e=Array.from(this.producers.values()).map(e=>e.disconnect());await Promise.all(e),this.logger.info(`Kafka: All producers disconnected`)}async disconnectProducer(e){await this.getProducer(e).disconnect()}};exports.KafkaError=t,exports.KafkaManager=o;
2
+ `:``}\nCurrent configuration:\n Brokers: ${t}\n Client ID: ${this.config.clientId||`${r}-${this.name}`}\n SASL: ${this.config.sasl?`enabled (${this.config.sasl.mechanism})`:`disabled`}\n SASL Password: ${this.config.sasl?this.config.sasl.password?`****`:`not set`:`N/A`}\n Auto-create topics: ${this.config.autoCreateTopics??!1}`}async ping(n){if(this._lastPingAt=Date.now(),!(this._isConnected&&!n?.force)){await this.initialize();try{let t=n?.timeout??5e3,r=Error(`Connection timeout after ${t}ms`),i=(0,e.setTimeout)(t).then(()=>{throw r}),a=await Promise.race([this.producer.metadata({topics:[]}),i]);this._isConnected=!0,this._lastPingSucceededAt=Date.now(),this._clusterId=a.id,this._brokerCount=a.brokers.size,this._lastError=null,this.logger.info(`Kafka: [${this.name}] Successfully connected to cluster`,{clusterId:a.id,brokers:a.brokers.size,duration:Date.now()-this._lastPingAt})}catch(e){let n=e;throw this._isConnected=!1,this._lastError={message:n.message,timestamp:Date.now(),stack:n.stack},this.logger.error(`Kafka: [${this.name}] Failed to connect to brokers`,{error:n.message,duration:Date.now()-this._lastPingAt}),new t(this.buildConnectionErrorMessage(n),{cause:n})}}}async publish(e,r,i){if(!e)throw new t(`[${this.name}] Topic name is required`);await this.ping();try{let t={[n]:Date.now().toString(),...i?.headers},a=await this.producer.send({messages:[{topic:e,value:JSON.stringify(r),key:i?.key,partition:i?.partition,headers:t}]});return this.logger.debug(`Kafka: [${this.name}] Published message to topic ${e}`,{topic:e}),[{topic:e,partition:i?.partition??0,offset:a?.offsets?.[0]?.offset?.toString()??`0`}]}catch(n){let i=n;throw this.logger.error(`Kafka: [${this.name}] Error publishing to topic ${e}`,{error:n,value:r}),new t(`[${this.name}] Failed to publish to topic ${e}: ${i.message||`Unknown error`}`,{cause:n})}}async publishBatch(e,r){if(!e)throw new t(`[${this.name}] Topic name is required`);if(!r.messages||r.messages.length===0)throw new t(`[${this.name}] At least one message is required`);await this.ping();try{let t=r.messages.map(t=>({topic:e,value:JSON.stringify(t.value),key:t.key,partition:t.partition,headers:{[n]:Date.now().toString(),...t.headers}})),i=await this.producer.send({messages:t});return this.logger.debug(`Kafka: [${this.name}] Published ${t.length} messages to topic ${e}`,{topic:e,count:t.length}),t.map((t,n)=>({topic:e,partition:t.partition??0,offset:i?.offsets?.[n]?.offset?.toString()??n.toString()}))}catch(n){let r=n;throw this.logger.error(`Kafka: [${this.name}] Error publishing batch to topic ${e}`,{error:n}),new t(`[${this.name}] Failed to publish batch to topic ${e}: ${r.message||`Unknown error`}`,{cause:n})}}async disconnect(){if(!this.producer||!this._isConnected){this.logger.debug(`Kafka: [${this.name}] Producer already disconnected`);return}this.logger.info(`Kafka: [${this.name}] Disconnecting producer`);try{await this.producer.close(),this._isConnected=!1,this.logger.info(`Kafka: [${this.name}] Producer disconnected successfully`)}catch(e){throw this.logger.error(`Kafka: [${this.name}] Error disconnecting producer`,{error:e}),new t(`[${this.name}] Failed to disconnect producer: ${e.message}`,{cause:e})}}},a=class{constructor(e,t,n){this.name=e,this.brokers=t,this.logger=n,this.isConnected=!0}getHealth(){return{name:this.name,enabled:!1,isConnected:!0,lastPingAt:null,lastPingSucceededAt:null,lastError:null,clusterId:`mock-cluster`,brokerCount:0,brokers:this.brokers}}async ping(){return this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping ping`),Promise.resolve()}async publish(e,t,n){return this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping publish to topic: ${e}`,{value:t}),Promise.resolve([{topic:e,partition:0,offset:`0`}])}async publishBatch(e,t){return this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping batch publish to topic: ${e}`,{messageCount:t.messages.length}),Promise.resolve(t.messages.map(()=>({topic:e,partition:0,offset:`0`})))}async disconnect(){return this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping disconnect`),Promise.resolve()}},o=class e{constructor(e){if(this.producers=new Map,this.topics=new Map,this.gracefulShutdownStarted=!1,this.fatalError=null,this.lastReadinessCheck=null,this.logger=e.logger,this.enabled=e.enabled??!0,this.healthCheckTimeoutMs=e.healthCheckTimeoutMs??5e3,this.healthCheckCacheMs=e.healthCheckCacheMs??1e3,this.bootstrapTimeoutMs=e.bootstrapTimeoutMs??3e4,this.strictBootstrap=e.strictBootstrap??!0,e.topics)for(let[t,n]of Object.entries(e.topics))this.topics.set(t,n);if(this.logger.info(`Kafka: Initialized KafkaManager`,{enabled:this.enabled,producerCount:Object.keys(e.producers??{}).length,producers:Object.keys(e.producers??{}),topicCount:this.topics.size,topics:Array.from(this.topics.keys())}),this.enabled)for(let[n,r]of Object.entries(e.producers??{})){if(!r.brokers||r.brokers.length===0)throw new t(`[${n}] At least one broker is required`);this.producers.set(n,new i(n,r,this.logger))}else{for(let[t,n]of Object.entries(e.producers??{}))this.producers.set(t,new a(t,n.brokers,this.logger));this.producers.size>0&&this.logger.info(`Kafka: Created mock producers (Kafka disabled)`)}e.dontGracefulShutdown||this.setupGracefulShutdown()}static create(n){if(n.topics){let e=Object.keys(n.producers??{});for(let[r,i]of Object.entries(n.topics))if(!e.includes(i.producer))throw new t(`Topic '${r}' references unknown producer '${i.producer}'. Available producers: ${e.join(`, `)}`)}return new e(n)}setupGracefulShutdown(){this.logger.info(`Kafka: [graceful-shutdown] adding graceful shutdown for process.pid ${process.pid}`),process.on(`SIGTERM`,async()=>{await this.gracefulShutdown(`SIGTERM`)}),process.on(`SIGINT`,async()=>{await this.gracefulShutdown(`SIGINT`)})}async gracefulShutdown(e){if(!this.gracefulShutdownStarted){this.gracefulShutdownStarted=!0,this.logger.info(`Kafka: [graceful-shutdown] received ${e}! Disconnecting all producers...`);try{await this.disconnect(),this.logger.info(`Kafka: [graceful-shutdown] all producers disconnected successfully`)}catch(e){throw this.logger.error(`Kafka: [graceful-shutdown] error during shutdown`,{error:e}),e}}}get isEnabled(){return this.enabled}get producerNames(){return Array.from(this.producers.keys())}hasProducer(e){return this.producers.has(e)}getProducer(e){let n=this.producers.get(e);if(!n)throw new t(`Producer '${e}' not found. Available producers: ${this.producerNames.join(`, `)}`);return n}async bootstrap(e){let n=Date.now(),r=e?.timeoutMs??this.bootstrapTimeoutMs,i=e?.strict??this.strictBootstrap,a=e?.producers??this.producerNames;this.logger.info(`Kafka: Starting bootstrap`,{producers:a,timeout:r,strict:i});let o={},s=[];for(let e of a){let t=this.producers.get(e);if(!t){o[e]={success:!1,duration:0,error:`Producer '${e}' not found`},s.push(`Producer '${e}' not found`);continue}let n=Date.now();try{await t.ping({timeout:r,force:!0});let i=t.getHealth();o[e]={success:!0,duration:Date.now()-n,clusterId:i.clusterId??void 0,brokerCount:i.brokerCount},this.logger.info(`Kafka: [${e}] Bootstrap successful`,{duration:Date.now()-n,clusterId:i.clusterId,brokerCount:i.brokerCount})}catch(t){let r=t;o[e]={success:!1,duration:Date.now()-n,error:r.message},s.push(`[${e}] ${r.message}`),this.logger.error(`Kafka: [${e}] Bootstrap failed`,{duration:Date.now()-n,error:r.message})}}let c=Date.now()-n,l=Object.values(o).filter(e=>e.success).length,u=Object.keys(o).length,d=i?l===u:l>0,f={success:d,duration:c,results:o};if(d)this.logger.info(`Kafka: Bootstrap completed successfully`,{duration:c,successCount:l,totalCount:u});else{let e=`Kafka bootstrap failed (${l}/${u} producers succeeded)\n\nFailures:\n${s.map(e=>` - ${e}`).join(`
3
+ `)}\n\nConfiguration:\n Strict mode: ${i}\n Timeout: ${r}ms\n Producers attempted: ${a.join(`, `)}`;throw this.logger.error(`Kafka: Bootstrap failed`,{duration:c,successCount:l,totalCount:u,errors:s}),new t(e)}return f}getHealth(){let e={};for(let[t,n]of this.producers.entries())e[t]=n.getHealth();return e}getProducerHealth(e){return this.getProducer(e).getHealth()}isLive(){return!this.fatalError&&!this.gracefulShutdownStarted}async ping(){let e=Array.from(this.producers.values(),e=>e.ping());await Promise.all(e)}async pingProducer(e){await this.getProducer(e).ping()}async isReady(e){if(!this.enabled)return!0;let t=Date.now();if(!(e?.force??!1)&&this.lastReadinessCheck){let e=t-this.lastReadinessCheck.timestamp;if(e<this.healthCheckCacheMs)return this.logger.debug(`Kafka: Using cached readiness result`,{result:this.lastReadinessCheck.result,age:e}),this.lastReadinessCheck.result}try{let n=e?.timeout??this.healthCheckTimeoutMs,r=Array.from(this.producers.values()).map(e=>e.ping({timeout:n,force:!0}));return await Promise.all(r),this.lastReadinessCheck={result:!0,timestamp:t},!0}catch(e){return this.lastReadinessCheck={result:!1,timestamp:t},this.logger.warn(`Kafka: Readiness check failed`,{error:e.message}),!1}}getConnectionStatus(){let e={};for(let[t,n]of this.producers.entries()){let r=n.getHealth();e[t]={connected:r.isConnected,lastSuccessAt:r.lastPingSucceededAt,lastError:r.lastError?.message??null}}return e}getTopicDefinition(e){let n=this.topics.get(e);if(!n)throw new t(`Topic '${e}' not found. Available topics: ${Array.from(this.topics.keys()).join(`, `)}`);return n}async publish(e,n,r){let i=this.getTopicDefinition(e);if(!r?.skipValidation)try{i.schema.parse(n)}catch(r){throw this.logger.error(`Kafka: Validation failed for topic '${e}'`,{error:r,value:n}),new t(`Validation failed for topic '${e}': ${r.message}`,{cause:r})}return this.getProducer(i.producer).publish(e,n,r)}async publishBatch(e,n){let r=this.getTopicDefinition(e);if(!n.skipValidation)for(let i=0;i<n.messages.length;i++)try{r.schema.parse(n.messages[i].value)}catch(r){throw this.logger.error(`Kafka: Validation failed for topic '${e}' message ${i}`,{error:r,value:n.messages[i].value}),new t(`Validation failed for topic '${e}' message ${i}: ${r.message}`,{cause:r})}return this.getProducer(r.producer).publishBatch(e,n)}async disconnect(){let e=Array.from(this.producers.values()).map(e=>e.disconnect());await Promise.all(e),this.logger.info(`Kafka: All producers disconnected`)}async disconnectProducer(e){await this.getProducer(e).disconnect()}};exports.KafkaError=t,exports.KafkaManager=o;
4
4
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["name: string","config: ProducerConfig","logger: LoggerInstanceManager","#loadKafkaAndSetup","headers: Record<string, string>","name: string","brokers: string[]","logger: LoggerInstanceManager","results: BootstrapResult['results']","errors: string[]","result: BootstrapResult","health: Record<string, ProducerHealth>","status: Record<string, ConnectionStatus>"],"sources":["../src/kafkaError.ts","../src/consts.ts","../src/producer.ts","../src/mockProducer.ts","../src/manager.ts"],"sourcesContent":["export default class KafkaError extends Error {\n name = 'KafkaError';\n}\n","export const TIMESTAMP_HEADER = 'x-timestamp';\n\nexport const DEFAULT_CLIENT_ID = 'autofleet-kafka-producer';\n","import { setTimeout } from 'node:timers/promises';\nimport type { LoggerInstanceManager } from '@autofleet/logger';\nimport type { Producer } from '@platformatic/kafka/dist/clients/producer/index.ts';\nimport KafkaError from './kafkaError';\nimport {\n DEFAULT_CLIENT_ID,\n TIMESTAMP_HEADER,\n} from './consts';\nimport type {\n ProducerConfig,\n PublishOptions,\n PublishBatchOptions,\n RecordMetadata,\n ProducerHealth,\n IProducer,\n} from './types';\n\n/**\n * Wrapper for a single Kafka producer instance with lazy initialization\n * Tracks health metadata including connection state, timestamps, and errors\n */\nexport class ProducerWrapper implements IProducer {\n private producer: Producer<string, string, string, string> | null = null;\n private _isConnected = false;\n private initPromise: Promise<void> | null = null;\n\n // Health tracking metadata\n private _lastPingAt: number | null = null;\n private _lastPingSucceededAt: number | null = null;\n private _lastError: { message: string; timestamp: number; stack?: string; } | null = null;\n private _clusterId: string | null = null;\n private _brokerCount = 0;\n\n constructor(\n private readonly name: string,\n private readonly config: ProducerConfig,\n private readonly logger: LoggerInstanceManager,\n ) {}\n\n async #loadKafkaAndSetup(): Promise<void> {\n const { Producer, stringSerializers } = await import('@platformatic/kafka');\n\n this.producer = new Producer({\n bootstrapBrokers: this.config.brokers,\n clientId: this.config.clientId || `${DEFAULT_CLIENT_ID}-${this.name}`,\n serializers: stringSerializers,\n autocreateTopics: this.config.autoCreateTopics ?? false,\n sasl: this.config.sasl,\n tls: this.config.tls,\n });\n\n this.logger.info(`Kafka: [${this.name}] Initialized producer`, {\n clientId: this.config.clientId || `${DEFAULT_CLIENT_ID}-${this.name}`,\n brokers: this.config.brokers,\n });\n\n // Clear promise to allow garbage collection\n this.initPromise = null;\n }\n\n private async initialize(): Promise<void> {\n if (this.producer) {\n return;\n }\n\n this.initPromise ??= this.#loadKafkaAndSetup();\n await this.initPromise;\n }\n\n get isConnected(): boolean {\n return this._isConnected;\n }\n\n /**\n * Get comprehensive health snapshot for this producer\n */\n getHealth(): ProducerHealth {\n return {\n name: this.name,\n enabled: true,\n isConnected: this._isConnected,\n lastPingAt: this._lastPingAt,\n lastPingSucceededAt: this._lastPingSucceededAt,\n lastError: this._lastError,\n clusterId: this._clusterId,\n brokerCount: this._brokerCount,\n brokers: this.config.brokers,\n };\n }\n\n /**\n * Build an actionable error message with troubleshooting guidance\n */\n private buildConnectionErrorMessage(error: Error): string {\n const brokerList = this.config.brokers.join(', ');\n const firstBroker = this.config.brokers[0];\n const [host, port] = firstBroker ? firstBroker.split(':') : ['', ''];\n\n return `[${this.name}] Failed to connect to Kafka brokers: ${error.message}\\n\\n`\n + `Possible causes:\\n`\n + ` 1. Brokers are unreachable: ${brokerList}\\n`\n + ` 2. Network connectivity issues\\n`\n + ` 3. Firewall blocking ports\\n`\n + ` 4. ${this.config.sasl ? 'SASL authentication failed' : 'Authentication not configured but required'}\\n\\n`\n + `Troubleshooting:\\n`\n + ` - Verify brokers are running and accessible\\n`\n + (host && port ? ` - Test connectivity: nc -zv ${host} ${port}\\n` : '')\n + ` - Check network policies and firewall rules\\n`\n + ` ${this.config.sasl ? '- Verify SASL credentials are correct\\n' : ''}\\n`\n + `Current configuration:\\n`\n + ` Brokers: ${brokerList}\\n`\n + ` Client ID: ${this.config.clientId || `${DEFAULT_CLIENT_ID}-${this.name}`}\\n`\n + ` SASL: ${this.config.sasl ? `enabled (${this.config.sasl.mechanism})` : 'disabled'}\\n`\n + ` SASL Password: ${this.config.sasl ? (this.config.sasl.password ? '****' : 'not set') : 'N/A'}\\n`\n + ` Auto-create topics: ${this.config.autoCreateTopics ?? false}`;\n }\n\n /**\n * Ping the Kafka cluster to verify connectivity\n * @param options.timeout - Maximum time to wait for connection (ms)\n * @param options.force - Force reconnection even if already connected\n */\n async ping(options?: { timeout?: number; force?: boolean; }): Promise<void> {\n this._lastPingAt = Date.now();\n\n // Skip if already connected and not forcing revalidation\n if (this._isConnected && !options?.force) {\n return;\n }\n\n await this.initialize();\n\n try {\n // Create error before promise to preserve stack trace\n const timeoutMs = options?.timeout ?? 5000;\n const timeoutError = new Error(`Connection timeout after ${timeoutMs}ms`);\n const timeoutPromise = setTimeout(timeoutMs).then(() => {\n throw timeoutError;\n });\n\n // Race between metadata fetch and timeout\n const metadata = await Promise.race([\n this.producer!.metadata({ topics: [] }),\n timeoutPromise,\n ]);\n\n this._isConnected = true;\n this._lastPingSucceededAt = Date.now();\n this._clusterId = metadata.id;\n this._brokerCount = metadata.brokers.size;\n this._lastError = null; // Clear any previous errors\n\n this.logger.info(`Kafka: [${this.name}] Successfully connected to cluster`, {\n clusterId: metadata.id,\n brokers: metadata.brokers.size,\n duration: Date.now() - this._lastPingAt,\n });\n } catch (error) {\n const err = error as Error;\n this._isConnected = false;\n this._lastError = {\n message: err.message,\n timestamp: Date.now(),\n stack: err.stack,\n };\n\n this.logger.error(`Kafka: [${this.name}] Failed to connect to brokers`, {\n error: err.message,\n duration: Date.now() - this._lastPingAt,\n });\n\n throw new KafkaError(this.buildConnectionErrorMessage(err), { cause: err });\n }\n }\n\n async publish<T = unknown>(\n topic: string,\n value: T,\n options?: PublishOptions,\n ): Promise<RecordMetadata[]> {\n if (!topic) {\n throw new KafkaError(`[${this.name}] Topic name is required`);\n }\n\n await this.ping();\n\n try {\n const headers: Record<string, string> = {\n [TIMESTAMP_HEADER]: Date.now().toString(),\n ...options?.headers,\n };\n\n const result = await this.producer!.send({\n messages: [{\n topic,\n value: JSON.stringify(value),\n key: options?.key,\n partition: options?.partition,\n headers,\n }],\n });\n\n this.logger.debug(`Kafka: [${this.name}] Published message to topic ${topic}`, {\n topic,\n });\n\n return [{\n topic,\n partition: options?.partition ?? 0,\n offset: result?.offsets?.[0]?.offset?.toString() ?? '0',\n }];\n } catch (error) {\n const err = error as { message?: string; };\n this.logger.error(`Kafka: [${this.name}] Error publishing to topic ${topic}`, { error, value });\n throw new KafkaError(`[${this.name}] Failed to publish to topic ${topic}: ${err.message || 'Unknown error'}`, { cause: error });\n }\n }\n\n async publishBatch(options: PublishBatchOptions): Promise<RecordMetadata[]> {\n if (!options.topic) {\n throw new KafkaError(`[${this.name}] Topic name is required`);\n }\n\n if (!options.messages || options.messages.length === 0) {\n throw new KafkaError(`[${this.name}] At least one message is required`);\n }\n\n await this.ping();\n\n try {\n const messages = options.messages.map(msg => ({\n topic: options.topic,\n value: JSON.stringify(msg.value),\n key: msg.key,\n partition: msg.partition,\n headers: {\n [TIMESTAMP_HEADER]: Date.now().toString(),\n ...msg.headers,\n },\n }));\n\n const result = await this.producer!.send({ messages });\n\n this.logger.debug(`Kafka: [${this.name}] Published ${messages.length} messages to topic ${options.topic}`, {\n topic: options.topic,\n count: messages.length,\n });\n\n return messages.map((msg, idx) => ({\n topic: options.topic,\n partition: msg.partition ?? 0,\n offset: result?.offsets?.[idx]?.offset?.toString() ?? idx.toString(),\n }));\n } catch (error) {\n const err = error as { message?: string; };\n this.logger.error(`Kafka: [${this.name}] Error publishing batch to topic ${options.topic}`, { error });\n throw new KafkaError(`[${this.name}] Failed to publish batch to topic ${options.topic}: ${err.message || 'Unknown error'}`, { cause: error });\n }\n }\n\n async disconnect(): Promise<void> {\n if (!this.producer || !this._isConnected) {\n this.logger.debug(`Kafka: [${this.name}] Producer already disconnected`);\n return;\n }\n\n this.logger.info(`Kafka: [${this.name}] Disconnecting producer`);\n\n try {\n await this.producer.close();\n this._isConnected = false;\n this.logger.info(`Kafka: [${this.name}] Producer disconnected successfully`);\n } catch (error) {\n this.logger.error(`Kafka: [${this.name}] Error disconnecting producer`, { error });\n throw new KafkaError(`[${this.name}] Failed to disconnect producer: ${(error as Error).message}`, { cause: error });\n }\n }\n}\n","import type { LoggerInstanceManager } from '@autofleet/logger';\nimport type {\n PublishOptions,\n PublishBatchOptions,\n RecordMetadata,\n ProducerHealth,\n IProducer,\n} from './types';\n\n/**\n * Mock producer for when Kafka is disabled\n */\nexport class MockProducer implements IProducer {\n constructor(private readonly name: string, private readonly brokers: string[], private readonly logger: LoggerInstanceManager) {}\n\n readonly isConnected = true;\n\n /**\n * Get health snapshot for mock producer\n */\n getHealth(): ProducerHealth {\n return {\n name: this.name,\n enabled: false, // Mock mode means Kafka is disabled\n isConnected: true, // Mock is always \"connected\"\n lastPingAt: null,\n lastPingSucceededAt: null,\n lastError: null,\n clusterId: 'mock-cluster',\n brokerCount: 0,\n brokers: this.brokers,\n };\n }\n\n async ping(): Promise<void> {\n this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping ping`);\n return Promise.resolve();\n }\n\n async publish<T = unknown>(topic: string, value: T, _options?: PublishOptions): Promise<RecordMetadata[]> {\n this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping publish to topic: ${topic}`, { value });\n return Promise.resolve([{\n topic,\n partition: 0,\n offset: '0',\n }]);\n }\n\n async publishBatch(options: PublishBatchOptions): Promise<RecordMetadata[]> {\n this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping batch publish to topic: ${options.topic}`, {\n messageCount: options.messages.length,\n });\n return Promise.resolve(options.messages.map(() => ({\n topic: options.topic,\n partition: 0,\n offset: '0',\n })));\n }\n\n async disconnect(): Promise<void> {\n this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping disconnect`);\n return Promise.resolve();\n }\n}\n","import type { LoggerInstanceManager } from '@autofleet/logger';\nimport KafkaError from './kafkaError';\nimport { ProducerWrapper } from './producer';\nimport { MockProducer } from './mockProducer';\nimport type {\n KafkaManagerOptions,\n PublishOptions,\n PublishBatchOptions,\n RecordMetadata,\n ProducerHealth,\n BootstrapOptions,\n BootstrapResult,\n ReadinessOptions,\n ConnectionStatus,\n IProducer,\n ProducerConfig,\n} from './types';\n\n/**\n * Manager for multiple Kafka producers\n * Provides lifecycle management, health tracking, and operational guarantees\n */\nexport class KafkaManager<TProducers extends string = string> {\n private readonly producers = new Map<string, IProducer>();\n private readonly logger: LoggerInstanceManager;\n private readonly enabled: boolean;\n private gracefulShutdownStarted = false;\n private fatalError: Error | null = null;\n\n // Configuration options\n private readonly healthCheckTimeoutMs: number;\n private readonly healthCheckCacheMs: number;\n private readonly bootstrapTimeoutMs: number;\n private readonly strictBootstrap: boolean;\n\n // Health check cache\n private lastReadinessCheck: { result: boolean; timestamp: number; } | null = null;\n\n private constructor(options: KafkaManagerOptions) {\n this.logger = options.logger;\n this.enabled = options.enabled ?? true;\n this.healthCheckTimeoutMs = options.healthCheckTimeoutMs ?? 5000;\n this.healthCheckCacheMs = options.healthCheckCacheMs ?? 1000;\n this.bootstrapTimeoutMs = options.bootstrapTimeoutMs ?? 30_000;\n this.strictBootstrap = options.strictBootstrap ?? true;\n\n this.logger.info('Kafka: Initialized KafkaManager', {\n enabled: this.enabled,\n producerCount: Object.keys(options.producers).length,\n producers: Object.keys(options.producers),\n });\n\n // Create producers\n if (!this.enabled) {\n // Create mock producers\n for (const [name, config] of Object.entries(options.producers)) {\n this.producers.set(name, new MockProducer(name, config.brokers, this.logger));\n }\n this.logger.info('Kafka: Created mock producers (Kafka disabled)');\n } else {\n // Create real producers (they will initialize lazily)\n for (const [name, config] of Object.entries(options.producers)) {\n if (!config.brokers || config.brokers.length === 0) {\n throw new KafkaError(`[${name}] At least one broker is required`);\n }\n\n this.producers.set(name, new ProducerWrapper(name, config, this.logger));\n }\n }\n\n if (!options.dontGracefulShutdown) {\n this.setupGracefulShutdown();\n }\n }\n\n /**\n * Create a new KafkaManager instance with multiple named producers\n * Producers are initialized lazily on first use\n * Producer names are type-safe based on the configuration\n */\n static create<T extends Record<string, ProducerConfig>>(\n options: Omit<KafkaManagerOptions, 'producers'> & { producers: T; },\n ): KafkaManager<keyof T & string> {\n return new KafkaManager(options);\n }\n\n private setupGracefulShutdown(): void {\n this.logger.info(`Kafka: [graceful-shutdown] adding graceful shutdown for process.pid ${process.pid}`);\n\n process.on('SIGTERM', async () => {\n await this.gracefulShutdown('SIGTERM');\n });\n\n process.on('SIGINT', async () => {\n await this.gracefulShutdown('SIGINT');\n });\n }\n\n public async gracefulShutdown(signal: string): Promise<void> {\n if (this.gracefulShutdownStarted) {\n return;\n }\n\n this.gracefulShutdownStarted = true;\n\n this.logger.info(`Kafka: [graceful-shutdown] received ${signal}! Disconnecting all producers...`);\n\n try {\n await this.disconnect();\n this.logger.info('Kafka: [graceful-shutdown] all producers disconnected successfully');\n } catch (error) {\n this.logger.error('Kafka: [graceful-shutdown] error during shutdown', { error });\n throw error;\n }\n }\n\n /**\n * Check if Kafka is enabled\n */\n get isEnabled(): boolean {\n return this.enabled;\n }\n\n /**\n * Get all producer names\n */\n get producerNames(): TProducers[] {\n return Array.from(this.producers.keys()) as TProducers[];\n }\n\n /**\n * Check if a producer exists\n */\n hasProducer(name: TProducers): boolean {\n return this.producers.has(name);\n }\n\n /**\n * Get a specific producer\n */\n private getProducer(name: TProducers): IProducer {\n const producer = this.producers.get(name);\n if (!producer) {\n throw new KafkaError(`Producer '${name}' not found. Available producers: ${this.producerNames.join(', ')}`);\n }\n return producer;\n }\n\n /**\n * Explicitly bootstrap all (or specific) producers\n * Connects to brokers, validates connectivity, and returns detailed results\n *\n * @example\n * // Bootstrap all producers with defaults (30s timeout, strict mode)\n * await kafka.bootstrap();\n *\n * @example\n * // Bootstrap with custom timeout and non-strict mode\n * const result = await kafka.bootstrap({ timeoutMs: 10000, strict: false });\n * if (!result.success) {\n * console.error('Some producers failed:', result.results);\n * }\n *\n * @example\n * // Bootstrap specific producers only\n * await kafka.bootstrap({ producers: ['main'] });\n */\n async bootstrap(options?: BootstrapOptions): Promise<BootstrapResult> {\n const startTime = Date.now();\n const timeoutMs = options?.timeoutMs ?? this.bootstrapTimeoutMs;\n const strict = options?.strict ?? this.strictBootstrap;\n const producersToBootstrap = options?.producers ?? this.producerNames;\n\n this.logger.info('Kafka: Starting bootstrap', {\n producers: producersToBootstrap,\n timeout: timeoutMs,\n strict,\n });\n\n const results: BootstrapResult['results'] = {};\n const errors: string[] = [];\n\n // Bootstrap each producer\n for (const name of producersToBootstrap) {\n const producer = this.producers.get(name);\n if (!producer) {\n results[name] = {\n success: false,\n duration: 0,\n error: `Producer '${name}' not found`,\n };\n errors.push(`Producer '${name}' not found`);\n continue;\n }\n\n const producerStartTime = Date.now();\n try {\n await producer.ping({ timeout: timeoutMs, force: true });\n const health = producer.getHealth();\n\n results[name] = {\n success: true,\n duration: Date.now() - producerStartTime,\n clusterId: health.clusterId ?? undefined,\n brokerCount: health.brokerCount,\n };\n\n this.logger.info(`Kafka: [${name}] Bootstrap successful`, {\n duration: Date.now() - producerStartTime,\n clusterId: health.clusterId,\n brokerCount: health.brokerCount,\n });\n } catch (error) {\n const err = error as Error;\n results[name] = {\n success: false,\n duration: Date.now() - producerStartTime,\n error: err.message,\n };\n errors.push(`[${name}] ${err.message}`);\n\n this.logger.error(`Kafka: [${name}] Bootstrap failed`, {\n duration: Date.now() - producerStartTime,\n error: err.message,\n });\n }\n }\n\n const totalDuration = Date.now() - startTime;\n const successCount = Object.values(results).filter(r => r.success).length;\n const totalCount = Object.keys(results).length;\n const success = strict ? successCount === totalCount : successCount > 0;\n\n const result: BootstrapResult = {\n success,\n duration: totalDuration,\n results,\n };\n\n if (success) {\n this.logger.info('Kafka: Bootstrap completed successfully', {\n duration: totalDuration,\n successCount,\n totalCount,\n });\n } else {\n const errorMessage = `Kafka bootstrap failed (${successCount}/${totalCount} producers succeeded)\\n\\n`\n + `Failures:\\n${errors.map(e => ` - ${e}`).join('\\n')}\\n\\n`\n + `Configuration:\\n`\n + ` Strict mode: ${strict}\\n`\n + ` Timeout: ${timeoutMs}ms\\n`\n + ` Producers attempted: ${producersToBootstrap.join(', ')}`;\n\n this.logger.error('Kafka: Bootstrap failed', {\n duration: totalDuration,\n successCount,\n totalCount,\n errors,\n });\n\n throw new KafkaError(errorMessage);\n }\n\n return result;\n }\n\n /**\n * Get detailed health snapshot for all producers\n * Returns comprehensive metadata including timestamps, errors, cluster info\n *\n * @example\n * const health = kafka.getHealth();\n * console.log(health.main.lastPingSucceededAt); // timestamp\n * console.log(health.main.clusterId); // 'prod-kafka-01'\n */\n getHealth(): Record<TProducers, ProducerHealth> {\n const health: Record<string, ProducerHealth> = {};\n for (const [name, producer] of this.producers.entries()) {\n health[name] = producer.getHealth();\n }\n return health as Record<TProducers, ProducerHealth>;\n }\n\n /**\n * Get detailed health for a specific producer\n */\n getProducerHealth(name: TProducers): ProducerHealth {\n const producer = this.getProducer(name);\n return producer.getHealth();\n }\n\n /**\n * Lightweight liveness check - does NOT perform Kafka operations\n * Only checks internal state for fatal errors\n * Suitable for Kubernetes liveness probes\n *\n * Returns false if:\n * - Manager is in a fatal state\n * - Graceful shutdown has started\n *\n * @example\n * app.get('/health/live', (req, res) => {\n * res.status(kafka.isLive() ? 200 : 503).json({ live: kafka.isLive() });\n * });\n */\n isLive(): boolean {\n return !this.fatalError && !this.gracefulShutdownStarted;\n }\n\n /**\n * Ping all producers to verify connectivity\n */\n async ping(): Promise<void> {\n const promises = Array.from(this.producers.values(), p => p.ping());\n await Promise.all(promises);\n }\n\n /**\n * Ping a specific producer\n */\n async pingProducer(name: TProducers): Promise<void> {\n await this.getProducer(name).ping();\n }\n\n /**\n * Check if Kafka is ready to handle traffic\n * Revalidates connectivity if cache is stale\n *\n * @param options.timeout - Override default health check timeout\n * @param options.force - Force revalidation, ignore cache\n *\n * @example\n * // Use cached result if fresh (within healthCheckCacheMs)\n * const ready = await kafka.isReady();\n *\n * @example\n * // Force immediate revalidation\n * const ready = await kafka.isReady({ force: true });\n */\n async isReady(options?: ReadinessOptions): Promise<boolean> {\n if (!this.enabled) {\n return true;\n }\n\n const now = Date.now();\n const force = options?.force ?? false;\n\n // Use cached result if available and fresh\n if (!force && this.lastReadinessCheck) {\n const age = now - this.lastReadinessCheck.timestamp;\n if (age < this.healthCheckCacheMs) {\n this.logger.debug('Kafka: Using cached readiness result', {\n result: this.lastReadinessCheck.result,\n age,\n });\n return this.lastReadinessCheck.result;\n }\n }\n\n // Revalidate connectivity\n try {\n const timeout = options?.timeout ?? this.healthCheckTimeoutMs;\n const promises = Array.from(this.producers.values()).map(p =>\n p.ping({ timeout, force: true }),\n );\n await Promise.all(promises);\n\n this.lastReadinessCheck = { result: true, timestamp: now };\n return true;\n } catch (error) {\n this.lastReadinessCheck = { result: false, timestamp: now };\n this.logger.warn('Kafka: Readiness check failed', {\n error: (error as Error).message,\n });\n return false;\n }\n }\n\n /**\n * Check connection status of all producers\n * Returns detailed status including last success time and errors\n */\n getConnectionStatus(): Record<TProducers, ConnectionStatus> {\n const status: Record<string, ConnectionStatus> = {};\n for (const [name, producer] of this.producers.entries()) {\n const health = producer.getHealth();\n status[name] = {\n connected: health.isConnected,\n lastSuccessAt: health.lastPingSucceededAt,\n lastError: health.lastError?.message ?? null,\n };\n }\n return status as Record<TProducers, ConnectionStatus>;\n }\n\n /**\n * Publish a message using a named producer\n */\n async publish<T = unknown>(\n producerName: TProducers,\n topic: string,\n value: T,\n options?: PublishOptions,\n ): Promise<RecordMetadata[]> {\n return this.getProducer(producerName).publish(topic, value, options);\n }\n\n /**\n * Publish a batch of messages using a named producer\n */\n async publishBatch(\n producerName: TProducers,\n options: PublishBatchOptions,\n ): Promise<RecordMetadata[]> {\n return this.getProducer(producerName).publishBatch(options);\n }\n\n /**\n * Disconnect all producers\n */\n async disconnect(): Promise<void> {\n const promises = Array.from(this.producers.values()).map(p => p.disconnect());\n await Promise.all(promises);\n this.logger.info('Kafka: All producers disconnected');\n }\n\n /**\n * Disconnect a specific producer\n */\n async disconnectProducer(name: TProducers): Promise<void> {\n await this.getProducer(name).disconnect();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["name: string","config: ProducerConfig","logger: LoggerInstanceManager","#loadKafkaAndSetup","headers: Record<string, string>","name: string","brokers: string[]","logger: LoggerInstanceManager","results: BootstrapResult['results']","errors: string[]","result: BootstrapResult","health: Record<string, ProducerHealth>","status: Record<string, ConnectionStatus>"],"sources":["../src/kafkaError.ts","../src/consts.ts","../src/producer.ts","../src/mockProducer.ts","../src/manager.ts"],"sourcesContent":["export default class KafkaError extends Error {\n name = 'KafkaError';\n}\n","export const TIMESTAMP_HEADER = 'x-timestamp';\n\nexport const DEFAULT_CLIENT_ID = 'autofleet-kafka-producer';\n","import { setTimeout } from 'node:timers/promises';\nimport type { LoggerInstanceManager } from '@autofleet/logger';\nimport type { Producer } from '@platformatic/kafka/dist/clients/producer/index.ts';\nimport KafkaError from './kafkaError';\nimport {\n DEFAULT_CLIENT_ID,\n TIMESTAMP_HEADER,\n} from './consts';\nimport type {\n ProducerConfig,\n PublishOptions,\n PublishBatchOptions,\n RecordMetadata,\n ProducerHealth,\n IProducer,\n} from './types';\n\n/**\n * Wrapper for a single Kafka producer instance with lazy initialization\n * Tracks health metadata including connection state, timestamps, and errors\n */\nexport class ProducerWrapper implements IProducer {\n private producer: Producer<string, string, string, string> | null = null;\n private _isConnected = false;\n private initPromise: Promise<void> | null = null;\n\n // Health tracking metadata\n private _lastPingAt: number | null = null;\n private _lastPingSucceededAt: number | null = null;\n private _lastError: { message: string; timestamp: number; stack?: string; } | null = null;\n private _clusterId: string | null = null;\n private _brokerCount = 0;\n\n constructor(\n private readonly name: string,\n private readonly config: ProducerConfig,\n private readonly logger: LoggerInstanceManager,\n ) {}\n\n async #loadKafkaAndSetup(): Promise<void> {\n const { Producer, stringSerializers } = await import('@platformatic/kafka');\n\n this.producer = new Producer({\n bootstrapBrokers: this.config.brokers,\n clientId: this.config.clientId || `${DEFAULT_CLIENT_ID}-${this.name}`,\n serializers: stringSerializers,\n autocreateTopics: this.config.autoCreateTopics ?? false,\n sasl: this.config.sasl,\n tls: this.config.tls,\n });\n\n this.logger.info(`Kafka: [${this.name}] Initialized producer`, {\n clientId: this.config.clientId || `${DEFAULT_CLIENT_ID}-${this.name}`,\n brokers: this.config.brokers,\n });\n\n // Clear promise to allow garbage collection\n this.initPromise = null;\n }\n\n private async initialize(): Promise<void> {\n if (this.producer) {\n return;\n }\n\n this.initPromise ??= this.#loadKafkaAndSetup();\n await this.initPromise;\n }\n\n get isConnected(): boolean {\n return this._isConnected;\n }\n\n /**\n * Get comprehensive health snapshot for this producer\n */\n getHealth(): ProducerHealth {\n return {\n name: this.name,\n enabled: true,\n isConnected: this._isConnected,\n lastPingAt: this._lastPingAt,\n lastPingSucceededAt: this._lastPingSucceededAt,\n lastError: this._lastError,\n clusterId: this._clusterId,\n brokerCount: this._brokerCount,\n brokers: this.config.brokers,\n };\n }\n\n /**\n * Build an actionable error message with troubleshooting guidance\n */\n private buildConnectionErrorMessage(error: Error): string {\n const brokerList = this.config.brokers.join(', ');\n const firstBroker = this.config.brokers[0];\n const [host, port] = firstBroker ? firstBroker.split(':') : ['', ''];\n\n return `[${this.name}] Failed to connect to Kafka brokers: ${error.message}\\n\\n`\n + `Possible causes:\\n`\n + ` 1. Brokers are unreachable: ${brokerList}\\n`\n + ` 2. Network connectivity issues\\n`\n + ` 3. Firewall blocking ports\\n`\n + ` 4. ${this.config.sasl ? 'SASL authentication failed' : 'Authentication not configured but required'}\\n\\n`\n + `Troubleshooting:\\n`\n + ` - Verify brokers are running and accessible\\n`\n + (host && port ? ` - Test connectivity: nc -zv ${host} ${port}\\n` : '')\n + ` - Check network policies and firewall rules\\n`\n + ` ${this.config.sasl ? '- Verify SASL credentials are correct\\n' : ''}\\n`\n + `Current configuration:\\n`\n + ` Brokers: ${brokerList}\\n`\n + ` Client ID: ${this.config.clientId || `${DEFAULT_CLIENT_ID}-${this.name}`}\\n`\n + ` SASL: ${this.config.sasl ? `enabled (${this.config.sasl.mechanism})` : 'disabled'}\\n`\n + ` SASL Password: ${this.config.sasl ? (this.config.sasl.password ? '****' : 'not set') : 'N/A'}\\n`\n + ` Auto-create topics: ${this.config.autoCreateTopics ?? false}`;\n }\n\n /**\n * Ping the Kafka cluster to verify connectivity\n * @param options.timeout - Maximum time to wait for connection (ms)\n * @param options.force - Force reconnection even if already connected\n */\n async ping(options?: { timeout?: number; force?: boolean; }): Promise<void> {\n this._lastPingAt = Date.now();\n\n // Skip if already connected and not forcing revalidation\n if (this._isConnected && !options?.force) {\n return;\n }\n\n await this.initialize();\n\n try {\n // Create error before promise to preserve stack trace\n const timeoutMs = options?.timeout ?? 5000;\n const timeoutError = new Error(`Connection timeout after ${timeoutMs}ms`);\n const timeoutPromise = setTimeout(timeoutMs).then(() => {\n throw timeoutError;\n });\n\n // Race between metadata fetch and timeout\n const metadata = await Promise.race([\n this.producer!.metadata({ topics: [] }),\n timeoutPromise,\n ]);\n\n this._isConnected = true;\n this._lastPingSucceededAt = Date.now();\n this._clusterId = metadata.id;\n this._brokerCount = metadata.brokers.size;\n this._lastError = null; // Clear any previous errors\n\n this.logger.info(`Kafka: [${this.name}] Successfully connected to cluster`, {\n clusterId: metadata.id,\n brokers: metadata.brokers.size,\n duration: Date.now() - this._lastPingAt,\n });\n } catch (error) {\n const err = error as Error;\n this._isConnected = false;\n this._lastError = {\n message: err.message,\n timestamp: Date.now(),\n stack: err.stack,\n };\n\n this.logger.error(`Kafka: [${this.name}] Failed to connect to brokers`, {\n error: err.message,\n duration: Date.now() - this._lastPingAt,\n });\n\n throw new KafkaError(this.buildConnectionErrorMessage(err), { cause: err });\n }\n }\n\n async publish<T = unknown>(\n topic: string,\n value: T,\n options?: PublishOptions,\n ): Promise<RecordMetadata[]> {\n if (!topic) {\n throw new KafkaError(`[${this.name}] Topic name is required`);\n }\n\n await this.ping();\n\n try {\n const headers: Record<string, string> = {\n [TIMESTAMP_HEADER]: Date.now().toString(),\n ...options?.headers,\n };\n\n const result = await this.producer!.send({\n messages: [{\n topic,\n value: JSON.stringify(value),\n key: options?.key,\n partition: options?.partition,\n headers,\n }],\n });\n\n this.logger.debug(`Kafka: [${this.name}] Published message to topic ${topic}`, {\n topic,\n });\n\n return [{\n topic,\n partition: options?.partition ?? 0,\n offset: result?.offsets?.[0]?.offset?.toString() ?? '0',\n }];\n } catch (error) {\n const err = error as { message?: string; };\n this.logger.error(`Kafka: [${this.name}] Error publishing to topic ${topic}`, { error, value });\n throw new KafkaError(`[${this.name}] Failed to publish to topic ${topic}: ${err.message || 'Unknown error'}`, { cause: error });\n }\n }\n\n async publishBatch<T = unknown>(topic: string, options: PublishBatchOptions<T>): Promise<RecordMetadata[]> {\n if (!topic) {\n throw new KafkaError(`[${this.name}] Topic name is required`);\n }\n\n if (!options.messages || options.messages.length === 0) {\n throw new KafkaError(`[${this.name}] At least one message is required`);\n }\n\n await this.ping();\n\n try {\n const messages = options.messages.map(msg => ({\n topic,\n value: JSON.stringify(msg.value),\n key: msg.key,\n partition: msg.partition,\n headers: {\n [TIMESTAMP_HEADER]: Date.now().toString(),\n ...msg.headers,\n },\n }));\n\n const result = await this.producer!.send({ messages });\n\n this.logger.debug(`Kafka: [${this.name}] Published ${messages.length} messages to topic ${topic}`, {\n topic,\n count: messages.length,\n });\n\n return messages.map((msg, idx) => ({\n topic,\n partition: msg.partition ?? 0,\n offset: result?.offsets?.[idx]?.offset?.toString() ?? idx.toString(),\n }));\n } catch (error) {\n const err = error as { message?: string; };\n this.logger.error(`Kafka: [${this.name}] Error publishing batch to topic ${topic}`, { error });\n throw new KafkaError(`[${this.name}] Failed to publish batch to topic ${topic}: ${err.message || 'Unknown error'}`, { cause: error });\n }\n }\n\n async disconnect(): Promise<void> {\n if (!this.producer || !this._isConnected) {\n this.logger.debug(`Kafka: [${this.name}] Producer already disconnected`);\n return;\n }\n\n this.logger.info(`Kafka: [${this.name}] Disconnecting producer`);\n\n try {\n await this.producer.close();\n this._isConnected = false;\n this.logger.info(`Kafka: [${this.name}] Producer disconnected successfully`);\n } catch (error) {\n this.logger.error(`Kafka: [${this.name}] Error disconnecting producer`, { error });\n throw new KafkaError(`[${this.name}] Failed to disconnect producer: ${(error as Error).message}`, { cause: error });\n }\n }\n}\n","import type { LoggerInstanceManager } from '@autofleet/logger';\nimport type {\n PublishOptions,\n PublishBatchOptions,\n RecordMetadata,\n ProducerHealth,\n IProducer,\n} from './types';\n\n/**\n * Mock producer for when Kafka is disabled\n */\nexport class MockProducer implements IProducer {\n constructor(private readonly name: string, private readonly brokers: string[], private readonly logger: LoggerInstanceManager) {}\n\n readonly isConnected = true;\n\n /**\n * Get health snapshot for mock producer\n */\n getHealth(): ProducerHealth {\n return {\n name: this.name,\n enabled: false, // Mock mode means Kafka is disabled\n isConnected: true, // Mock is always \"connected\"\n lastPingAt: null,\n lastPingSucceededAt: null,\n lastError: null,\n clusterId: 'mock-cluster',\n brokerCount: 0,\n brokers: this.brokers,\n };\n }\n\n async ping(): Promise<void> {\n this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping ping`);\n return Promise.resolve();\n }\n\n async publish<T = unknown>(topic: string, value: T, _options?: PublishOptions): Promise<RecordMetadata[]> {\n this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping publish to topic: ${topic}`, { value });\n return Promise.resolve([{\n topic,\n partition: 0,\n offset: '0',\n }]);\n }\n\n async publishBatch<T = unknown>(topic: string, options: PublishBatchOptions<T>): Promise<RecordMetadata[]> {\n this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping batch publish to topic: ${topic}`, {\n messageCount: options.messages.length,\n });\n return Promise.resolve(options.messages.map(() => ({\n topic,\n partition: 0,\n offset: '0',\n })));\n }\n\n async disconnect(): Promise<void> {\n this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping disconnect`);\n return Promise.resolve();\n }\n}\n","import type { LoggerInstanceManager } from '@autofleet/logger';\nimport KafkaError from './kafkaError';\nimport { ProducerWrapper } from './producer';\nimport { MockProducer } from './mockProducer';\nimport type {\n KafkaManagerOptions,\n PublishOptions,\n PublishBatchOptions,\n RecordMetadata,\n ProducerHealth,\n BootstrapOptions,\n BootstrapResult,\n ReadinessOptions,\n ConnectionStatus,\n IProducer,\n ProducerConfig,\n TopicsConfig,\n TopicDefinition,\n TopicPayload,\n} from './types';\n\n/**\n * Manager for multiple Kafka producers\n * Provides lifecycle management, health tracking, and operational guarantees\n *\n * @template TProducers - Union of producer names\n * @template TTopics - Typed topics configuration with Zod schemas\n */\nexport class KafkaManager<TProducers extends string = string, TTopics extends TopicsConfig = {}> {\n private readonly producers = new Map<string, IProducer>();\n private readonly topics = new Map<string, TopicDefinition>();\n private readonly logger: LoggerInstanceManager;\n private readonly enabled: boolean;\n private gracefulShutdownStarted = false;\n private fatalError: Error | null = null;\n\n // Configuration options\n private readonly healthCheckTimeoutMs: number;\n private readonly healthCheckCacheMs: number;\n private readonly bootstrapTimeoutMs: number;\n private readonly strictBootstrap: boolean;\n\n // Health check cache\n private lastReadinessCheck: { result: boolean; timestamp: number; } | null = null;\n\n private constructor(options: KafkaManagerOptions) {\n this.logger = options.logger;\n this.enabled = options.enabled ?? true;\n this.healthCheckTimeoutMs = options.healthCheckTimeoutMs ?? 5000;\n this.healthCheckCacheMs = options.healthCheckCacheMs ?? 1000;\n this.bootstrapTimeoutMs = options.bootstrapTimeoutMs ?? 30_000;\n this.strictBootstrap = options.strictBootstrap ?? true;\n\n // Store topic definitions\n if (options.topics) {\n for (const [topicName, definition] of Object.entries(options.topics)) {\n this.topics.set(topicName, definition);\n }\n }\n\n this.logger.info('Kafka: Initialized KafkaManager', {\n enabled: this.enabled,\n producerCount: Object.keys(options.producers ?? {}).length,\n producers: Object.keys(options.producers ?? {}),\n topicCount: this.topics.size,\n topics: Array.from(this.topics.keys()),\n });\n\n // Create producers\n if (!this.enabled) {\n // Create mock producers\n for (const [name, config] of Object.entries(options.producers ?? {})) {\n this.producers.set(name, new MockProducer(name, config.brokers, this.logger));\n }\n if (this.producers.size > 0) {\n this.logger.info('Kafka: Created mock producers (Kafka disabled)');\n }\n } else {\n // Create real producers (they will initialize lazily)\n for (const [name, config] of Object.entries(options.producers ?? {})) {\n if (!config.brokers || config.brokers.length === 0) {\n throw new KafkaError(`[${name}] At least one broker is required`);\n }\n\n this.producers.set(name, new ProducerWrapper(name, config, this.logger));\n }\n }\n\n if (!options.dontGracefulShutdown) {\n this.setupGracefulShutdown();\n }\n }\n\n /**\n * Create a new KafkaManager instance with multiple named producers\n * Producers are initialized lazily on first use\n * Producer names are type-safe based on the configuration\n * Topics configuration provides type-safe publishing with Zod validation\n */\n static create<\n P extends Record<string, ProducerConfig>,\n T extends TopicsConfig = {},\n >(\n options: Omit<KafkaManagerOptions, 'producers' | 'topics'> & {\n producers?: P;\n topics?: T;\n },\n ): KafkaManager<keyof P & string, T> {\n // Validate that topic producers exist\n if (options.topics) {\n const producerNames = Object.keys(options.producers ?? {});\n for (const [topicName, topicDef] of Object.entries(options.topics)) {\n if (!producerNames.includes(topicDef.producer)) {\n throw new KafkaError(\n `Topic '${topicName}' references unknown producer '${topicDef.producer}'. `\n + `Available producers: ${producerNames.join(', ')}`,\n );\n }\n }\n }\n\n return new KafkaManager(options);\n }\n\n private setupGracefulShutdown(): void {\n this.logger.info(`Kafka: [graceful-shutdown] adding graceful shutdown for process.pid ${process.pid}`);\n\n process.on('SIGTERM', async () => {\n await this.gracefulShutdown('SIGTERM');\n });\n\n process.on('SIGINT', async () => {\n await this.gracefulShutdown('SIGINT');\n });\n }\n\n public async gracefulShutdown(signal: string): Promise<void> {\n if (this.gracefulShutdownStarted) {\n return;\n }\n\n this.gracefulShutdownStarted = true;\n\n this.logger.info(`Kafka: [graceful-shutdown] received ${signal}! Disconnecting all producers...`);\n\n try {\n await this.disconnect();\n this.logger.info('Kafka: [graceful-shutdown] all producers disconnected successfully');\n } catch (error) {\n this.logger.error('Kafka: [graceful-shutdown] error during shutdown', { error });\n throw error;\n }\n }\n\n /**\n * Check if Kafka is enabled\n */\n get isEnabled(): boolean {\n return this.enabled;\n }\n\n /**\n * Get all producer names\n */\n get producerNames(): TProducers[] {\n return Array.from(this.producers.keys()) as TProducers[];\n }\n\n /**\n * Check if a producer exists\n */\n hasProducer(name: TProducers): boolean {\n return this.producers.has(name);\n }\n\n /**\n * Get a specific producer\n */\n private getProducer(name: TProducers): IProducer {\n const producer = this.producers.get(name);\n if (!producer) {\n throw new KafkaError(`Producer '${name}' not found. Available producers: ${this.producerNames.join(', ')}`);\n }\n return producer;\n }\n\n /**\n * Explicitly bootstrap all (or specific) producers\n * Connects to brokers, validates connectivity, and returns detailed results\n *\n * @example\n * // Bootstrap all producers with defaults (30s timeout, strict mode)\n * await kafka.bootstrap();\n *\n * @example\n * // Bootstrap with custom timeout and non-strict mode\n * const result = await kafka.bootstrap({ timeoutMs: 10000, strict: false });\n * if (!result.success) {\n * console.error('Some producers failed:', result.results);\n * }\n *\n * @example\n * // Bootstrap specific producers only\n * await kafka.bootstrap({ producers: ['main'] });\n */\n async bootstrap(options?: BootstrapOptions): Promise<BootstrapResult> {\n const startTime = Date.now();\n const timeoutMs = options?.timeoutMs ?? this.bootstrapTimeoutMs;\n const strict = options?.strict ?? this.strictBootstrap;\n const producersToBootstrap = options?.producers ?? this.producerNames;\n\n this.logger.info('Kafka: Starting bootstrap', {\n producers: producersToBootstrap,\n timeout: timeoutMs,\n strict,\n });\n\n const results: BootstrapResult['results'] = {};\n const errors: string[] = [];\n\n // Bootstrap each producer\n for (const name of producersToBootstrap) {\n const producer = this.producers.get(name);\n if (!producer) {\n results[name] = {\n success: false,\n duration: 0,\n error: `Producer '${name}' not found`,\n };\n errors.push(`Producer '${name}' not found`);\n continue;\n }\n\n const producerStartTime = Date.now();\n try {\n await producer.ping({ timeout: timeoutMs, force: true });\n const health = producer.getHealth();\n\n results[name] = {\n success: true,\n duration: Date.now() - producerStartTime,\n clusterId: health.clusterId ?? undefined,\n brokerCount: health.brokerCount,\n };\n\n this.logger.info(`Kafka: [${name}] Bootstrap successful`, {\n duration: Date.now() - producerStartTime,\n clusterId: health.clusterId,\n brokerCount: health.brokerCount,\n });\n } catch (error) {\n const err = error as Error;\n results[name] = {\n success: false,\n duration: Date.now() - producerStartTime,\n error: err.message,\n };\n errors.push(`[${name}] ${err.message}`);\n\n this.logger.error(`Kafka: [${name}] Bootstrap failed`, {\n duration: Date.now() - producerStartTime,\n error: err.message,\n });\n }\n }\n\n const totalDuration = Date.now() - startTime;\n const successCount = Object.values(results).filter(r => r.success).length;\n const totalCount = Object.keys(results).length;\n const success = strict ? successCount === totalCount : successCount > 0;\n\n const result: BootstrapResult = {\n success,\n duration: totalDuration,\n results,\n };\n\n if (success) {\n this.logger.info('Kafka: Bootstrap completed successfully', {\n duration: totalDuration,\n successCount,\n totalCount,\n });\n } else {\n const errorMessage = `Kafka bootstrap failed (${successCount}/${totalCount} producers succeeded)\\n\\n`\n + `Failures:\\n${errors.map(e => ` - ${e}`).join('\\n')}\\n\\n`\n + `Configuration:\\n`\n + ` Strict mode: ${strict}\\n`\n + ` Timeout: ${timeoutMs}ms\\n`\n + ` Producers attempted: ${producersToBootstrap.join(', ')}`;\n\n this.logger.error('Kafka: Bootstrap failed', {\n duration: totalDuration,\n successCount,\n totalCount,\n errors,\n });\n\n throw new KafkaError(errorMessage);\n }\n\n return result;\n }\n\n /**\n * Get detailed health snapshot for all producers\n * Returns comprehensive metadata including timestamps, errors, cluster info\n *\n * @example\n * const health = kafka.getHealth();\n * console.log(health.main.lastPingSucceededAt); // timestamp\n * console.log(health.main.clusterId); // 'prod-kafka-01'\n */\n getHealth(): Record<TProducers, ProducerHealth> {\n const health: Record<string, ProducerHealth> = {};\n for (const [name, producer] of this.producers.entries()) {\n health[name] = producer.getHealth();\n }\n return health as Record<TProducers, ProducerHealth>;\n }\n\n /**\n * Get detailed health for a specific producer\n */\n getProducerHealth(name: TProducers): ProducerHealth {\n const producer = this.getProducer(name);\n return producer.getHealth();\n }\n\n /**\n * Lightweight liveness check - does NOT perform Kafka operations\n * Only checks internal state for fatal errors\n * Suitable for Kubernetes liveness probes\n *\n * Returns false if:\n * - Manager is in a fatal state\n * - Graceful shutdown has started\n *\n * @example\n * app.get('/health/live', (req, res) => {\n * res.status(kafka.isLive() ? 200 : 503).json({ live: kafka.isLive() });\n * });\n */\n isLive(): boolean {\n return !this.fatalError && !this.gracefulShutdownStarted;\n }\n\n /**\n * Ping all producers to verify connectivity\n */\n async ping(): Promise<void> {\n const promises = Array.from(this.producers.values(), p => p.ping());\n await Promise.all(promises);\n }\n\n /**\n * Ping a specific producer\n */\n async pingProducer(name: TProducers): Promise<void> {\n await this.getProducer(name).ping();\n }\n\n /**\n * Check if Kafka is ready to handle traffic\n * Revalidates connectivity if cache is stale\n *\n * @param options.timeout - Override default health check timeout\n * @param options.force - Force revalidation, ignore cache\n *\n * @example\n * // Use cached result if fresh (within healthCheckCacheMs)\n * const ready = await kafka.isReady();\n *\n * @example\n * // Force immediate revalidation\n * const ready = await kafka.isReady({ force: true });\n */\n async isReady(options?: ReadinessOptions): Promise<boolean> {\n if (!this.enabled) {\n return true;\n }\n\n const now = Date.now();\n const force = options?.force ?? false;\n\n // Use cached result if available and fresh\n if (!force && this.lastReadinessCheck) {\n const age = now - this.lastReadinessCheck.timestamp;\n if (age < this.healthCheckCacheMs) {\n this.logger.debug('Kafka: Using cached readiness result', {\n result: this.lastReadinessCheck.result,\n age,\n });\n return this.lastReadinessCheck.result;\n }\n }\n\n // Revalidate connectivity\n try {\n const timeout = options?.timeout ?? this.healthCheckTimeoutMs;\n const promises = Array.from(this.producers.values()).map(p =>\n p.ping({ timeout, force: true }),\n );\n await Promise.all(promises);\n\n this.lastReadinessCheck = { result: true, timestamp: now };\n return true;\n } catch (error) {\n this.lastReadinessCheck = { result: false, timestamp: now };\n this.logger.warn('Kafka: Readiness check failed', {\n error: (error as Error).message,\n });\n return false;\n }\n }\n\n /**\n * Check connection status of all producers\n * Returns detailed status including last success time and errors\n */\n getConnectionStatus(): Record<TProducers, ConnectionStatus> {\n const status: Record<string, ConnectionStatus> = {};\n for (const [name, producer] of this.producers.entries()) {\n const health = producer.getHealth();\n status[name] = {\n connected: health.isConnected,\n lastSuccessAt: health.lastPingSucceededAt,\n lastError: health.lastError?.message ?? null,\n };\n }\n return status as Record<TProducers, ConnectionStatus>;\n }\n\n /**\n * Get a topic definition\n */\n private getTopicDefinition<TName extends keyof TTopics & string>(topicName: TName): TopicDefinition {\n const topic = this.topics.get(topicName);\n if (!topic) {\n throw new KafkaError(\n `Topic '${topicName}' not found. Available topics: ${Array.from(this.topics.keys()).join(', ')}`,\n );\n }\n return topic;\n }\n\n /**\n * Publish a message to a typed topic\n * Automatically validates payload against Zod schema and routes to correct producer\n *\n * @template TName - Topic name (inferred from topics config)\n * @param topicName - Name of the topic to publish to\n * @param value - Payload (type-checked against topic's Zod schema)\n * @param options - Optional publish options including headers, key, partition\n *\n * @example\n * await kafka.publish('order.created', {\n * orderId: '123',\n * amount: 99.99,\n * customerId: '456',\n * });\n */\n async publish<TName extends keyof TTopics & string>(\n topicName: TName,\n value: TopicPayload<TTopics, TName>,\n options?: PublishOptions,\n ): Promise<RecordMetadata[]> {\n const topicDef = this.getTopicDefinition(topicName);\n\n // Validate payload against schema unless skipped\n if (!options?.skipValidation) {\n try {\n topicDef.schema.parse(value);\n } catch (error) {\n this.logger.error(`Kafka: Validation failed for topic '${topicName}'`, { error, value });\n throw new KafkaError(\n `Validation failed for topic '${topicName}': ${(error as Error).message}`,\n { cause: error },\n );\n }\n }\n\n // Get producer and publish\n const producer = this.getProducer(topicDef.producer as TProducers);\n return producer.publish(topicName, value, options);\n }\n\n /**\n * Publish a batch of messages to a typed topic\n * Automatically validates payloads against Zod schema and routes to correct producer\n *\n * @template TName - Topic name (inferred from topics config)\n * @param topicName - Name of the topic to publish to\n * @param options - Batch options with array of messages\n *\n * @example\n * await kafka.publishBatch('order.created', {\n * messages: [\n * { value: { orderId: '1', amount: 10, customerId: 'a' } },\n * { value: { orderId: '2', amount: 20, customerId: 'b' } },\n * ],\n * });\n */\n async publishBatch<TName extends keyof TTopics & string>(\n topicName: TName,\n options: PublishBatchOptions<TopicPayload<TTopics, TName>>,\n ): Promise<RecordMetadata[]> {\n const topicDef = this.getTopicDefinition(topicName);\n\n // Validate all payloads against schema unless skipped\n if (!options.skipValidation) {\n for (let i = 0; i < options.messages.length; i++) {\n try {\n topicDef.schema.parse(options.messages[i].value);\n } catch (error) {\n this.logger.error(`Kafka: Validation failed for topic '${topicName}' message ${i}`, {\n error,\n value: options.messages[i].value,\n });\n throw new KafkaError(\n `Validation failed for topic '${topicName}' message ${i}: ${(error as Error).message}`,\n { cause: error },\n );\n }\n }\n }\n\n // Get producer and publish batch\n const producer = this.getProducer(topicDef.producer as TProducers);\n return producer.publishBatch(topicName, options);\n }\n\n /**\n * Disconnect all producers\n */\n async disconnect(): Promise<void> {\n const promises = Array.from(this.producers.values()).map(p => p.disconnect());\n await Promise.all(promises);\n this.logger.info('Kafka: All producers disconnected');\n }\n\n /**\n * Disconnect a specific producer\n */\n async disconnectProducer(name: TProducers): Promise<void> {\n await this.getProducer(name).disconnect();\n }\n}\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -1,7 +1,26 @@
1
1
  import { LoggerInstanceManager } from "@autofleet/logger";
2
2
  import { ConnectionOptions } from "@platformatic/kafka";
3
+ import { z } from "zod";
3
4
 
4
5
  //#region src/types.d.ts
6
+
7
+ /**
8
+ * Topic definition with Zod schema for type-safe publishing
9
+ */
10
+ interface TopicDefinition<TSchema extends z.ZodType = z.ZodType> {
11
+ /** Zod schema for payload validation */
12
+ schema: TSchema;
13
+ /** Producer to use for this topic (name or alias) */
14
+ producer: string;
15
+ }
16
+ /**
17
+ * Configuration for typed topics
18
+ */
19
+ type TopicsConfig = Record<string, TopicDefinition>;
20
+ /**
21
+ * Extract payload type for a specific topic
22
+ */
23
+ type TopicPayload<TTopics extends TopicsConfig, TName extends keyof TTopics> = z.infer<TTopics[TName]["schema"]>;
5
24
  interface ProducerConfig {
6
25
  /** Kafka broker addresses */
7
26
  brokers: string[];
@@ -27,7 +46,9 @@ interface KafkaManagerOptions {
27
46
  */
28
47
  dontGracefulShutdown?: boolean;
29
48
  /** Named producers configuration */
30
- producers: Record<string, ProducerConfig>;
49
+ producers?: Record<string, ProducerConfig>;
50
+ /** Typed topics configuration with Zod schemas */
51
+ topics?: TopicsConfig;
31
52
  /**
32
53
  * Global timeout for health checks in ms
33
54
  * @default 5000 (5 seconds)
@@ -57,17 +78,20 @@ interface PublishOptions {
57
78
  key?: string;
58
79
  /** Specific partition to send to */
59
80
  partition?: number;
81
+ /** Skip Zod validation (for advanced use cases) */
82
+ skipValidation?: boolean;
60
83
  }
61
- interface PublishBatchOptions {
62
- /** Topic to publish to */
63
- topic: string;
84
+ interface PublishBatchMessage<T = unknown> {
85
+ value: T;
86
+ key?: string;
87
+ headers?: Record<string, string>;
88
+ partition?: number;
89
+ }
90
+ interface PublishBatchOptions<T = unknown> {
64
91
  /** Messages to publish */
65
- messages: {
66
- value: unknown;
67
- key?: string;
68
- headers?: Record<string, string>;
69
- partition?: number;
70
- }[];
92
+ messages: PublishBatchMessage<T>[];
93
+ /** Skip Zod validation (for advanced use cases) */
94
+ skipValidation?: boolean;
71
95
  }
72
96
  interface RecordMetadata {
73
97
  topic: string;
@@ -161,9 +185,13 @@ interface ConnectionStatus {
161
185
  /**
162
186
  * Manager for multiple Kafka producers
163
187
  * Provides lifecycle management, health tracking, and operational guarantees
188
+ *
189
+ * @template TProducers - Union of producer names
190
+ * @template TTopics - Typed topics configuration with Zod schemas
164
191
  */
165
- declare class KafkaManager<TProducers extends string = string> {
192
+ declare class KafkaManager<TProducers extends string = string, TTopics extends TopicsConfig = {}> {
166
193
  private readonly producers;
194
+ private readonly topics;
167
195
  private readonly logger;
168
196
  private readonly enabled;
169
197
  private gracefulShutdownStarted;
@@ -178,10 +206,12 @@ declare class KafkaManager<TProducers extends string = string> {
178
206
  * Create a new KafkaManager instance with multiple named producers
179
207
  * Producers are initialized lazily on first use
180
208
  * Producer names are type-safe based on the configuration
209
+ * Topics configuration provides type-safe publishing with Zod validation
181
210
  */
182
- static create<T extends Record<string, ProducerConfig>>(options: Omit<KafkaManagerOptions, "producers"> & {
183
- producers: T;
184
- }): KafkaManager<keyof T & string>;
211
+ static create<P extends Record<string, ProducerConfig>, T extends TopicsConfig = {}>(options: Omit<KafkaManagerOptions, "producers" | "topics"> & {
212
+ producers?: P;
213
+ topics?: T;
214
+ }): KafkaManager<keyof P & string, T>;
185
215
  private setupGracefulShutdown;
186
216
  gracefulShutdown(signal: string): Promise<void>;
187
217
  /**
@@ -279,13 +309,43 @@ declare class KafkaManager<TProducers extends string = string> {
279
309
  */
280
310
  getConnectionStatus(): Record<TProducers, ConnectionStatus>;
281
311
  /**
282
- * Publish a message using a named producer
312
+ * Get a topic definition
283
313
  */
284
- publish<T = unknown>(producerName: TProducers, topic: string, value: T, options?: PublishOptions): Promise<RecordMetadata[]>;
314
+ private getTopicDefinition;
285
315
  /**
286
- * Publish a batch of messages using a named producer
316
+ * Publish a message to a typed topic
317
+ * Automatically validates payload against Zod schema and routes to correct producer
318
+ *
319
+ * @template TName - Topic name (inferred from topics config)
320
+ * @param topicName - Name of the topic to publish to
321
+ * @param value - Payload (type-checked against topic's Zod schema)
322
+ * @param options - Optional publish options including headers, key, partition
323
+ *
324
+ * @example
325
+ * await kafka.publish('order.created', {
326
+ * orderId: '123',
327
+ * amount: 99.99,
328
+ * customerId: '456',
329
+ * });
330
+ */
331
+ publish<TName extends keyof TTopics & string>(topicName: TName, value: TopicPayload<TTopics, TName>, options?: PublishOptions): Promise<RecordMetadata[]>;
332
+ /**
333
+ * Publish a batch of messages to a typed topic
334
+ * Automatically validates payloads against Zod schema and routes to correct producer
335
+ *
336
+ * @template TName - Topic name (inferred from topics config)
337
+ * @param topicName - Name of the topic to publish to
338
+ * @param options - Batch options with array of messages
339
+ *
340
+ * @example
341
+ * await kafka.publishBatch('order.created', {
342
+ * messages: [
343
+ * { value: { orderId: '1', amount: 10, customerId: 'a' } },
344
+ * { value: { orderId: '2', amount: 20, customerId: 'b' } },
345
+ * ],
346
+ * });
287
347
  */
288
- publishBatch(producerName: TProducers, options: PublishBatchOptions): Promise<RecordMetadata[]>;
348
+ publishBatch<TName extends keyof TTopics & string>(topicName: TName, options: PublishBatchOptions<TopicPayload<TTopics, TName>>): Promise<RecordMetadata[]>;
289
349
  /**
290
350
  * Disconnect all producers
291
351
  */
package/dist/index.d.ts CHANGED
@@ -1,7 +1,26 @@
1
1
  import { ConnectionOptions } from "@platformatic/kafka";
2
2
  import { LoggerInstanceManager } from "@autofleet/logger";
3
+ import { z } from "zod";
3
4
 
4
5
  //#region src/types.d.ts
6
+
7
+ /**
8
+ * Topic definition with Zod schema for type-safe publishing
9
+ */
10
+ interface TopicDefinition<TSchema extends z.ZodType = z.ZodType> {
11
+ /** Zod schema for payload validation */
12
+ schema: TSchema;
13
+ /** Producer to use for this topic (name or alias) */
14
+ producer: string;
15
+ }
16
+ /**
17
+ * Configuration for typed topics
18
+ */
19
+ type TopicsConfig = Record<string, TopicDefinition>;
20
+ /**
21
+ * Extract payload type for a specific topic
22
+ */
23
+ type TopicPayload<TTopics extends TopicsConfig, TName extends keyof TTopics> = z.infer<TTopics[TName]["schema"]>;
5
24
  interface ProducerConfig {
6
25
  /** Kafka broker addresses */
7
26
  brokers: string[];
@@ -27,7 +46,9 @@ interface KafkaManagerOptions {
27
46
  */
28
47
  dontGracefulShutdown?: boolean;
29
48
  /** Named producers configuration */
30
- producers: Record<string, ProducerConfig>;
49
+ producers?: Record<string, ProducerConfig>;
50
+ /** Typed topics configuration with Zod schemas */
51
+ topics?: TopicsConfig;
31
52
  /**
32
53
  * Global timeout for health checks in ms
33
54
  * @default 5000 (5 seconds)
@@ -57,17 +78,20 @@ interface PublishOptions {
57
78
  key?: string;
58
79
  /** Specific partition to send to */
59
80
  partition?: number;
81
+ /** Skip Zod validation (for advanced use cases) */
82
+ skipValidation?: boolean;
60
83
  }
61
- interface PublishBatchOptions {
62
- /** Topic to publish to */
63
- topic: string;
84
+ interface PublishBatchMessage<T = unknown> {
85
+ value: T;
86
+ key?: string;
87
+ headers?: Record<string, string>;
88
+ partition?: number;
89
+ }
90
+ interface PublishBatchOptions<T = unknown> {
64
91
  /** Messages to publish */
65
- messages: {
66
- value: unknown;
67
- key?: string;
68
- headers?: Record<string, string>;
69
- partition?: number;
70
- }[];
92
+ messages: PublishBatchMessage<T>[];
93
+ /** Skip Zod validation (for advanced use cases) */
94
+ skipValidation?: boolean;
71
95
  }
72
96
  interface RecordMetadata {
73
97
  topic: string;
@@ -161,9 +185,13 @@ interface ConnectionStatus {
161
185
  /**
162
186
  * Manager for multiple Kafka producers
163
187
  * Provides lifecycle management, health tracking, and operational guarantees
188
+ *
189
+ * @template TProducers - Union of producer names
190
+ * @template TTopics - Typed topics configuration with Zod schemas
164
191
  */
165
- declare class KafkaManager<TProducers extends string = string> {
192
+ declare class KafkaManager<TProducers extends string = string, TTopics extends TopicsConfig = {}> {
166
193
  private readonly producers;
194
+ private readonly topics;
167
195
  private readonly logger;
168
196
  private readonly enabled;
169
197
  private gracefulShutdownStarted;
@@ -178,10 +206,12 @@ declare class KafkaManager<TProducers extends string = string> {
178
206
  * Create a new KafkaManager instance with multiple named producers
179
207
  * Producers are initialized lazily on first use
180
208
  * Producer names are type-safe based on the configuration
209
+ * Topics configuration provides type-safe publishing with Zod validation
181
210
  */
182
- static create<T extends Record<string, ProducerConfig>>(options: Omit<KafkaManagerOptions, "producers"> & {
183
- producers: T;
184
- }): KafkaManager<keyof T & string>;
211
+ static create<P extends Record<string, ProducerConfig>, T extends TopicsConfig = {}>(options: Omit<KafkaManagerOptions, "producers" | "topics"> & {
212
+ producers?: P;
213
+ topics?: T;
214
+ }): KafkaManager<keyof P & string, T>;
185
215
  private setupGracefulShutdown;
186
216
  gracefulShutdown(signal: string): Promise<void>;
187
217
  /**
@@ -279,13 +309,43 @@ declare class KafkaManager<TProducers extends string = string> {
279
309
  */
280
310
  getConnectionStatus(): Record<TProducers, ConnectionStatus>;
281
311
  /**
282
- * Publish a message using a named producer
312
+ * Get a topic definition
283
313
  */
284
- publish<T = unknown>(producerName: TProducers, topic: string, value: T, options?: PublishOptions): Promise<RecordMetadata[]>;
314
+ private getTopicDefinition;
285
315
  /**
286
- * Publish a batch of messages using a named producer
316
+ * Publish a message to a typed topic
317
+ * Automatically validates payload against Zod schema and routes to correct producer
318
+ *
319
+ * @template TName - Topic name (inferred from topics config)
320
+ * @param topicName - Name of the topic to publish to
321
+ * @param value - Payload (type-checked against topic's Zod schema)
322
+ * @param options - Optional publish options including headers, key, partition
323
+ *
324
+ * @example
325
+ * await kafka.publish('order.created', {
326
+ * orderId: '123',
327
+ * amount: 99.99,
328
+ * customerId: '456',
329
+ * });
330
+ */
331
+ publish<TName extends keyof TTopics & string>(topicName: TName, value: TopicPayload<TTopics, TName>, options?: PublishOptions): Promise<RecordMetadata[]>;
332
+ /**
333
+ * Publish a batch of messages to a typed topic
334
+ * Automatically validates payloads against Zod schema and routes to correct producer
335
+ *
336
+ * @template TName - Topic name (inferred from topics config)
337
+ * @param topicName - Name of the topic to publish to
338
+ * @param options - Batch options with array of messages
339
+ *
340
+ * @example
341
+ * await kafka.publishBatch('order.created', {
342
+ * messages: [
343
+ * { value: { orderId: '1', amount: 10, customerId: 'a' } },
344
+ * { value: { orderId: '2', amount: 20, customerId: 'b' } },
345
+ * ],
346
+ * });
287
347
  */
288
- publishBatch(producerName: TProducers, options: PublishBatchOptions): Promise<RecordMetadata[]>;
348
+ publishBatch<TName extends keyof TTopics & string>(topicName: TName, options: PublishBatchOptions<TopicPayload<TTopics, TName>>): Promise<RecordMetadata[]>;
289
349
  /**
290
350
  * Disconnect all producers
291
351
  */
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import{setTimeout as e}from"node:timers/promises";var t=class extends Error{constructor(...e){super(...e),this.name=`KafkaError`}};const n=`x-timestamp`,r=`autofleet-kafka-producer`;var i=class{constructor(e,t,n){this.name=e,this.config=t,this.logger=n,this.producer=null,this._isConnected=!1,this.initPromise=null,this._lastPingAt=null,this._lastPingSucceededAt=null,this._lastError=null,this._clusterId=null,this._brokerCount=0}async#e(){let{Producer:e,stringSerializers:t}=await import(`@platformatic/kafka`);this.producer=new e({bootstrapBrokers:this.config.brokers,clientId:this.config.clientId||`${r}-${this.name}`,serializers:t,autocreateTopics:this.config.autoCreateTopics??!1,sasl:this.config.sasl,tls:this.config.tls}),this.logger.info(`Kafka: [${this.name}] Initialized producer`,{clientId:this.config.clientId||`${r}-${this.name}`,brokers:this.config.brokers}),this.initPromise=null}async initialize(){this.producer||(this.initPromise??=this.#e(),await this.initPromise)}get isConnected(){return this._isConnected}getHealth(){return{name:this.name,enabled:!0,isConnected:this._isConnected,lastPingAt:this._lastPingAt,lastPingSucceededAt:this._lastPingSucceededAt,lastError:this._lastError,clusterId:this._clusterId,brokerCount:this._brokerCount,brokers:this.config.brokers}}buildConnectionErrorMessage(e){let t=this.config.brokers.join(`, `),n=this.config.brokers[0],[i,a]=n?n.split(`:`):[``,``];return`[${this.name}] Failed to connect to Kafka brokers: ${e.message}\n\nPossible causes:\n 1. Brokers are unreachable: ${t}\n 2. Network connectivity issues\n 3. Firewall blocking ports\n 4. ${this.config.sasl?`SASL authentication failed`:`Authentication not configured but required`}\n\nTroubleshooting:\n - Verify brokers are running and accessible\n`+(i&&a?` - Test connectivity: nc -zv ${i} ${a}\n`:``)+` - Check network policies and firewall rules\n ${this.config.sasl?`- Verify SASL credentials are correct
2
- `:``}\nCurrent configuration:\n Brokers: ${t}\n Client ID: ${this.config.clientId||`${r}-${this.name}`}\n SASL: ${this.config.sasl?`enabled (${this.config.sasl.mechanism})`:`disabled`}\n SASL Password: ${this.config.sasl?this.config.sasl.password?`****`:`not set`:`N/A`}\n Auto-create topics: ${this.config.autoCreateTopics??!1}`}async ping(n){if(this._lastPingAt=Date.now(),!(this._isConnected&&!n?.force)){await this.initialize();try{let t=n?.timeout??5e3,r=Error(`Connection timeout after ${t}ms`),i=e(t).then(()=>{throw r}),a=await Promise.race([this.producer.metadata({topics:[]}),i]);this._isConnected=!0,this._lastPingSucceededAt=Date.now(),this._clusterId=a.id,this._brokerCount=a.brokers.size,this._lastError=null,this.logger.info(`Kafka: [${this.name}] Successfully connected to cluster`,{clusterId:a.id,brokers:a.brokers.size,duration:Date.now()-this._lastPingAt})}catch(e){let n=e;throw this._isConnected=!1,this._lastError={message:n.message,timestamp:Date.now(),stack:n.stack},this.logger.error(`Kafka: [${this.name}] Failed to connect to brokers`,{error:n.message,duration:Date.now()-this._lastPingAt}),new t(this.buildConnectionErrorMessage(n),{cause:n})}}}async publish(e,r,i){if(!e)throw new t(`[${this.name}] Topic name is required`);await this.ping();try{let t={[n]:Date.now().toString(),...i?.headers},a=await this.producer.send({messages:[{topic:e,value:JSON.stringify(r),key:i?.key,partition:i?.partition,headers:t}]});return this.logger.debug(`Kafka: [${this.name}] Published message to topic ${e}`,{topic:e}),[{topic:e,partition:i?.partition??0,offset:a?.offsets?.[0]?.offset?.toString()??`0`}]}catch(n){let i=n;throw this.logger.error(`Kafka: [${this.name}] Error publishing to topic ${e}`,{error:n,value:r}),new t(`[${this.name}] Failed to publish to topic ${e}: ${i.message||`Unknown error`}`,{cause:n})}}async publishBatch(e){if(!e.topic)throw new t(`[${this.name}] Topic name is required`);if(!e.messages||e.messages.length===0)throw new t(`[${this.name}] At least one message is required`);await this.ping();try{let t=e.messages.map(t=>({topic:e.topic,value:JSON.stringify(t.value),key:t.key,partition:t.partition,headers:{[n]:Date.now().toString(),...t.headers}})),r=await this.producer.send({messages:t});return this.logger.debug(`Kafka: [${this.name}] Published ${t.length} messages to topic ${e.topic}`,{topic:e.topic,count:t.length}),t.map((t,n)=>({topic:e.topic,partition:t.partition??0,offset:r?.offsets?.[n]?.offset?.toString()??n.toString()}))}catch(n){let r=n;throw this.logger.error(`Kafka: [${this.name}] Error publishing batch to topic ${e.topic}`,{error:n}),new t(`[${this.name}] Failed to publish batch to topic ${e.topic}: ${r.message||`Unknown error`}`,{cause:n})}}async disconnect(){if(!this.producer||!this._isConnected){this.logger.debug(`Kafka: [${this.name}] Producer already disconnected`);return}this.logger.info(`Kafka: [${this.name}] Disconnecting producer`);try{await this.producer.close(),this._isConnected=!1,this.logger.info(`Kafka: [${this.name}] Producer disconnected successfully`)}catch(e){throw this.logger.error(`Kafka: [${this.name}] Error disconnecting producer`,{error:e}),new t(`[${this.name}] Failed to disconnect producer: ${e.message}`,{cause:e})}}},a=class{constructor(e,t,n){this.name=e,this.brokers=t,this.logger=n,this.isConnected=!0}getHealth(){return{name:this.name,enabled:!1,isConnected:!0,lastPingAt:null,lastPingSucceededAt:null,lastError:null,clusterId:`mock-cluster`,brokerCount:0,brokers:this.brokers}}async ping(){return this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping ping`),Promise.resolve()}async publish(e,t,n){return this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping publish to topic: ${e}`,{value:t}),Promise.resolve([{topic:e,partition:0,offset:`0`}])}async publishBatch(e){return this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping batch publish to topic: ${e.topic}`,{messageCount:e.messages.length}),Promise.resolve(e.messages.map(()=>({topic:e.topic,partition:0,offset:`0`})))}async disconnect(){return this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping disconnect`),Promise.resolve()}},o=class e{constructor(e){if(this.producers=new Map,this.gracefulShutdownStarted=!1,this.fatalError=null,this.lastReadinessCheck=null,this.logger=e.logger,this.enabled=e.enabled??!0,this.healthCheckTimeoutMs=e.healthCheckTimeoutMs??5e3,this.healthCheckCacheMs=e.healthCheckCacheMs??1e3,this.bootstrapTimeoutMs=e.bootstrapTimeoutMs??3e4,this.strictBootstrap=e.strictBootstrap??!0,this.logger.info(`Kafka: Initialized KafkaManager`,{enabled:this.enabled,producerCount:Object.keys(e.producers).length,producers:Object.keys(e.producers)}),this.enabled)for(let[n,r]of Object.entries(e.producers)){if(!r.brokers||r.brokers.length===0)throw new t(`[${n}] At least one broker is required`);this.producers.set(n,new i(n,r,this.logger))}else{for(let[t,n]of Object.entries(e.producers))this.producers.set(t,new a(t,n.brokers,this.logger));this.logger.info(`Kafka: Created mock producers (Kafka disabled)`)}e.dontGracefulShutdown||this.setupGracefulShutdown()}static create(t){return new e(t)}setupGracefulShutdown(){this.logger.info(`Kafka: [graceful-shutdown] adding graceful shutdown for process.pid ${process.pid}`),process.on(`SIGTERM`,async()=>{await this.gracefulShutdown(`SIGTERM`)}),process.on(`SIGINT`,async()=>{await this.gracefulShutdown(`SIGINT`)})}async gracefulShutdown(e){if(!this.gracefulShutdownStarted){this.gracefulShutdownStarted=!0,this.logger.info(`Kafka: [graceful-shutdown] received ${e}! Disconnecting all producers...`);try{await this.disconnect(),this.logger.info(`Kafka: [graceful-shutdown] all producers disconnected successfully`)}catch(e){throw this.logger.error(`Kafka: [graceful-shutdown] error during shutdown`,{error:e}),e}}}get isEnabled(){return this.enabled}get producerNames(){return Array.from(this.producers.keys())}hasProducer(e){return this.producers.has(e)}getProducer(e){let n=this.producers.get(e);if(!n)throw new t(`Producer '${e}' not found. Available producers: ${this.producerNames.join(`, `)}`);return n}async bootstrap(e){let n=Date.now(),r=e?.timeoutMs??this.bootstrapTimeoutMs,i=e?.strict??this.strictBootstrap,a=e?.producers??this.producerNames;this.logger.info(`Kafka: Starting bootstrap`,{producers:a,timeout:r,strict:i});let o={},s=[];for(let e of a){let t=this.producers.get(e);if(!t){o[e]={success:!1,duration:0,error:`Producer '${e}' not found`},s.push(`Producer '${e}' not found`);continue}let n=Date.now();try{await t.ping({timeout:r,force:!0});let i=t.getHealth();o[e]={success:!0,duration:Date.now()-n,clusterId:i.clusterId??void 0,brokerCount:i.brokerCount},this.logger.info(`Kafka: [${e}] Bootstrap successful`,{duration:Date.now()-n,clusterId:i.clusterId,brokerCount:i.brokerCount})}catch(t){let r=t;o[e]={success:!1,duration:Date.now()-n,error:r.message},s.push(`[${e}] ${r.message}`),this.logger.error(`Kafka: [${e}] Bootstrap failed`,{duration:Date.now()-n,error:r.message})}}let c=Date.now()-n,l=Object.values(o).filter(e=>e.success).length,u=Object.keys(o).length,d=i?l===u:l>0,f={success:d,duration:c,results:o};if(d)this.logger.info(`Kafka: Bootstrap completed successfully`,{duration:c,successCount:l,totalCount:u});else{let e=`Kafka bootstrap failed (${l}/${u} producers succeeded)\n\nFailures:\n${s.map(e=>` - ${e}`).join(`
3
- `)}\n\nConfiguration:\n Strict mode: ${i}\n Timeout: ${r}ms\n Producers attempted: ${a.join(`, `)}`;throw this.logger.error(`Kafka: Bootstrap failed`,{duration:c,successCount:l,totalCount:u,errors:s}),new t(e)}return f}getHealth(){let e={};for(let[t,n]of this.producers.entries())e[t]=n.getHealth();return e}getProducerHealth(e){return this.getProducer(e).getHealth()}isLive(){return!this.fatalError&&!this.gracefulShutdownStarted}async ping(){let e=Array.from(this.producers.values(),e=>e.ping());await Promise.all(e)}async pingProducer(e){await this.getProducer(e).ping()}async isReady(e){if(!this.enabled)return!0;let t=Date.now();if(!(e?.force??!1)&&this.lastReadinessCheck){let e=t-this.lastReadinessCheck.timestamp;if(e<this.healthCheckCacheMs)return this.logger.debug(`Kafka: Using cached readiness result`,{result:this.lastReadinessCheck.result,age:e}),this.lastReadinessCheck.result}try{let n=e?.timeout??this.healthCheckTimeoutMs,r=Array.from(this.producers.values()).map(e=>e.ping({timeout:n,force:!0}));return await Promise.all(r),this.lastReadinessCheck={result:!0,timestamp:t},!0}catch(e){return this.lastReadinessCheck={result:!1,timestamp:t},this.logger.warn(`Kafka: Readiness check failed`,{error:e.message}),!1}}getConnectionStatus(){let e={};for(let[t,n]of this.producers.entries()){let r=n.getHealth();e[t]={connected:r.isConnected,lastSuccessAt:r.lastPingSucceededAt,lastError:r.lastError?.message??null}}return e}async publish(e,t,n,r){return this.getProducer(e).publish(t,n,r)}async publishBatch(e,t){return this.getProducer(e).publishBatch(t)}async disconnect(){let e=Array.from(this.producers.values()).map(e=>e.disconnect());await Promise.all(e),this.logger.info(`Kafka: All producers disconnected`)}async disconnectProducer(e){await this.getProducer(e).disconnect()}};export{t as KafkaError,o as KafkaManager};
2
+ `:``}\nCurrent configuration:\n Brokers: ${t}\n Client ID: ${this.config.clientId||`${r}-${this.name}`}\n SASL: ${this.config.sasl?`enabled (${this.config.sasl.mechanism})`:`disabled`}\n SASL Password: ${this.config.sasl?this.config.sasl.password?`****`:`not set`:`N/A`}\n Auto-create topics: ${this.config.autoCreateTopics??!1}`}async ping(n){if(this._lastPingAt=Date.now(),!(this._isConnected&&!n?.force)){await this.initialize();try{let t=n?.timeout??5e3,r=Error(`Connection timeout after ${t}ms`),i=e(t).then(()=>{throw r}),a=await Promise.race([this.producer.metadata({topics:[]}),i]);this._isConnected=!0,this._lastPingSucceededAt=Date.now(),this._clusterId=a.id,this._brokerCount=a.brokers.size,this._lastError=null,this.logger.info(`Kafka: [${this.name}] Successfully connected to cluster`,{clusterId:a.id,brokers:a.brokers.size,duration:Date.now()-this._lastPingAt})}catch(e){let n=e;throw this._isConnected=!1,this._lastError={message:n.message,timestamp:Date.now(),stack:n.stack},this.logger.error(`Kafka: [${this.name}] Failed to connect to brokers`,{error:n.message,duration:Date.now()-this._lastPingAt}),new t(this.buildConnectionErrorMessage(n),{cause:n})}}}async publish(e,r,i){if(!e)throw new t(`[${this.name}] Topic name is required`);await this.ping();try{let t={[n]:Date.now().toString(),...i?.headers},a=await this.producer.send({messages:[{topic:e,value:JSON.stringify(r),key:i?.key,partition:i?.partition,headers:t}]});return this.logger.debug(`Kafka: [${this.name}] Published message to topic ${e}`,{topic:e}),[{topic:e,partition:i?.partition??0,offset:a?.offsets?.[0]?.offset?.toString()??`0`}]}catch(n){let i=n;throw this.logger.error(`Kafka: [${this.name}] Error publishing to topic ${e}`,{error:n,value:r}),new t(`[${this.name}] Failed to publish to topic ${e}: ${i.message||`Unknown error`}`,{cause:n})}}async publishBatch(e,r){if(!e)throw new t(`[${this.name}] Topic name is required`);if(!r.messages||r.messages.length===0)throw new t(`[${this.name}] At least one message is required`);await this.ping();try{let t=r.messages.map(t=>({topic:e,value:JSON.stringify(t.value),key:t.key,partition:t.partition,headers:{[n]:Date.now().toString(),...t.headers}})),i=await this.producer.send({messages:t});return this.logger.debug(`Kafka: [${this.name}] Published ${t.length} messages to topic ${e}`,{topic:e,count:t.length}),t.map((t,n)=>({topic:e,partition:t.partition??0,offset:i?.offsets?.[n]?.offset?.toString()??n.toString()}))}catch(n){let r=n;throw this.logger.error(`Kafka: [${this.name}] Error publishing batch to topic ${e}`,{error:n}),new t(`[${this.name}] Failed to publish batch to topic ${e}: ${r.message||`Unknown error`}`,{cause:n})}}async disconnect(){if(!this.producer||!this._isConnected){this.logger.debug(`Kafka: [${this.name}] Producer already disconnected`);return}this.logger.info(`Kafka: [${this.name}] Disconnecting producer`);try{await this.producer.close(),this._isConnected=!1,this.logger.info(`Kafka: [${this.name}] Producer disconnected successfully`)}catch(e){throw this.logger.error(`Kafka: [${this.name}] Error disconnecting producer`,{error:e}),new t(`[${this.name}] Failed to disconnect producer: ${e.message}`,{cause:e})}}},a=class{constructor(e,t,n){this.name=e,this.brokers=t,this.logger=n,this.isConnected=!0}getHealth(){return{name:this.name,enabled:!1,isConnected:!0,lastPingAt:null,lastPingSucceededAt:null,lastError:null,clusterId:`mock-cluster`,brokerCount:0,brokers:this.brokers}}async ping(){return this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping ping`),Promise.resolve()}async publish(e,t,n){return this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping publish to topic: ${e}`,{value:t}),Promise.resolve([{topic:e,partition:0,offset:`0`}])}async publishBatch(e,t){return this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping batch publish to topic: ${e}`,{messageCount:t.messages.length}),Promise.resolve(t.messages.map(()=>({topic:e,partition:0,offset:`0`})))}async disconnect(){return this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping disconnect`),Promise.resolve()}},o=class e{constructor(e){if(this.producers=new Map,this.topics=new Map,this.gracefulShutdownStarted=!1,this.fatalError=null,this.lastReadinessCheck=null,this.logger=e.logger,this.enabled=e.enabled??!0,this.healthCheckTimeoutMs=e.healthCheckTimeoutMs??5e3,this.healthCheckCacheMs=e.healthCheckCacheMs??1e3,this.bootstrapTimeoutMs=e.bootstrapTimeoutMs??3e4,this.strictBootstrap=e.strictBootstrap??!0,e.topics)for(let[t,n]of Object.entries(e.topics))this.topics.set(t,n);if(this.logger.info(`Kafka: Initialized KafkaManager`,{enabled:this.enabled,producerCount:Object.keys(e.producers??{}).length,producers:Object.keys(e.producers??{}),topicCount:this.topics.size,topics:Array.from(this.topics.keys())}),this.enabled)for(let[n,r]of Object.entries(e.producers??{})){if(!r.brokers||r.brokers.length===0)throw new t(`[${n}] At least one broker is required`);this.producers.set(n,new i(n,r,this.logger))}else{for(let[t,n]of Object.entries(e.producers??{}))this.producers.set(t,new a(t,n.brokers,this.logger));this.producers.size>0&&this.logger.info(`Kafka: Created mock producers (Kafka disabled)`)}e.dontGracefulShutdown||this.setupGracefulShutdown()}static create(n){if(n.topics){let e=Object.keys(n.producers??{});for(let[r,i]of Object.entries(n.topics))if(!e.includes(i.producer))throw new t(`Topic '${r}' references unknown producer '${i.producer}'. Available producers: ${e.join(`, `)}`)}return new e(n)}setupGracefulShutdown(){this.logger.info(`Kafka: [graceful-shutdown] adding graceful shutdown for process.pid ${process.pid}`),process.on(`SIGTERM`,async()=>{await this.gracefulShutdown(`SIGTERM`)}),process.on(`SIGINT`,async()=>{await this.gracefulShutdown(`SIGINT`)})}async gracefulShutdown(e){if(!this.gracefulShutdownStarted){this.gracefulShutdownStarted=!0,this.logger.info(`Kafka: [graceful-shutdown] received ${e}! Disconnecting all producers...`);try{await this.disconnect(),this.logger.info(`Kafka: [graceful-shutdown] all producers disconnected successfully`)}catch(e){throw this.logger.error(`Kafka: [graceful-shutdown] error during shutdown`,{error:e}),e}}}get isEnabled(){return this.enabled}get producerNames(){return Array.from(this.producers.keys())}hasProducer(e){return this.producers.has(e)}getProducer(e){let n=this.producers.get(e);if(!n)throw new t(`Producer '${e}' not found. Available producers: ${this.producerNames.join(`, `)}`);return n}async bootstrap(e){let n=Date.now(),r=e?.timeoutMs??this.bootstrapTimeoutMs,i=e?.strict??this.strictBootstrap,a=e?.producers??this.producerNames;this.logger.info(`Kafka: Starting bootstrap`,{producers:a,timeout:r,strict:i});let o={},s=[];for(let e of a){let t=this.producers.get(e);if(!t){o[e]={success:!1,duration:0,error:`Producer '${e}' not found`},s.push(`Producer '${e}' not found`);continue}let n=Date.now();try{await t.ping({timeout:r,force:!0});let i=t.getHealth();o[e]={success:!0,duration:Date.now()-n,clusterId:i.clusterId??void 0,brokerCount:i.brokerCount},this.logger.info(`Kafka: [${e}] Bootstrap successful`,{duration:Date.now()-n,clusterId:i.clusterId,brokerCount:i.brokerCount})}catch(t){let r=t;o[e]={success:!1,duration:Date.now()-n,error:r.message},s.push(`[${e}] ${r.message}`),this.logger.error(`Kafka: [${e}] Bootstrap failed`,{duration:Date.now()-n,error:r.message})}}let c=Date.now()-n,l=Object.values(o).filter(e=>e.success).length,u=Object.keys(o).length,d=i?l===u:l>0,f={success:d,duration:c,results:o};if(d)this.logger.info(`Kafka: Bootstrap completed successfully`,{duration:c,successCount:l,totalCount:u});else{let e=`Kafka bootstrap failed (${l}/${u} producers succeeded)\n\nFailures:\n${s.map(e=>` - ${e}`).join(`
3
+ `)}\n\nConfiguration:\n Strict mode: ${i}\n Timeout: ${r}ms\n Producers attempted: ${a.join(`, `)}`;throw this.logger.error(`Kafka: Bootstrap failed`,{duration:c,successCount:l,totalCount:u,errors:s}),new t(e)}return f}getHealth(){let e={};for(let[t,n]of this.producers.entries())e[t]=n.getHealth();return e}getProducerHealth(e){return this.getProducer(e).getHealth()}isLive(){return!this.fatalError&&!this.gracefulShutdownStarted}async ping(){let e=Array.from(this.producers.values(),e=>e.ping());await Promise.all(e)}async pingProducer(e){await this.getProducer(e).ping()}async isReady(e){if(!this.enabled)return!0;let t=Date.now();if(!(e?.force??!1)&&this.lastReadinessCheck){let e=t-this.lastReadinessCheck.timestamp;if(e<this.healthCheckCacheMs)return this.logger.debug(`Kafka: Using cached readiness result`,{result:this.lastReadinessCheck.result,age:e}),this.lastReadinessCheck.result}try{let n=e?.timeout??this.healthCheckTimeoutMs,r=Array.from(this.producers.values()).map(e=>e.ping({timeout:n,force:!0}));return await Promise.all(r),this.lastReadinessCheck={result:!0,timestamp:t},!0}catch(e){return this.lastReadinessCheck={result:!1,timestamp:t},this.logger.warn(`Kafka: Readiness check failed`,{error:e.message}),!1}}getConnectionStatus(){let e={};for(let[t,n]of this.producers.entries()){let r=n.getHealth();e[t]={connected:r.isConnected,lastSuccessAt:r.lastPingSucceededAt,lastError:r.lastError?.message??null}}return e}getTopicDefinition(e){let n=this.topics.get(e);if(!n)throw new t(`Topic '${e}' not found. Available topics: ${Array.from(this.topics.keys()).join(`, `)}`);return n}async publish(e,n,r){let i=this.getTopicDefinition(e);if(!r?.skipValidation)try{i.schema.parse(n)}catch(r){throw this.logger.error(`Kafka: Validation failed for topic '${e}'`,{error:r,value:n}),new t(`Validation failed for topic '${e}': ${r.message}`,{cause:r})}return this.getProducer(i.producer).publish(e,n,r)}async publishBatch(e,n){let r=this.getTopicDefinition(e);if(!n.skipValidation)for(let i=0;i<n.messages.length;i++)try{r.schema.parse(n.messages[i].value)}catch(r){throw this.logger.error(`Kafka: Validation failed for topic '${e}' message ${i}`,{error:r,value:n.messages[i].value}),new t(`Validation failed for topic '${e}' message ${i}: ${r.message}`,{cause:r})}return this.getProducer(r.producer).publishBatch(e,n)}async disconnect(){let e=Array.from(this.producers.values()).map(e=>e.disconnect());await Promise.all(e),this.logger.info(`Kafka: All producers disconnected`)}async disconnectProducer(e){await this.getProducer(e).disconnect()}};export{t as KafkaError,o as KafkaManager};
4
4
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["name: string","config: ProducerConfig","logger: LoggerInstanceManager","#loadKafkaAndSetup","headers: Record<string, string>","name: string","brokers: string[]","logger: LoggerInstanceManager","results: BootstrapResult['results']","errors: string[]","result: BootstrapResult","health: Record<string, ProducerHealth>","status: Record<string, ConnectionStatus>"],"sources":["../src/kafkaError.ts","../src/consts.ts","../src/producer.ts","../src/mockProducer.ts","../src/manager.ts"],"sourcesContent":["export default class KafkaError extends Error {\n name = 'KafkaError';\n}\n","export const TIMESTAMP_HEADER = 'x-timestamp';\n\nexport const DEFAULT_CLIENT_ID = 'autofleet-kafka-producer';\n","import { setTimeout } from 'node:timers/promises';\nimport type { LoggerInstanceManager } from '@autofleet/logger';\nimport type { Producer } from '@platformatic/kafka/dist/clients/producer/index.ts';\nimport KafkaError from './kafkaError';\nimport {\n DEFAULT_CLIENT_ID,\n TIMESTAMP_HEADER,\n} from './consts';\nimport type {\n ProducerConfig,\n PublishOptions,\n PublishBatchOptions,\n RecordMetadata,\n ProducerHealth,\n IProducer,\n} from './types';\n\n/**\n * Wrapper for a single Kafka producer instance with lazy initialization\n * Tracks health metadata including connection state, timestamps, and errors\n */\nexport class ProducerWrapper implements IProducer {\n private producer: Producer<string, string, string, string> | null = null;\n private _isConnected = false;\n private initPromise: Promise<void> | null = null;\n\n // Health tracking metadata\n private _lastPingAt: number | null = null;\n private _lastPingSucceededAt: number | null = null;\n private _lastError: { message: string; timestamp: number; stack?: string; } | null = null;\n private _clusterId: string | null = null;\n private _brokerCount = 0;\n\n constructor(\n private readonly name: string,\n private readonly config: ProducerConfig,\n private readonly logger: LoggerInstanceManager,\n ) {}\n\n async #loadKafkaAndSetup(): Promise<void> {\n const { Producer, stringSerializers } = await import('@platformatic/kafka');\n\n this.producer = new Producer({\n bootstrapBrokers: this.config.brokers,\n clientId: this.config.clientId || `${DEFAULT_CLIENT_ID}-${this.name}`,\n serializers: stringSerializers,\n autocreateTopics: this.config.autoCreateTopics ?? false,\n sasl: this.config.sasl,\n tls: this.config.tls,\n });\n\n this.logger.info(`Kafka: [${this.name}] Initialized producer`, {\n clientId: this.config.clientId || `${DEFAULT_CLIENT_ID}-${this.name}`,\n brokers: this.config.brokers,\n });\n\n // Clear promise to allow garbage collection\n this.initPromise = null;\n }\n\n private async initialize(): Promise<void> {\n if (this.producer) {\n return;\n }\n\n this.initPromise ??= this.#loadKafkaAndSetup();\n await this.initPromise;\n }\n\n get isConnected(): boolean {\n return this._isConnected;\n }\n\n /**\n * Get comprehensive health snapshot for this producer\n */\n getHealth(): ProducerHealth {\n return {\n name: this.name,\n enabled: true,\n isConnected: this._isConnected,\n lastPingAt: this._lastPingAt,\n lastPingSucceededAt: this._lastPingSucceededAt,\n lastError: this._lastError,\n clusterId: this._clusterId,\n brokerCount: this._brokerCount,\n brokers: this.config.brokers,\n };\n }\n\n /**\n * Build an actionable error message with troubleshooting guidance\n */\n private buildConnectionErrorMessage(error: Error): string {\n const brokerList = this.config.brokers.join(', ');\n const firstBroker = this.config.brokers[0];\n const [host, port] = firstBroker ? firstBroker.split(':') : ['', ''];\n\n return `[${this.name}] Failed to connect to Kafka brokers: ${error.message}\\n\\n`\n + `Possible causes:\\n`\n + ` 1. Brokers are unreachable: ${brokerList}\\n`\n + ` 2. Network connectivity issues\\n`\n + ` 3. Firewall blocking ports\\n`\n + ` 4. ${this.config.sasl ? 'SASL authentication failed' : 'Authentication not configured but required'}\\n\\n`\n + `Troubleshooting:\\n`\n + ` - Verify brokers are running and accessible\\n`\n + (host && port ? ` - Test connectivity: nc -zv ${host} ${port}\\n` : '')\n + ` - Check network policies and firewall rules\\n`\n + ` ${this.config.sasl ? '- Verify SASL credentials are correct\\n' : ''}\\n`\n + `Current configuration:\\n`\n + ` Brokers: ${brokerList}\\n`\n + ` Client ID: ${this.config.clientId || `${DEFAULT_CLIENT_ID}-${this.name}`}\\n`\n + ` SASL: ${this.config.sasl ? `enabled (${this.config.sasl.mechanism})` : 'disabled'}\\n`\n + ` SASL Password: ${this.config.sasl ? (this.config.sasl.password ? '****' : 'not set') : 'N/A'}\\n`\n + ` Auto-create topics: ${this.config.autoCreateTopics ?? false}`;\n }\n\n /**\n * Ping the Kafka cluster to verify connectivity\n * @param options.timeout - Maximum time to wait for connection (ms)\n * @param options.force - Force reconnection even if already connected\n */\n async ping(options?: { timeout?: number; force?: boolean; }): Promise<void> {\n this._lastPingAt = Date.now();\n\n // Skip if already connected and not forcing revalidation\n if (this._isConnected && !options?.force) {\n return;\n }\n\n await this.initialize();\n\n try {\n // Create error before promise to preserve stack trace\n const timeoutMs = options?.timeout ?? 5000;\n const timeoutError = new Error(`Connection timeout after ${timeoutMs}ms`);\n const timeoutPromise = setTimeout(timeoutMs).then(() => {\n throw timeoutError;\n });\n\n // Race between metadata fetch and timeout\n const metadata = await Promise.race([\n this.producer!.metadata({ topics: [] }),\n timeoutPromise,\n ]);\n\n this._isConnected = true;\n this._lastPingSucceededAt = Date.now();\n this._clusterId = metadata.id;\n this._brokerCount = metadata.brokers.size;\n this._lastError = null; // Clear any previous errors\n\n this.logger.info(`Kafka: [${this.name}] Successfully connected to cluster`, {\n clusterId: metadata.id,\n brokers: metadata.brokers.size,\n duration: Date.now() - this._lastPingAt,\n });\n } catch (error) {\n const err = error as Error;\n this._isConnected = false;\n this._lastError = {\n message: err.message,\n timestamp: Date.now(),\n stack: err.stack,\n };\n\n this.logger.error(`Kafka: [${this.name}] Failed to connect to brokers`, {\n error: err.message,\n duration: Date.now() - this._lastPingAt,\n });\n\n throw new KafkaError(this.buildConnectionErrorMessage(err), { cause: err });\n }\n }\n\n async publish<T = unknown>(\n topic: string,\n value: T,\n options?: PublishOptions,\n ): Promise<RecordMetadata[]> {\n if (!topic) {\n throw new KafkaError(`[${this.name}] Topic name is required`);\n }\n\n await this.ping();\n\n try {\n const headers: Record<string, string> = {\n [TIMESTAMP_HEADER]: Date.now().toString(),\n ...options?.headers,\n };\n\n const result = await this.producer!.send({\n messages: [{\n topic,\n value: JSON.stringify(value),\n key: options?.key,\n partition: options?.partition,\n headers,\n }],\n });\n\n this.logger.debug(`Kafka: [${this.name}] Published message to topic ${topic}`, {\n topic,\n });\n\n return [{\n topic,\n partition: options?.partition ?? 0,\n offset: result?.offsets?.[0]?.offset?.toString() ?? '0',\n }];\n } catch (error) {\n const err = error as { message?: string; };\n this.logger.error(`Kafka: [${this.name}] Error publishing to topic ${topic}`, { error, value });\n throw new KafkaError(`[${this.name}] Failed to publish to topic ${topic}: ${err.message || 'Unknown error'}`, { cause: error });\n }\n }\n\n async publishBatch(options: PublishBatchOptions): Promise<RecordMetadata[]> {\n if (!options.topic) {\n throw new KafkaError(`[${this.name}] Topic name is required`);\n }\n\n if (!options.messages || options.messages.length === 0) {\n throw new KafkaError(`[${this.name}] At least one message is required`);\n }\n\n await this.ping();\n\n try {\n const messages = options.messages.map(msg => ({\n topic: options.topic,\n value: JSON.stringify(msg.value),\n key: msg.key,\n partition: msg.partition,\n headers: {\n [TIMESTAMP_HEADER]: Date.now().toString(),\n ...msg.headers,\n },\n }));\n\n const result = await this.producer!.send({ messages });\n\n this.logger.debug(`Kafka: [${this.name}] Published ${messages.length} messages to topic ${options.topic}`, {\n topic: options.topic,\n count: messages.length,\n });\n\n return messages.map((msg, idx) => ({\n topic: options.topic,\n partition: msg.partition ?? 0,\n offset: result?.offsets?.[idx]?.offset?.toString() ?? idx.toString(),\n }));\n } catch (error) {\n const err = error as { message?: string; };\n this.logger.error(`Kafka: [${this.name}] Error publishing batch to topic ${options.topic}`, { error });\n throw new KafkaError(`[${this.name}] Failed to publish batch to topic ${options.topic}: ${err.message || 'Unknown error'}`, { cause: error });\n }\n }\n\n async disconnect(): Promise<void> {\n if (!this.producer || !this._isConnected) {\n this.logger.debug(`Kafka: [${this.name}] Producer already disconnected`);\n return;\n }\n\n this.logger.info(`Kafka: [${this.name}] Disconnecting producer`);\n\n try {\n await this.producer.close();\n this._isConnected = false;\n this.logger.info(`Kafka: [${this.name}] Producer disconnected successfully`);\n } catch (error) {\n this.logger.error(`Kafka: [${this.name}] Error disconnecting producer`, { error });\n throw new KafkaError(`[${this.name}] Failed to disconnect producer: ${(error as Error).message}`, { cause: error });\n }\n }\n}\n","import type { LoggerInstanceManager } from '@autofleet/logger';\nimport type {\n PublishOptions,\n PublishBatchOptions,\n RecordMetadata,\n ProducerHealth,\n IProducer,\n} from './types';\n\n/**\n * Mock producer for when Kafka is disabled\n */\nexport class MockProducer implements IProducer {\n constructor(private readonly name: string, private readonly brokers: string[], private readonly logger: LoggerInstanceManager) {}\n\n readonly isConnected = true;\n\n /**\n * Get health snapshot for mock producer\n */\n getHealth(): ProducerHealth {\n return {\n name: this.name,\n enabled: false, // Mock mode means Kafka is disabled\n isConnected: true, // Mock is always \"connected\"\n lastPingAt: null,\n lastPingSucceededAt: null,\n lastError: null,\n clusterId: 'mock-cluster',\n brokerCount: 0,\n brokers: this.brokers,\n };\n }\n\n async ping(): Promise<void> {\n this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping ping`);\n return Promise.resolve();\n }\n\n async publish<T = unknown>(topic: string, value: T, _options?: PublishOptions): Promise<RecordMetadata[]> {\n this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping publish to topic: ${topic}`, { value });\n return Promise.resolve([{\n topic,\n partition: 0,\n offset: '0',\n }]);\n }\n\n async publishBatch(options: PublishBatchOptions): Promise<RecordMetadata[]> {\n this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping batch publish to topic: ${options.topic}`, {\n messageCount: options.messages.length,\n });\n return Promise.resolve(options.messages.map(() => ({\n topic: options.topic,\n partition: 0,\n offset: '0',\n })));\n }\n\n async disconnect(): Promise<void> {\n this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping disconnect`);\n return Promise.resolve();\n }\n}\n","import type { LoggerInstanceManager } from '@autofleet/logger';\nimport KafkaError from './kafkaError';\nimport { ProducerWrapper } from './producer';\nimport { MockProducer } from './mockProducer';\nimport type {\n KafkaManagerOptions,\n PublishOptions,\n PublishBatchOptions,\n RecordMetadata,\n ProducerHealth,\n BootstrapOptions,\n BootstrapResult,\n ReadinessOptions,\n ConnectionStatus,\n IProducer,\n ProducerConfig,\n} from './types';\n\n/**\n * Manager for multiple Kafka producers\n * Provides lifecycle management, health tracking, and operational guarantees\n */\nexport class KafkaManager<TProducers extends string = string> {\n private readonly producers = new Map<string, IProducer>();\n private readonly logger: LoggerInstanceManager;\n private readonly enabled: boolean;\n private gracefulShutdownStarted = false;\n private fatalError: Error | null = null;\n\n // Configuration options\n private readonly healthCheckTimeoutMs: number;\n private readonly healthCheckCacheMs: number;\n private readonly bootstrapTimeoutMs: number;\n private readonly strictBootstrap: boolean;\n\n // Health check cache\n private lastReadinessCheck: { result: boolean; timestamp: number; } | null = null;\n\n private constructor(options: KafkaManagerOptions) {\n this.logger = options.logger;\n this.enabled = options.enabled ?? true;\n this.healthCheckTimeoutMs = options.healthCheckTimeoutMs ?? 5000;\n this.healthCheckCacheMs = options.healthCheckCacheMs ?? 1000;\n this.bootstrapTimeoutMs = options.bootstrapTimeoutMs ?? 30_000;\n this.strictBootstrap = options.strictBootstrap ?? true;\n\n this.logger.info('Kafka: Initialized KafkaManager', {\n enabled: this.enabled,\n producerCount: Object.keys(options.producers).length,\n producers: Object.keys(options.producers),\n });\n\n // Create producers\n if (!this.enabled) {\n // Create mock producers\n for (const [name, config] of Object.entries(options.producers)) {\n this.producers.set(name, new MockProducer(name, config.brokers, this.logger));\n }\n this.logger.info('Kafka: Created mock producers (Kafka disabled)');\n } else {\n // Create real producers (they will initialize lazily)\n for (const [name, config] of Object.entries(options.producers)) {\n if (!config.brokers || config.brokers.length === 0) {\n throw new KafkaError(`[${name}] At least one broker is required`);\n }\n\n this.producers.set(name, new ProducerWrapper(name, config, this.logger));\n }\n }\n\n if (!options.dontGracefulShutdown) {\n this.setupGracefulShutdown();\n }\n }\n\n /**\n * Create a new KafkaManager instance with multiple named producers\n * Producers are initialized lazily on first use\n * Producer names are type-safe based on the configuration\n */\n static create<T extends Record<string, ProducerConfig>>(\n options: Omit<KafkaManagerOptions, 'producers'> & { producers: T; },\n ): KafkaManager<keyof T & string> {\n return new KafkaManager(options);\n }\n\n private setupGracefulShutdown(): void {\n this.logger.info(`Kafka: [graceful-shutdown] adding graceful shutdown for process.pid ${process.pid}`);\n\n process.on('SIGTERM', async () => {\n await this.gracefulShutdown('SIGTERM');\n });\n\n process.on('SIGINT', async () => {\n await this.gracefulShutdown('SIGINT');\n });\n }\n\n public async gracefulShutdown(signal: string): Promise<void> {\n if (this.gracefulShutdownStarted) {\n return;\n }\n\n this.gracefulShutdownStarted = true;\n\n this.logger.info(`Kafka: [graceful-shutdown] received ${signal}! Disconnecting all producers...`);\n\n try {\n await this.disconnect();\n this.logger.info('Kafka: [graceful-shutdown] all producers disconnected successfully');\n } catch (error) {\n this.logger.error('Kafka: [graceful-shutdown] error during shutdown', { error });\n throw error;\n }\n }\n\n /**\n * Check if Kafka is enabled\n */\n get isEnabled(): boolean {\n return this.enabled;\n }\n\n /**\n * Get all producer names\n */\n get producerNames(): TProducers[] {\n return Array.from(this.producers.keys()) as TProducers[];\n }\n\n /**\n * Check if a producer exists\n */\n hasProducer(name: TProducers): boolean {\n return this.producers.has(name);\n }\n\n /**\n * Get a specific producer\n */\n private getProducer(name: TProducers): IProducer {\n const producer = this.producers.get(name);\n if (!producer) {\n throw new KafkaError(`Producer '${name}' not found. Available producers: ${this.producerNames.join(', ')}`);\n }\n return producer;\n }\n\n /**\n * Explicitly bootstrap all (or specific) producers\n * Connects to brokers, validates connectivity, and returns detailed results\n *\n * @example\n * // Bootstrap all producers with defaults (30s timeout, strict mode)\n * await kafka.bootstrap();\n *\n * @example\n * // Bootstrap with custom timeout and non-strict mode\n * const result = await kafka.bootstrap({ timeoutMs: 10000, strict: false });\n * if (!result.success) {\n * console.error('Some producers failed:', result.results);\n * }\n *\n * @example\n * // Bootstrap specific producers only\n * await kafka.bootstrap({ producers: ['main'] });\n */\n async bootstrap(options?: BootstrapOptions): Promise<BootstrapResult> {\n const startTime = Date.now();\n const timeoutMs = options?.timeoutMs ?? this.bootstrapTimeoutMs;\n const strict = options?.strict ?? this.strictBootstrap;\n const producersToBootstrap = options?.producers ?? this.producerNames;\n\n this.logger.info('Kafka: Starting bootstrap', {\n producers: producersToBootstrap,\n timeout: timeoutMs,\n strict,\n });\n\n const results: BootstrapResult['results'] = {};\n const errors: string[] = [];\n\n // Bootstrap each producer\n for (const name of producersToBootstrap) {\n const producer = this.producers.get(name);\n if (!producer) {\n results[name] = {\n success: false,\n duration: 0,\n error: `Producer '${name}' not found`,\n };\n errors.push(`Producer '${name}' not found`);\n continue;\n }\n\n const producerStartTime = Date.now();\n try {\n await producer.ping({ timeout: timeoutMs, force: true });\n const health = producer.getHealth();\n\n results[name] = {\n success: true,\n duration: Date.now() - producerStartTime,\n clusterId: health.clusterId ?? undefined,\n brokerCount: health.brokerCount,\n };\n\n this.logger.info(`Kafka: [${name}] Bootstrap successful`, {\n duration: Date.now() - producerStartTime,\n clusterId: health.clusterId,\n brokerCount: health.brokerCount,\n });\n } catch (error) {\n const err = error as Error;\n results[name] = {\n success: false,\n duration: Date.now() - producerStartTime,\n error: err.message,\n };\n errors.push(`[${name}] ${err.message}`);\n\n this.logger.error(`Kafka: [${name}] Bootstrap failed`, {\n duration: Date.now() - producerStartTime,\n error: err.message,\n });\n }\n }\n\n const totalDuration = Date.now() - startTime;\n const successCount = Object.values(results).filter(r => r.success).length;\n const totalCount = Object.keys(results).length;\n const success = strict ? successCount === totalCount : successCount > 0;\n\n const result: BootstrapResult = {\n success,\n duration: totalDuration,\n results,\n };\n\n if (success) {\n this.logger.info('Kafka: Bootstrap completed successfully', {\n duration: totalDuration,\n successCount,\n totalCount,\n });\n } else {\n const errorMessage = `Kafka bootstrap failed (${successCount}/${totalCount} producers succeeded)\\n\\n`\n + `Failures:\\n${errors.map(e => ` - ${e}`).join('\\n')}\\n\\n`\n + `Configuration:\\n`\n + ` Strict mode: ${strict}\\n`\n + ` Timeout: ${timeoutMs}ms\\n`\n + ` Producers attempted: ${producersToBootstrap.join(', ')}`;\n\n this.logger.error('Kafka: Bootstrap failed', {\n duration: totalDuration,\n successCount,\n totalCount,\n errors,\n });\n\n throw new KafkaError(errorMessage);\n }\n\n return result;\n }\n\n /**\n * Get detailed health snapshot for all producers\n * Returns comprehensive metadata including timestamps, errors, cluster info\n *\n * @example\n * const health = kafka.getHealth();\n * console.log(health.main.lastPingSucceededAt); // timestamp\n * console.log(health.main.clusterId); // 'prod-kafka-01'\n */\n getHealth(): Record<TProducers, ProducerHealth> {\n const health: Record<string, ProducerHealth> = {};\n for (const [name, producer] of this.producers.entries()) {\n health[name] = producer.getHealth();\n }\n return health as Record<TProducers, ProducerHealth>;\n }\n\n /**\n * Get detailed health for a specific producer\n */\n getProducerHealth(name: TProducers): ProducerHealth {\n const producer = this.getProducer(name);\n return producer.getHealth();\n }\n\n /**\n * Lightweight liveness check - does NOT perform Kafka operations\n * Only checks internal state for fatal errors\n * Suitable for Kubernetes liveness probes\n *\n * Returns false if:\n * - Manager is in a fatal state\n * - Graceful shutdown has started\n *\n * @example\n * app.get('/health/live', (req, res) => {\n * res.status(kafka.isLive() ? 200 : 503).json({ live: kafka.isLive() });\n * });\n */\n isLive(): boolean {\n return !this.fatalError && !this.gracefulShutdownStarted;\n }\n\n /**\n * Ping all producers to verify connectivity\n */\n async ping(): Promise<void> {\n const promises = Array.from(this.producers.values(), p => p.ping());\n await Promise.all(promises);\n }\n\n /**\n * Ping a specific producer\n */\n async pingProducer(name: TProducers): Promise<void> {\n await this.getProducer(name).ping();\n }\n\n /**\n * Check if Kafka is ready to handle traffic\n * Revalidates connectivity if cache is stale\n *\n * @param options.timeout - Override default health check timeout\n * @param options.force - Force revalidation, ignore cache\n *\n * @example\n * // Use cached result if fresh (within healthCheckCacheMs)\n * const ready = await kafka.isReady();\n *\n * @example\n * // Force immediate revalidation\n * const ready = await kafka.isReady({ force: true });\n */\n async isReady(options?: ReadinessOptions): Promise<boolean> {\n if (!this.enabled) {\n return true;\n }\n\n const now = Date.now();\n const force = options?.force ?? false;\n\n // Use cached result if available and fresh\n if (!force && this.lastReadinessCheck) {\n const age = now - this.lastReadinessCheck.timestamp;\n if (age < this.healthCheckCacheMs) {\n this.logger.debug('Kafka: Using cached readiness result', {\n result: this.lastReadinessCheck.result,\n age,\n });\n return this.lastReadinessCheck.result;\n }\n }\n\n // Revalidate connectivity\n try {\n const timeout = options?.timeout ?? this.healthCheckTimeoutMs;\n const promises = Array.from(this.producers.values()).map(p =>\n p.ping({ timeout, force: true }),\n );\n await Promise.all(promises);\n\n this.lastReadinessCheck = { result: true, timestamp: now };\n return true;\n } catch (error) {\n this.lastReadinessCheck = { result: false, timestamp: now };\n this.logger.warn('Kafka: Readiness check failed', {\n error: (error as Error).message,\n });\n return false;\n }\n }\n\n /**\n * Check connection status of all producers\n * Returns detailed status including last success time and errors\n */\n getConnectionStatus(): Record<TProducers, ConnectionStatus> {\n const status: Record<string, ConnectionStatus> = {};\n for (const [name, producer] of this.producers.entries()) {\n const health = producer.getHealth();\n status[name] = {\n connected: health.isConnected,\n lastSuccessAt: health.lastPingSucceededAt,\n lastError: health.lastError?.message ?? null,\n };\n }\n return status as Record<TProducers, ConnectionStatus>;\n }\n\n /**\n * Publish a message using a named producer\n */\n async publish<T = unknown>(\n producerName: TProducers,\n topic: string,\n value: T,\n options?: PublishOptions,\n ): Promise<RecordMetadata[]> {\n return this.getProducer(producerName).publish(topic, value, options);\n }\n\n /**\n * Publish a batch of messages using a named producer\n */\n async publishBatch(\n producerName: TProducers,\n options: PublishBatchOptions,\n ): Promise<RecordMetadata[]> {\n return this.getProducer(producerName).publishBatch(options);\n }\n\n /**\n * Disconnect all producers\n */\n async disconnect(): Promise<void> {\n const promises = Array.from(this.producers.values()).map(p => p.disconnect());\n await Promise.all(promises);\n this.logger.info('Kafka: All producers disconnected');\n }\n\n /**\n * Disconnect a specific producer\n */\n async disconnectProducer(name: TProducers): Promise<void> {\n await this.getProducer(name).disconnect();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["name: string","config: ProducerConfig","logger: LoggerInstanceManager","#loadKafkaAndSetup","headers: Record<string, string>","name: string","brokers: string[]","logger: LoggerInstanceManager","results: BootstrapResult['results']","errors: string[]","result: BootstrapResult","health: Record<string, ProducerHealth>","status: Record<string, ConnectionStatus>"],"sources":["../src/kafkaError.ts","../src/consts.ts","../src/producer.ts","../src/mockProducer.ts","../src/manager.ts"],"sourcesContent":["export default class KafkaError extends Error {\n name = 'KafkaError';\n}\n","export const TIMESTAMP_HEADER = 'x-timestamp';\n\nexport const DEFAULT_CLIENT_ID = 'autofleet-kafka-producer';\n","import { setTimeout } from 'node:timers/promises';\nimport type { LoggerInstanceManager } from '@autofleet/logger';\nimport type { Producer } from '@platformatic/kafka/dist/clients/producer/index.ts';\nimport KafkaError from './kafkaError';\nimport {\n DEFAULT_CLIENT_ID,\n TIMESTAMP_HEADER,\n} from './consts';\nimport type {\n ProducerConfig,\n PublishOptions,\n PublishBatchOptions,\n RecordMetadata,\n ProducerHealth,\n IProducer,\n} from './types';\n\n/**\n * Wrapper for a single Kafka producer instance with lazy initialization\n * Tracks health metadata including connection state, timestamps, and errors\n */\nexport class ProducerWrapper implements IProducer {\n private producer: Producer<string, string, string, string> | null = null;\n private _isConnected = false;\n private initPromise: Promise<void> | null = null;\n\n // Health tracking metadata\n private _lastPingAt: number | null = null;\n private _lastPingSucceededAt: number | null = null;\n private _lastError: { message: string; timestamp: number; stack?: string; } | null = null;\n private _clusterId: string | null = null;\n private _brokerCount = 0;\n\n constructor(\n private readonly name: string,\n private readonly config: ProducerConfig,\n private readonly logger: LoggerInstanceManager,\n ) {}\n\n async #loadKafkaAndSetup(): Promise<void> {\n const { Producer, stringSerializers } = await import('@platformatic/kafka');\n\n this.producer = new Producer({\n bootstrapBrokers: this.config.brokers,\n clientId: this.config.clientId || `${DEFAULT_CLIENT_ID}-${this.name}`,\n serializers: stringSerializers,\n autocreateTopics: this.config.autoCreateTopics ?? false,\n sasl: this.config.sasl,\n tls: this.config.tls,\n });\n\n this.logger.info(`Kafka: [${this.name}] Initialized producer`, {\n clientId: this.config.clientId || `${DEFAULT_CLIENT_ID}-${this.name}`,\n brokers: this.config.brokers,\n });\n\n // Clear promise to allow garbage collection\n this.initPromise = null;\n }\n\n private async initialize(): Promise<void> {\n if (this.producer) {\n return;\n }\n\n this.initPromise ??= this.#loadKafkaAndSetup();\n await this.initPromise;\n }\n\n get isConnected(): boolean {\n return this._isConnected;\n }\n\n /**\n * Get comprehensive health snapshot for this producer\n */\n getHealth(): ProducerHealth {\n return {\n name: this.name,\n enabled: true,\n isConnected: this._isConnected,\n lastPingAt: this._lastPingAt,\n lastPingSucceededAt: this._lastPingSucceededAt,\n lastError: this._lastError,\n clusterId: this._clusterId,\n brokerCount: this._brokerCount,\n brokers: this.config.brokers,\n };\n }\n\n /**\n * Build an actionable error message with troubleshooting guidance\n */\n private buildConnectionErrorMessage(error: Error): string {\n const brokerList = this.config.brokers.join(', ');\n const firstBroker = this.config.brokers[0];\n const [host, port] = firstBroker ? firstBroker.split(':') : ['', ''];\n\n return `[${this.name}] Failed to connect to Kafka brokers: ${error.message}\\n\\n`\n + `Possible causes:\\n`\n + ` 1. Brokers are unreachable: ${brokerList}\\n`\n + ` 2. Network connectivity issues\\n`\n + ` 3. Firewall blocking ports\\n`\n + ` 4. ${this.config.sasl ? 'SASL authentication failed' : 'Authentication not configured but required'}\\n\\n`\n + `Troubleshooting:\\n`\n + ` - Verify brokers are running and accessible\\n`\n + (host && port ? ` - Test connectivity: nc -zv ${host} ${port}\\n` : '')\n + ` - Check network policies and firewall rules\\n`\n + ` ${this.config.sasl ? '- Verify SASL credentials are correct\\n' : ''}\\n`\n + `Current configuration:\\n`\n + ` Brokers: ${brokerList}\\n`\n + ` Client ID: ${this.config.clientId || `${DEFAULT_CLIENT_ID}-${this.name}`}\\n`\n + ` SASL: ${this.config.sasl ? `enabled (${this.config.sasl.mechanism})` : 'disabled'}\\n`\n + ` SASL Password: ${this.config.sasl ? (this.config.sasl.password ? '****' : 'not set') : 'N/A'}\\n`\n + ` Auto-create topics: ${this.config.autoCreateTopics ?? false}`;\n }\n\n /**\n * Ping the Kafka cluster to verify connectivity\n * @param options.timeout - Maximum time to wait for connection (ms)\n * @param options.force - Force reconnection even if already connected\n */\n async ping(options?: { timeout?: number; force?: boolean; }): Promise<void> {\n this._lastPingAt = Date.now();\n\n // Skip if already connected and not forcing revalidation\n if (this._isConnected && !options?.force) {\n return;\n }\n\n await this.initialize();\n\n try {\n // Create error before promise to preserve stack trace\n const timeoutMs = options?.timeout ?? 5000;\n const timeoutError = new Error(`Connection timeout after ${timeoutMs}ms`);\n const timeoutPromise = setTimeout(timeoutMs).then(() => {\n throw timeoutError;\n });\n\n // Race between metadata fetch and timeout\n const metadata = await Promise.race([\n this.producer!.metadata({ topics: [] }),\n timeoutPromise,\n ]);\n\n this._isConnected = true;\n this._lastPingSucceededAt = Date.now();\n this._clusterId = metadata.id;\n this._brokerCount = metadata.brokers.size;\n this._lastError = null; // Clear any previous errors\n\n this.logger.info(`Kafka: [${this.name}] Successfully connected to cluster`, {\n clusterId: metadata.id,\n brokers: metadata.brokers.size,\n duration: Date.now() - this._lastPingAt,\n });\n } catch (error) {\n const err = error as Error;\n this._isConnected = false;\n this._lastError = {\n message: err.message,\n timestamp: Date.now(),\n stack: err.stack,\n };\n\n this.logger.error(`Kafka: [${this.name}] Failed to connect to brokers`, {\n error: err.message,\n duration: Date.now() - this._lastPingAt,\n });\n\n throw new KafkaError(this.buildConnectionErrorMessage(err), { cause: err });\n }\n }\n\n async publish<T = unknown>(\n topic: string,\n value: T,\n options?: PublishOptions,\n ): Promise<RecordMetadata[]> {\n if (!topic) {\n throw new KafkaError(`[${this.name}] Topic name is required`);\n }\n\n await this.ping();\n\n try {\n const headers: Record<string, string> = {\n [TIMESTAMP_HEADER]: Date.now().toString(),\n ...options?.headers,\n };\n\n const result = await this.producer!.send({\n messages: [{\n topic,\n value: JSON.stringify(value),\n key: options?.key,\n partition: options?.partition,\n headers,\n }],\n });\n\n this.logger.debug(`Kafka: [${this.name}] Published message to topic ${topic}`, {\n topic,\n });\n\n return [{\n topic,\n partition: options?.partition ?? 0,\n offset: result?.offsets?.[0]?.offset?.toString() ?? '0',\n }];\n } catch (error) {\n const err = error as { message?: string; };\n this.logger.error(`Kafka: [${this.name}] Error publishing to topic ${topic}`, { error, value });\n throw new KafkaError(`[${this.name}] Failed to publish to topic ${topic}: ${err.message || 'Unknown error'}`, { cause: error });\n }\n }\n\n async publishBatch<T = unknown>(topic: string, options: PublishBatchOptions<T>): Promise<RecordMetadata[]> {\n if (!topic) {\n throw new KafkaError(`[${this.name}] Topic name is required`);\n }\n\n if (!options.messages || options.messages.length === 0) {\n throw new KafkaError(`[${this.name}] At least one message is required`);\n }\n\n await this.ping();\n\n try {\n const messages = options.messages.map(msg => ({\n topic,\n value: JSON.stringify(msg.value),\n key: msg.key,\n partition: msg.partition,\n headers: {\n [TIMESTAMP_HEADER]: Date.now().toString(),\n ...msg.headers,\n },\n }));\n\n const result = await this.producer!.send({ messages });\n\n this.logger.debug(`Kafka: [${this.name}] Published ${messages.length} messages to topic ${topic}`, {\n topic,\n count: messages.length,\n });\n\n return messages.map((msg, idx) => ({\n topic,\n partition: msg.partition ?? 0,\n offset: result?.offsets?.[idx]?.offset?.toString() ?? idx.toString(),\n }));\n } catch (error) {\n const err = error as { message?: string; };\n this.logger.error(`Kafka: [${this.name}] Error publishing batch to topic ${topic}`, { error });\n throw new KafkaError(`[${this.name}] Failed to publish batch to topic ${topic}: ${err.message || 'Unknown error'}`, { cause: error });\n }\n }\n\n async disconnect(): Promise<void> {\n if (!this.producer || !this._isConnected) {\n this.logger.debug(`Kafka: [${this.name}] Producer already disconnected`);\n return;\n }\n\n this.logger.info(`Kafka: [${this.name}] Disconnecting producer`);\n\n try {\n await this.producer.close();\n this._isConnected = false;\n this.logger.info(`Kafka: [${this.name}] Producer disconnected successfully`);\n } catch (error) {\n this.logger.error(`Kafka: [${this.name}] Error disconnecting producer`, { error });\n throw new KafkaError(`[${this.name}] Failed to disconnect producer: ${(error as Error).message}`, { cause: error });\n }\n }\n}\n","import type { LoggerInstanceManager } from '@autofleet/logger';\nimport type {\n PublishOptions,\n PublishBatchOptions,\n RecordMetadata,\n ProducerHealth,\n IProducer,\n} from './types';\n\n/**\n * Mock producer for when Kafka is disabled\n */\nexport class MockProducer implements IProducer {\n constructor(private readonly name: string, private readonly brokers: string[], private readonly logger: LoggerInstanceManager) {}\n\n readonly isConnected = true;\n\n /**\n * Get health snapshot for mock producer\n */\n getHealth(): ProducerHealth {\n return {\n name: this.name,\n enabled: false, // Mock mode means Kafka is disabled\n isConnected: true, // Mock is always \"connected\"\n lastPingAt: null,\n lastPingSucceededAt: null,\n lastError: null,\n clusterId: 'mock-cluster',\n brokerCount: 0,\n brokers: this.brokers,\n };\n }\n\n async ping(): Promise<void> {\n this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping ping`);\n return Promise.resolve();\n }\n\n async publish<T = unknown>(topic: string, value: T, _options?: PublishOptions): Promise<RecordMetadata[]> {\n this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping publish to topic: ${topic}`, { value });\n return Promise.resolve([{\n topic,\n partition: 0,\n offset: '0',\n }]);\n }\n\n async publishBatch<T = unknown>(topic: string, options: PublishBatchOptions<T>): Promise<RecordMetadata[]> {\n this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping batch publish to topic: ${topic}`, {\n messageCount: options.messages.length,\n });\n return Promise.resolve(options.messages.map(() => ({\n topic,\n partition: 0,\n offset: '0',\n })));\n }\n\n async disconnect(): Promise<void> {\n this.logger.debug(`Kafka: [${this.name}] Mock mode - skipping disconnect`);\n return Promise.resolve();\n }\n}\n","import type { LoggerInstanceManager } from '@autofleet/logger';\nimport KafkaError from './kafkaError';\nimport { ProducerWrapper } from './producer';\nimport { MockProducer } from './mockProducer';\nimport type {\n KafkaManagerOptions,\n PublishOptions,\n PublishBatchOptions,\n RecordMetadata,\n ProducerHealth,\n BootstrapOptions,\n BootstrapResult,\n ReadinessOptions,\n ConnectionStatus,\n IProducer,\n ProducerConfig,\n TopicsConfig,\n TopicDefinition,\n TopicPayload,\n} from './types';\n\n/**\n * Manager for multiple Kafka producers\n * Provides lifecycle management, health tracking, and operational guarantees\n *\n * @template TProducers - Union of producer names\n * @template TTopics - Typed topics configuration with Zod schemas\n */\nexport class KafkaManager<TProducers extends string = string, TTopics extends TopicsConfig = {}> {\n private readonly producers = new Map<string, IProducer>();\n private readonly topics = new Map<string, TopicDefinition>();\n private readonly logger: LoggerInstanceManager;\n private readonly enabled: boolean;\n private gracefulShutdownStarted = false;\n private fatalError: Error | null = null;\n\n // Configuration options\n private readonly healthCheckTimeoutMs: number;\n private readonly healthCheckCacheMs: number;\n private readonly bootstrapTimeoutMs: number;\n private readonly strictBootstrap: boolean;\n\n // Health check cache\n private lastReadinessCheck: { result: boolean; timestamp: number; } | null = null;\n\n private constructor(options: KafkaManagerOptions) {\n this.logger = options.logger;\n this.enabled = options.enabled ?? true;\n this.healthCheckTimeoutMs = options.healthCheckTimeoutMs ?? 5000;\n this.healthCheckCacheMs = options.healthCheckCacheMs ?? 1000;\n this.bootstrapTimeoutMs = options.bootstrapTimeoutMs ?? 30_000;\n this.strictBootstrap = options.strictBootstrap ?? true;\n\n // Store topic definitions\n if (options.topics) {\n for (const [topicName, definition] of Object.entries(options.topics)) {\n this.topics.set(topicName, definition);\n }\n }\n\n this.logger.info('Kafka: Initialized KafkaManager', {\n enabled: this.enabled,\n producerCount: Object.keys(options.producers ?? {}).length,\n producers: Object.keys(options.producers ?? {}),\n topicCount: this.topics.size,\n topics: Array.from(this.topics.keys()),\n });\n\n // Create producers\n if (!this.enabled) {\n // Create mock producers\n for (const [name, config] of Object.entries(options.producers ?? {})) {\n this.producers.set(name, new MockProducer(name, config.brokers, this.logger));\n }\n if (this.producers.size > 0) {\n this.logger.info('Kafka: Created mock producers (Kafka disabled)');\n }\n } else {\n // Create real producers (they will initialize lazily)\n for (const [name, config] of Object.entries(options.producers ?? {})) {\n if (!config.brokers || config.brokers.length === 0) {\n throw new KafkaError(`[${name}] At least one broker is required`);\n }\n\n this.producers.set(name, new ProducerWrapper(name, config, this.logger));\n }\n }\n\n if (!options.dontGracefulShutdown) {\n this.setupGracefulShutdown();\n }\n }\n\n /**\n * Create a new KafkaManager instance with multiple named producers\n * Producers are initialized lazily on first use\n * Producer names are type-safe based on the configuration\n * Topics configuration provides type-safe publishing with Zod validation\n */\n static create<\n P extends Record<string, ProducerConfig>,\n T extends TopicsConfig = {},\n >(\n options: Omit<KafkaManagerOptions, 'producers' | 'topics'> & {\n producers?: P;\n topics?: T;\n },\n ): KafkaManager<keyof P & string, T> {\n // Validate that topic producers exist\n if (options.topics) {\n const producerNames = Object.keys(options.producers ?? {});\n for (const [topicName, topicDef] of Object.entries(options.topics)) {\n if (!producerNames.includes(topicDef.producer)) {\n throw new KafkaError(\n `Topic '${topicName}' references unknown producer '${topicDef.producer}'. `\n + `Available producers: ${producerNames.join(', ')}`,\n );\n }\n }\n }\n\n return new KafkaManager(options);\n }\n\n private setupGracefulShutdown(): void {\n this.logger.info(`Kafka: [graceful-shutdown] adding graceful shutdown for process.pid ${process.pid}`);\n\n process.on('SIGTERM', async () => {\n await this.gracefulShutdown('SIGTERM');\n });\n\n process.on('SIGINT', async () => {\n await this.gracefulShutdown('SIGINT');\n });\n }\n\n public async gracefulShutdown(signal: string): Promise<void> {\n if (this.gracefulShutdownStarted) {\n return;\n }\n\n this.gracefulShutdownStarted = true;\n\n this.logger.info(`Kafka: [graceful-shutdown] received ${signal}! Disconnecting all producers...`);\n\n try {\n await this.disconnect();\n this.logger.info('Kafka: [graceful-shutdown] all producers disconnected successfully');\n } catch (error) {\n this.logger.error('Kafka: [graceful-shutdown] error during shutdown', { error });\n throw error;\n }\n }\n\n /**\n * Check if Kafka is enabled\n */\n get isEnabled(): boolean {\n return this.enabled;\n }\n\n /**\n * Get all producer names\n */\n get producerNames(): TProducers[] {\n return Array.from(this.producers.keys()) as TProducers[];\n }\n\n /**\n * Check if a producer exists\n */\n hasProducer(name: TProducers): boolean {\n return this.producers.has(name);\n }\n\n /**\n * Get a specific producer\n */\n private getProducer(name: TProducers): IProducer {\n const producer = this.producers.get(name);\n if (!producer) {\n throw new KafkaError(`Producer '${name}' not found. Available producers: ${this.producerNames.join(', ')}`);\n }\n return producer;\n }\n\n /**\n * Explicitly bootstrap all (or specific) producers\n * Connects to brokers, validates connectivity, and returns detailed results\n *\n * @example\n * // Bootstrap all producers with defaults (30s timeout, strict mode)\n * await kafka.bootstrap();\n *\n * @example\n * // Bootstrap with custom timeout and non-strict mode\n * const result = await kafka.bootstrap({ timeoutMs: 10000, strict: false });\n * if (!result.success) {\n * console.error('Some producers failed:', result.results);\n * }\n *\n * @example\n * // Bootstrap specific producers only\n * await kafka.bootstrap({ producers: ['main'] });\n */\n async bootstrap(options?: BootstrapOptions): Promise<BootstrapResult> {\n const startTime = Date.now();\n const timeoutMs = options?.timeoutMs ?? this.bootstrapTimeoutMs;\n const strict = options?.strict ?? this.strictBootstrap;\n const producersToBootstrap = options?.producers ?? this.producerNames;\n\n this.logger.info('Kafka: Starting bootstrap', {\n producers: producersToBootstrap,\n timeout: timeoutMs,\n strict,\n });\n\n const results: BootstrapResult['results'] = {};\n const errors: string[] = [];\n\n // Bootstrap each producer\n for (const name of producersToBootstrap) {\n const producer = this.producers.get(name);\n if (!producer) {\n results[name] = {\n success: false,\n duration: 0,\n error: `Producer '${name}' not found`,\n };\n errors.push(`Producer '${name}' not found`);\n continue;\n }\n\n const producerStartTime = Date.now();\n try {\n await producer.ping({ timeout: timeoutMs, force: true });\n const health = producer.getHealth();\n\n results[name] = {\n success: true,\n duration: Date.now() - producerStartTime,\n clusterId: health.clusterId ?? undefined,\n brokerCount: health.brokerCount,\n };\n\n this.logger.info(`Kafka: [${name}] Bootstrap successful`, {\n duration: Date.now() - producerStartTime,\n clusterId: health.clusterId,\n brokerCount: health.brokerCount,\n });\n } catch (error) {\n const err = error as Error;\n results[name] = {\n success: false,\n duration: Date.now() - producerStartTime,\n error: err.message,\n };\n errors.push(`[${name}] ${err.message}`);\n\n this.logger.error(`Kafka: [${name}] Bootstrap failed`, {\n duration: Date.now() - producerStartTime,\n error: err.message,\n });\n }\n }\n\n const totalDuration = Date.now() - startTime;\n const successCount = Object.values(results).filter(r => r.success).length;\n const totalCount = Object.keys(results).length;\n const success = strict ? successCount === totalCount : successCount > 0;\n\n const result: BootstrapResult = {\n success,\n duration: totalDuration,\n results,\n };\n\n if (success) {\n this.logger.info('Kafka: Bootstrap completed successfully', {\n duration: totalDuration,\n successCount,\n totalCount,\n });\n } else {\n const errorMessage = `Kafka bootstrap failed (${successCount}/${totalCount} producers succeeded)\\n\\n`\n + `Failures:\\n${errors.map(e => ` - ${e}`).join('\\n')}\\n\\n`\n + `Configuration:\\n`\n + ` Strict mode: ${strict}\\n`\n + ` Timeout: ${timeoutMs}ms\\n`\n + ` Producers attempted: ${producersToBootstrap.join(', ')}`;\n\n this.logger.error('Kafka: Bootstrap failed', {\n duration: totalDuration,\n successCount,\n totalCount,\n errors,\n });\n\n throw new KafkaError(errorMessage);\n }\n\n return result;\n }\n\n /**\n * Get detailed health snapshot for all producers\n * Returns comprehensive metadata including timestamps, errors, cluster info\n *\n * @example\n * const health = kafka.getHealth();\n * console.log(health.main.lastPingSucceededAt); // timestamp\n * console.log(health.main.clusterId); // 'prod-kafka-01'\n */\n getHealth(): Record<TProducers, ProducerHealth> {\n const health: Record<string, ProducerHealth> = {};\n for (const [name, producer] of this.producers.entries()) {\n health[name] = producer.getHealth();\n }\n return health as Record<TProducers, ProducerHealth>;\n }\n\n /**\n * Get detailed health for a specific producer\n */\n getProducerHealth(name: TProducers): ProducerHealth {\n const producer = this.getProducer(name);\n return producer.getHealth();\n }\n\n /**\n * Lightweight liveness check - does NOT perform Kafka operations\n * Only checks internal state for fatal errors\n * Suitable for Kubernetes liveness probes\n *\n * Returns false if:\n * - Manager is in a fatal state\n * - Graceful shutdown has started\n *\n * @example\n * app.get('/health/live', (req, res) => {\n * res.status(kafka.isLive() ? 200 : 503).json({ live: kafka.isLive() });\n * });\n */\n isLive(): boolean {\n return !this.fatalError && !this.gracefulShutdownStarted;\n }\n\n /**\n * Ping all producers to verify connectivity\n */\n async ping(): Promise<void> {\n const promises = Array.from(this.producers.values(), p => p.ping());\n await Promise.all(promises);\n }\n\n /**\n * Ping a specific producer\n */\n async pingProducer(name: TProducers): Promise<void> {\n await this.getProducer(name).ping();\n }\n\n /**\n * Check if Kafka is ready to handle traffic\n * Revalidates connectivity if cache is stale\n *\n * @param options.timeout - Override default health check timeout\n * @param options.force - Force revalidation, ignore cache\n *\n * @example\n * // Use cached result if fresh (within healthCheckCacheMs)\n * const ready = await kafka.isReady();\n *\n * @example\n * // Force immediate revalidation\n * const ready = await kafka.isReady({ force: true });\n */\n async isReady(options?: ReadinessOptions): Promise<boolean> {\n if (!this.enabled) {\n return true;\n }\n\n const now = Date.now();\n const force = options?.force ?? false;\n\n // Use cached result if available and fresh\n if (!force && this.lastReadinessCheck) {\n const age = now - this.lastReadinessCheck.timestamp;\n if (age < this.healthCheckCacheMs) {\n this.logger.debug('Kafka: Using cached readiness result', {\n result: this.lastReadinessCheck.result,\n age,\n });\n return this.lastReadinessCheck.result;\n }\n }\n\n // Revalidate connectivity\n try {\n const timeout = options?.timeout ?? this.healthCheckTimeoutMs;\n const promises = Array.from(this.producers.values()).map(p =>\n p.ping({ timeout, force: true }),\n );\n await Promise.all(promises);\n\n this.lastReadinessCheck = { result: true, timestamp: now };\n return true;\n } catch (error) {\n this.lastReadinessCheck = { result: false, timestamp: now };\n this.logger.warn('Kafka: Readiness check failed', {\n error: (error as Error).message,\n });\n return false;\n }\n }\n\n /**\n * Check connection status of all producers\n * Returns detailed status including last success time and errors\n */\n getConnectionStatus(): Record<TProducers, ConnectionStatus> {\n const status: Record<string, ConnectionStatus> = {};\n for (const [name, producer] of this.producers.entries()) {\n const health = producer.getHealth();\n status[name] = {\n connected: health.isConnected,\n lastSuccessAt: health.lastPingSucceededAt,\n lastError: health.lastError?.message ?? null,\n };\n }\n return status as Record<TProducers, ConnectionStatus>;\n }\n\n /**\n * Get a topic definition\n */\n private getTopicDefinition<TName extends keyof TTopics & string>(topicName: TName): TopicDefinition {\n const topic = this.topics.get(topicName);\n if (!topic) {\n throw new KafkaError(\n `Topic '${topicName}' not found. Available topics: ${Array.from(this.topics.keys()).join(', ')}`,\n );\n }\n return topic;\n }\n\n /**\n * Publish a message to a typed topic\n * Automatically validates payload against Zod schema and routes to correct producer\n *\n * @template TName - Topic name (inferred from topics config)\n * @param topicName - Name of the topic to publish to\n * @param value - Payload (type-checked against topic's Zod schema)\n * @param options - Optional publish options including headers, key, partition\n *\n * @example\n * await kafka.publish('order.created', {\n * orderId: '123',\n * amount: 99.99,\n * customerId: '456',\n * });\n */\n async publish<TName extends keyof TTopics & string>(\n topicName: TName,\n value: TopicPayload<TTopics, TName>,\n options?: PublishOptions,\n ): Promise<RecordMetadata[]> {\n const topicDef = this.getTopicDefinition(topicName);\n\n // Validate payload against schema unless skipped\n if (!options?.skipValidation) {\n try {\n topicDef.schema.parse(value);\n } catch (error) {\n this.logger.error(`Kafka: Validation failed for topic '${topicName}'`, { error, value });\n throw new KafkaError(\n `Validation failed for topic '${topicName}': ${(error as Error).message}`,\n { cause: error },\n );\n }\n }\n\n // Get producer and publish\n const producer = this.getProducer(topicDef.producer as TProducers);\n return producer.publish(topicName, value, options);\n }\n\n /**\n * Publish a batch of messages to a typed topic\n * Automatically validates payloads against Zod schema and routes to correct producer\n *\n * @template TName - Topic name (inferred from topics config)\n * @param topicName - Name of the topic to publish to\n * @param options - Batch options with array of messages\n *\n * @example\n * await kafka.publishBatch('order.created', {\n * messages: [\n * { value: { orderId: '1', amount: 10, customerId: 'a' } },\n * { value: { orderId: '2', amount: 20, customerId: 'b' } },\n * ],\n * });\n */\n async publishBatch<TName extends keyof TTopics & string>(\n topicName: TName,\n options: PublishBatchOptions<TopicPayload<TTopics, TName>>,\n ): Promise<RecordMetadata[]> {\n const topicDef = this.getTopicDefinition(topicName);\n\n // Validate all payloads against schema unless skipped\n if (!options.skipValidation) {\n for (let i = 0; i < options.messages.length; i++) {\n try {\n topicDef.schema.parse(options.messages[i].value);\n } catch (error) {\n this.logger.error(`Kafka: Validation failed for topic '${topicName}' message ${i}`, {\n error,\n value: options.messages[i].value,\n });\n throw new KafkaError(\n `Validation failed for topic '${topicName}' message ${i}: ${(error as Error).message}`,\n { cause: error },\n );\n }\n }\n }\n\n // Get producer and publish batch\n const producer = this.getProducer(topicDef.producer as TProducers);\n return producer.publishBatch(topicName, options);\n }\n\n /**\n * Disconnect all producers\n */\n async disconnect(): Promise<void> {\n const promises = Array.from(this.producers.values()).map(p => p.disconnect());\n await Promise.all(promises);\n this.logger.info('Kafka: All producers disconnected');\n }\n\n /**\n * Disconnect a specific producer\n */\n async disconnectProducer(name: TProducers): Promise<void> {\n await this.getProducer(name).disconnect();\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autofleet/kafka",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Internal wrapper for Apache Kafka producer",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -35,11 +35,13 @@
35
35
  "@platformatic/kafka": "^1.21.0"
36
36
  },
37
37
  "peerDependencies": {
38
- "@autofleet/logger": "*"
38
+ "@autofleet/logger": "*",
39
+ "zod": ">=4"
39
40
  },
40
41
  "devDependencies": {
41
42
  "@types/node": "^20.14.11",
42
- "@autofleet/logger": "^4.2.41"
43
+ "zod": "^4.2.1",
44
+ "@autofleet/logger": "^4.2.42"
43
45
  },
44
46
  "keywords": [
45
47
  "kafka",