navis.js 1.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -20
- package/bin/generators/service.js +167 -0
- package/bin/navis.js +56 -2
- package/examples/v2-features-demo.js +190 -0
- package/examples/v3-features-demo.js +226 -0
- package/package.json +1 -1
- package/src/index.js +50 -12
- package/src/messaging/base-messaging.js +82 -0
- package/src/messaging/kafka-adapter.js +152 -0
- package/src/messaging/nats-adapter.js +141 -0
- package/src/messaging/sqs-adapter.js +175 -0
- package/src/observability/logger.js +141 -0
- package/src/observability/metrics.js +193 -0
- package/src/observability/tracer.js +205 -0
- package/src/utils/circuit-breaker.js +107 -0
- package/src/utils/retry.js +88 -0
- package/src/utils/service-client.js +234 -104
- package/src/utils/service-config.js +88 -0
- package/src/utils/service-discovery.js +184 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navis.js v3 Features Demo
|
|
3
|
+
* Demonstrates async messaging, observability, and enhanced features
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
Logger,
|
|
8
|
+
Metrics,
|
|
9
|
+
Tracer,
|
|
10
|
+
SQSMessaging,
|
|
11
|
+
KafkaMessaging,
|
|
12
|
+
NATSMessaging,
|
|
13
|
+
} = require('../src/index');
|
|
14
|
+
|
|
15
|
+
// Example 1: Structured Logging
|
|
16
|
+
async function demoLogger() {
|
|
17
|
+
console.log('\n=== Structured Logging (v3) ===\n');
|
|
18
|
+
|
|
19
|
+
const logger = new Logger({
|
|
20
|
+
level: 'DEBUG',
|
|
21
|
+
format: 'text',
|
|
22
|
+
enableColors: true,
|
|
23
|
+
context: { service: 'demo-service', version: '1.0.0' },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
logger.debug('Debug message', { userId: 123 });
|
|
27
|
+
logger.info('User logged in', { userId: 123, ip: '192.168.1.1' });
|
|
28
|
+
logger.warn('Rate limit approaching', { userId: 123, requests: 95 });
|
|
29
|
+
logger.error('Failed to process request', new Error('Connection timeout'), { userId: 123 });
|
|
30
|
+
|
|
31
|
+
// Child logger with additional context
|
|
32
|
+
const requestLogger = logger.child({ requestId: 'req-123' });
|
|
33
|
+
requestLogger.info('Processing request');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Example 2: Metrics Collection
|
|
37
|
+
async function demoMetrics() {
|
|
38
|
+
console.log('\n=== Metrics Collection (v3) ===\n');
|
|
39
|
+
|
|
40
|
+
const metrics = new Metrics();
|
|
41
|
+
|
|
42
|
+
// Record some metrics
|
|
43
|
+
metrics.increment('api_calls_total', 1, { endpoint: '/users', method: 'GET' });
|
|
44
|
+
metrics.increment('api_calls_total', 1, { endpoint: '/users', method: 'POST' });
|
|
45
|
+
metrics.gauge('active_connections', 42);
|
|
46
|
+
metrics.histogram('response_time_ms', 150, { endpoint: '/users' });
|
|
47
|
+
metrics.histogram('response_time_ms', 200, { endpoint: '/users' });
|
|
48
|
+
metrics.histogram('response_time_ms', 120, { endpoint: '/users' });
|
|
49
|
+
|
|
50
|
+
// Record HTTP request
|
|
51
|
+
metrics.recordRequest('GET', '/users', 150, 200);
|
|
52
|
+
metrics.recordRequest('POST', '/users', 250, 201);
|
|
53
|
+
|
|
54
|
+
// Get all metrics
|
|
55
|
+
const allMetrics = metrics.getAll();
|
|
56
|
+
console.log('All metrics:', JSON.stringify(allMetrics, null, 2));
|
|
57
|
+
|
|
58
|
+
// Prometheus format
|
|
59
|
+
console.log('\nPrometheus format:');
|
|
60
|
+
console.log(metrics.toPrometheus());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Example 3: Distributed Tracing
|
|
64
|
+
async function demoTracer() {
|
|
65
|
+
console.log('\n=== Distributed Tracing (v3) ===\n');
|
|
66
|
+
|
|
67
|
+
const tracer = new Tracer({ serviceName: 'demo-service' });
|
|
68
|
+
|
|
69
|
+
// Start a trace
|
|
70
|
+
const traceId = tracer.startTrace('user-request');
|
|
71
|
+
console.log('Trace ID:', traceId);
|
|
72
|
+
|
|
73
|
+
// Start spans
|
|
74
|
+
const span1 = tracer.startSpan('database-query', { traceId });
|
|
75
|
+
tracer.addTag(span1, 'db.type', 'postgres');
|
|
76
|
+
tracer.addTag(span1, 'db.query', 'SELECT * FROM users');
|
|
77
|
+
tracer.addLog(span1, 'Query executed', { rows: 10 });
|
|
78
|
+
|
|
79
|
+
// Simulate work
|
|
80
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
81
|
+
|
|
82
|
+
tracer.finishSpan(span1, { status: 'ok' });
|
|
83
|
+
|
|
84
|
+
const span2 = tracer.startSpan('cache-lookup', { traceId, parentSpanId: span1 });
|
|
85
|
+
tracer.addTag(span2, 'cache.type', 'redis');
|
|
86
|
+
tracer.finishSpan(span2, { status: 'ok' });
|
|
87
|
+
|
|
88
|
+
// Get trace
|
|
89
|
+
const trace = tracer.getTrace(traceId);
|
|
90
|
+
console.log('Trace data:', JSON.stringify(trace, null, 2));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Example 4: SQS Messaging (requires AWS SDK)
|
|
94
|
+
async function demoSQS() {
|
|
95
|
+
console.log('\n=== SQS Messaging (v3) ===\n');
|
|
96
|
+
console.log('Note: Requires @aws-sdk/client-sqs and AWS credentials');
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const sqs = new SQSMessaging({
|
|
100
|
+
region: 'us-east-1',
|
|
101
|
+
queueUrl: process.env.SQS_QUEUE_URL,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await sqs.connect();
|
|
105
|
+
console.log('✅ Connected to SQS');
|
|
106
|
+
|
|
107
|
+
// Publish message
|
|
108
|
+
const result = await sqs.publish(process.env.SQS_QUEUE_URL, {
|
|
109
|
+
userId: 123,
|
|
110
|
+
action: 'user.created',
|
|
111
|
+
data: { name: 'John Doe' },
|
|
112
|
+
});
|
|
113
|
+
console.log('Published message:', result);
|
|
114
|
+
|
|
115
|
+
// Subscribe (would need actual queue)
|
|
116
|
+
// await sqs.subscribe(process.env.SQS_QUEUE_URL, async (message, metadata) => {
|
|
117
|
+
// console.log('Received:', message);
|
|
118
|
+
// });
|
|
119
|
+
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error.message.includes('@aws-sdk/client-sqs')) {
|
|
122
|
+
console.log('⚠️ AWS SDK not installed. Install with: npm install @aws-sdk/client-sqs');
|
|
123
|
+
} else {
|
|
124
|
+
console.log('⚠️ SQS demo requires AWS credentials and queue URL');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Example 5: Kafka Messaging (requires kafkajs)
|
|
130
|
+
async function demoKafka() {
|
|
131
|
+
console.log('\n=== Kafka Messaging (v3) ===\n');
|
|
132
|
+
console.log('Note: Requires kafkajs and Kafka broker');
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const kafka = new KafkaMessaging({
|
|
136
|
+
brokers: ['localhost:9092'],
|
|
137
|
+
clientId: 'navis-demo',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await kafka.connect();
|
|
141
|
+
console.log('✅ Connected to Kafka');
|
|
142
|
+
|
|
143
|
+
// Publish message
|
|
144
|
+
const result = await kafka.publish('user-events', {
|
|
145
|
+
userId: 123,
|
|
146
|
+
event: 'user.created',
|
|
147
|
+
data: { name: 'Jane Doe' },
|
|
148
|
+
});
|
|
149
|
+
console.log('Published to Kafka:', result);
|
|
150
|
+
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error.message.includes('kafkajs')) {
|
|
153
|
+
console.log('⚠️ kafkajs not installed. Install with: npm install kafkajs');
|
|
154
|
+
} else {
|
|
155
|
+
console.log('⚠️ Kafka demo requires running Kafka broker');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Example 6: NATS Messaging (requires nats)
|
|
161
|
+
async function demoNATS() {
|
|
162
|
+
console.log('\n=== NATS Messaging (v3) ===\n');
|
|
163
|
+
console.log('Note: Requires nats and NATS server');
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const nats = new NATSMessaging({
|
|
167
|
+
servers: ['nats://localhost:4222'],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await nats.connect();
|
|
171
|
+
console.log('✅ Connected to NATS');
|
|
172
|
+
|
|
173
|
+
// Publish message
|
|
174
|
+
const result = await nats.publish('user.created', {
|
|
175
|
+
userId: 123,
|
|
176
|
+
name: 'Bob Smith',
|
|
177
|
+
});
|
|
178
|
+
console.log('Published to NATS:', result);
|
|
179
|
+
|
|
180
|
+
// Request-reply pattern
|
|
181
|
+
// const response = await nats.request('user.get', { userId: 123 });
|
|
182
|
+
// console.log('Response:', response);
|
|
183
|
+
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (error.message.includes('nats')) {
|
|
186
|
+
console.log('⚠️ nats not installed. Install with: npm install nats');
|
|
187
|
+
} else {
|
|
188
|
+
console.log('⚠️ NATS demo requires running NATS server');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Run all demos
|
|
194
|
+
async function runDemos() {
|
|
195
|
+
console.log('🚀 Navis.js v3 Features Demo\n');
|
|
196
|
+
console.log('='.repeat(50));
|
|
197
|
+
|
|
198
|
+
await demoLogger();
|
|
199
|
+
await demoMetrics();
|
|
200
|
+
await demoTracer();
|
|
201
|
+
await demoSQS();
|
|
202
|
+
await demoKafka();
|
|
203
|
+
await demoNATS();
|
|
204
|
+
|
|
205
|
+
console.log('\n' + '='.repeat(50));
|
|
206
|
+
console.log('✅ v3 features demo completed!');
|
|
207
|
+
console.log('\nNote: Messaging adapters require their respective dependencies:');
|
|
208
|
+
console.log(' - SQS: npm install @aws-sdk/client-sqs');
|
|
209
|
+
console.log(' - Kafka: npm install kafkajs');
|
|
210
|
+
console.log(' - NATS: npm install nats');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Run if called directly
|
|
214
|
+
if (require.main === module) {
|
|
215
|
+
runDemos().catch(console.error);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = {
|
|
219
|
+
demoLogger,
|
|
220
|
+
demoMetrics,
|
|
221
|
+
demoTracer,
|
|
222
|
+
demoSQS,
|
|
223
|
+
demoKafka,
|
|
224
|
+
demoNATS,
|
|
225
|
+
};
|
|
226
|
+
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,12 +1,50 @@
|
|
|
1
|
-
const NavisApp = require('./core/app');
|
|
2
|
-
const ServiceClient = require('./utils/service-client');
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
const NavisApp = require('./core/app');
|
|
2
|
+
const ServiceClient = require('./utils/service-client');
|
|
3
|
+
const ServiceConfig = require('./utils/service-config');
|
|
4
|
+
const ServiceDiscovery = require('./utils/service-discovery');
|
|
5
|
+
const CircuitBreaker = require('./utils/circuit-breaker');
|
|
6
|
+
const { success, error } = require('./utils/response');
|
|
7
|
+
const { retry, shouldRetryHttpStatus } = require('./utils/retry');
|
|
8
|
+
|
|
9
|
+
// v3: Async Messaging
|
|
10
|
+
const SQSMessaging = require('./messaging/sqs-adapter');
|
|
11
|
+
const KafkaMessaging = require('./messaging/kafka-adapter');
|
|
12
|
+
const NATSMessaging = require('./messaging/nats-adapter');
|
|
13
|
+
|
|
14
|
+
// v3: Observability
|
|
15
|
+
const Logger = require('./observability/logger');
|
|
16
|
+
const Metrics = require('./observability/metrics');
|
|
17
|
+
const Tracer = require('./observability/tracer');
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
// Core
|
|
21
|
+
NavisApp,
|
|
22
|
+
|
|
23
|
+
// Service Client (v2 enhanced)
|
|
24
|
+
ServiceClient,
|
|
25
|
+
|
|
26
|
+
// v2 Features
|
|
27
|
+
ServiceConfig,
|
|
28
|
+
ServiceDiscovery,
|
|
29
|
+
CircuitBreaker,
|
|
30
|
+
|
|
31
|
+
// v3: Async Messaging
|
|
32
|
+
SQSMessaging,
|
|
33
|
+
KafkaMessaging,
|
|
34
|
+
NATSMessaging,
|
|
35
|
+
|
|
36
|
+
// v3: Observability
|
|
37
|
+
Logger,
|
|
38
|
+
Metrics,
|
|
39
|
+
Tracer,
|
|
40
|
+
|
|
41
|
+
// Utilities
|
|
42
|
+
response: {
|
|
43
|
+
success,
|
|
44
|
+
error,
|
|
45
|
+
},
|
|
46
|
+
retry: {
|
|
47
|
+
retry,
|
|
48
|
+
shouldRetryHttpStatus,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Messaging Interface
|
|
3
|
+
* v3: Abstract base class for async messaging adapters
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class BaseMessaging {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
this.subscribers = new Map(); // topic -> [handlers]
|
|
10
|
+
this.isConnected = false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Connect to messaging broker
|
|
15
|
+
* @abstract
|
|
16
|
+
*/
|
|
17
|
+
async connect() {
|
|
18
|
+
throw new Error('connect() must be implemented by subclass');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Disconnect from messaging broker
|
|
23
|
+
* @abstract
|
|
24
|
+
*/
|
|
25
|
+
async disconnect() {
|
|
26
|
+
throw new Error('disconnect() must be implemented by subclass');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Publish a message to a topic/queue
|
|
31
|
+
* @abstract
|
|
32
|
+
* @param {string} topic - Topic/queue name
|
|
33
|
+
* @param {Object} message - Message payload
|
|
34
|
+
* @param {Object} options - Publishing options
|
|
35
|
+
*/
|
|
36
|
+
async publish(topic, message, options = {}) {
|
|
37
|
+
throw new Error('publish() must be implemented by subclass');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Subscribe to a topic/queue
|
|
42
|
+
* @abstract
|
|
43
|
+
* @param {string} topic - Topic/queue name
|
|
44
|
+
* @param {Function} handler - Message handler function
|
|
45
|
+
* @param {Object} options - Subscription options
|
|
46
|
+
*/
|
|
47
|
+
async subscribe(topic, handler, options = {}) {
|
|
48
|
+
throw new Error('subscribe() must be implemented by subclass');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Unsubscribe from a topic/queue
|
|
53
|
+
* @abstract
|
|
54
|
+
* @param {string} topic - Topic/queue name
|
|
55
|
+
* @param {Function} handler - Optional specific handler to unsubscribe
|
|
56
|
+
*/
|
|
57
|
+
async unsubscribe(topic, handler = null) {
|
|
58
|
+
throw new Error('unsubscribe() must be implemented by subclass');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Register a handler for a topic
|
|
63
|
+
* @protected
|
|
64
|
+
*/
|
|
65
|
+
_registerHandler(topic, handler) {
|
|
66
|
+
if (!this.subscribers.has(topic)) {
|
|
67
|
+
this.subscribers.set(topic, []);
|
|
68
|
+
}
|
|
69
|
+
this.subscribers.get(topic).push(handler);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get all handlers for a topic
|
|
74
|
+
* @protected
|
|
75
|
+
*/
|
|
76
|
+
_getHandlers(topic) {
|
|
77
|
+
return this.subscribers.get(topic) || [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = BaseMessaging;
|
|
82
|
+
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kafka Messaging Adapter
|
|
3
|
+
* v3: Kafka integration for async messaging
|
|
4
|
+
*
|
|
5
|
+
* Note: Requires kafkajs to be installed
|
|
6
|
+
* npm install kafkajs
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const BaseMessaging = require('./base-messaging');
|
|
10
|
+
|
|
11
|
+
class KafkaMessaging extends BaseMessaging {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
super(options);
|
|
14
|
+
this.brokers = options.brokers || ['localhost:9092'];
|
|
15
|
+
this.clientId = options.clientId || 'navis-client';
|
|
16
|
+
this.consumerGroupId = options.consumerGroupId || 'navis-group';
|
|
17
|
+
this.kafka = null;
|
|
18
|
+
this.producer = null;
|
|
19
|
+
this.consumer = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Connect to Kafka
|
|
24
|
+
*/
|
|
25
|
+
async connect() {
|
|
26
|
+
try {
|
|
27
|
+
const { Kafka } = require('kafkajs');
|
|
28
|
+
this.Kafka = Kafka;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
throw new Error('kafkajs is required. Install it with: npm install kafkajs');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.kafka = new this.Kafka({
|
|
34
|
+
clientId: this.clientId,
|
|
35
|
+
brokers: this.brokers,
|
|
36
|
+
...this.options.kafkaConfig,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
this.producer = this.kafka.producer();
|
|
40
|
+
await this.producer.connect();
|
|
41
|
+
|
|
42
|
+
this.consumer = this.kafka.consumer({ groupId: this.consumerGroupId });
|
|
43
|
+
await this.consumer.connect();
|
|
44
|
+
|
|
45
|
+
this.isConnected = true;
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Disconnect from Kafka
|
|
51
|
+
*/
|
|
52
|
+
async disconnect() {
|
|
53
|
+
this.isConnected = false;
|
|
54
|
+
|
|
55
|
+
if (this.producer) {
|
|
56
|
+
await this.producer.disconnect();
|
|
57
|
+
this.producer = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (this.consumer) {
|
|
61
|
+
await this.consumer.disconnect();
|
|
62
|
+
this.consumer = null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Publish message to Kafka topic
|
|
68
|
+
* @param {string} topic - Kafka topic name
|
|
69
|
+
* @param {Object} message - Message payload
|
|
70
|
+
* @param {Object} options - Publishing options
|
|
71
|
+
*/
|
|
72
|
+
async publish(topic, message, options = {}) {
|
|
73
|
+
if (!this.isConnected) {
|
|
74
|
+
await this.connect();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = await this.producer.send({
|
|
78
|
+
topic,
|
|
79
|
+
messages: [{
|
|
80
|
+
key: options.key || null,
|
|
81
|
+
value: JSON.stringify(message),
|
|
82
|
+
headers: options.headers || {},
|
|
83
|
+
}],
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
topic,
|
|
88
|
+
partition: result[0].partition,
|
|
89
|
+
offset: result[0].offset,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Subscribe to Kafka topic
|
|
95
|
+
* @param {string} topic - Kafka topic name
|
|
96
|
+
* @param {Function} handler - Message handler function
|
|
97
|
+
* @param {Object} options - Subscription options
|
|
98
|
+
*/
|
|
99
|
+
async subscribe(topic, handler, options = {}) {
|
|
100
|
+
if (!this.isConnected) {
|
|
101
|
+
await this.connect();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this._registerHandler(topic, handler);
|
|
105
|
+
|
|
106
|
+
await this.consumer.subscribe({
|
|
107
|
+
topic,
|
|
108
|
+
fromBeginning: options.fromBeginning || false,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await this.consumer.run({
|
|
112
|
+
eachMessage: async ({ topic, partition, message }) => {
|
|
113
|
+
try {
|
|
114
|
+
const body = JSON.parse(message.value.toString());
|
|
115
|
+
const handlers = this._getHandlers(topic);
|
|
116
|
+
|
|
117
|
+
for (const h of handlers) {
|
|
118
|
+
await h(body, {
|
|
119
|
+
topic,
|
|
120
|
+
partition,
|
|
121
|
+
offset: message.offset,
|
|
122
|
+
key: message.key?.toString(),
|
|
123
|
+
headers: message.headers,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error('Error processing Kafka message:', error);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return { topic, handler };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Unsubscribe from topic
|
|
137
|
+
*/
|
|
138
|
+
async unsubscribe(topic, handler = null) {
|
|
139
|
+
if (handler) {
|
|
140
|
+
const handlers = this._getHandlers(topic);
|
|
141
|
+
const index = handlers.indexOf(handler);
|
|
142
|
+
if (index > -1) {
|
|
143
|
+
handlers.splice(index, 1);
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
this.subscribers.delete(topic);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = KafkaMessaging;
|
|
152
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NATS Messaging Adapter
|
|
3
|
+
* v3: NATS integration for async messaging
|
|
4
|
+
*
|
|
5
|
+
* Note: Requires nats to be installed
|
|
6
|
+
* npm install nats
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const BaseMessaging = require('./base-messaging');
|
|
10
|
+
|
|
11
|
+
class NATSMessaging extends BaseMessaging {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
super(options);
|
|
14
|
+
this.servers = options.servers || ['nats://localhost:4222'];
|
|
15
|
+
this.nc = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Connect to NATS
|
|
20
|
+
*/
|
|
21
|
+
async connect() {
|
|
22
|
+
try {
|
|
23
|
+
const { connect } = require('nats');
|
|
24
|
+
this.connectNATS = connect;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
throw new Error('nats is required. Install it with: npm install nats');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.nc = await this.connectNATS({
|
|
30
|
+
servers: this.servers,
|
|
31
|
+
...this.options.natsConfig,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
this.isConnected = true;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Disconnect from NATS
|
|
40
|
+
*/
|
|
41
|
+
async disconnect() {
|
|
42
|
+
this.isConnected = false;
|
|
43
|
+
if (this.nc) {
|
|
44
|
+
await this.nc.close();
|
|
45
|
+
this.nc = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Publish message to NATS subject
|
|
51
|
+
* @param {string} subject - NATS subject
|
|
52
|
+
* @param {Object} message - Message payload
|
|
53
|
+
* @param {Object} options - Publishing options
|
|
54
|
+
*/
|
|
55
|
+
async publish(subject, message, options = {}) {
|
|
56
|
+
if (!this.isConnected) {
|
|
57
|
+
await this.connect();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const data = Buffer.from(JSON.stringify(message));
|
|
61
|
+
this.nc.publish(subject, data);
|
|
62
|
+
|
|
63
|
+
return { subject, published: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Subscribe to NATS subject
|
|
68
|
+
* @param {string} subject - NATS subject
|
|
69
|
+
* @param {Function} handler - Message handler function
|
|
70
|
+
* @param {Object} options - Subscription options
|
|
71
|
+
*/
|
|
72
|
+
async subscribe(subject, handler, options = {}) {
|
|
73
|
+
if (!this.isConnected) {
|
|
74
|
+
await this.connect();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this._registerHandler(subject, handler);
|
|
78
|
+
|
|
79
|
+
const subscription = this.nc.subscribe(subject, {
|
|
80
|
+
queue: options.queue || undefined,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
(async () => {
|
|
84
|
+
for await (const msg of subscription) {
|
|
85
|
+
try {
|
|
86
|
+
const body = JSON.parse(msg.data.toString());
|
|
87
|
+
const handlers = this._getHandlers(subject);
|
|
88
|
+
|
|
89
|
+
for (const h of handlers) {
|
|
90
|
+
await h(body, {
|
|
91
|
+
subject: msg.subject,
|
|
92
|
+
reply: msg.reply,
|
|
93
|
+
sid: msg.sid,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('Error processing NATS message:', error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
})().catch(console.error);
|
|
101
|
+
|
|
102
|
+
return { subject, handler, subscription };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Unsubscribe from subject
|
|
107
|
+
*/
|
|
108
|
+
async unsubscribe(subject, handler = null) {
|
|
109
|
+
if (handler) {
|
|
110
|
+
const handlers = this._getHandlers(subject);
|
|
111
|
+
const index = handlers.indexOf(handler);
|
|
112
|
+
if (index > -1) {
|
|
113
|
+
handlers.splice(index, 1);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
this.subscribers.delete(subject);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Request-reply pattern
|
|
122
|
+
* @param {string} subject - NATS subject
|
|
123
|
+
* @param {Object} message - Request message
|
|
124
|
+
* @param {Object} options - Request options
|
|
125
|
+
*/
|
|
126
|
+
async request(subject, message, options = {}) {
|
|
127
|
+
if (!this.isConnected) {
|
|
128
|
+
await this.connect();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const data = Buffer.from(JSON.stringify(message));
|
|
132
|
+
const response = await this.nc.request(subject, data, {
|
|
133
|
+
timeout: options.timeout || 5000,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return JSON.parse(response.data.toString());
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = NATSMessaging;
|
|
141
|
+
|