navis.js 2.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+
@@ -0,0 +1,175 @@
1
+ /**
2
+ * AWS SQS Messaging Adapter
3
+ * v3: SQS integration for async messaging
4
+ *
5
+ * Note: Requires @aws-sdk/client-sqs to be installed
6
+ * npm install @aws-sdk/client-sqs
7
+ */
8
+
9
+ const BaseMessaging = require('./base-messaging');
10
+
11
+ class SQSMessaging extends BaseMessaging {
12
+ constructor(options = {}) {
13
+ super(options);
14
+ this.region = options.region || process.env.AWS_REGION || 'us-east-1';
15
+ this.queueUrl = options.queueUrl;
16
+ this.sqsClient = null;
17
+
18
+ // Try to load AWS SDK (optional dependency)
19
+ try {
20
+ const { SQSClient, SendMessageCommand, ReceiveMessageCommand, DeleteMessageCommand } = require('@aws-sdk/client-sqs');
21
+ this.SQSClient = SQSClient;
22
+ this.SendMessageCommand = SendMessageCommand;
23
+ this.ReceiveMessageCommand = ReceiveMessageCommand;
24
+ this.DeleteMessageCommand = DeleteMessageCommand;
25
+ } catch (err) {
26
+ // AWS SDK not installed - will throw error on connect
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Connect to SQS
32
+ */
33
+ async connect() {
34
+ if (!this.SQSClient) {
35
+ throw new Error('@aws-sdk/client-sqs is required. Install it with: npm install @aws-sdk/client-sqs');
36
+ }
37
+
38
+ this.sqsClient = new this.SQSClient({
39
+ region: this.region,
40
+ ...this.options.awsConfig,
41
+ });
42
+
43
+ this.isConnected = true;
44
+ return this;
45
+ }
46
+
47
+ /**
48
+ * Disconnect from SQS
49
+ */
50
+ async disconnect() {
51
+ this.isConnected = false;
52
+ if (this.sqsClient) {
53
+ // SQS client doesn't need explicit disconnect
54
+ this.sqsClient = null;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Publish message to SQS queue
60
+ * @param {string} queueUrl - SQS queue URL
61
+ * @param {Object} message - Message payload
62
+ * @param {Object} options - Publishing options
63
+ */
64
+ async publish(queueUrl, message, options = {}) {
65
+ if (!this.isConnected) {
66
+ await this.connect();
67
+ }
68
+
69
+ const command = new this.SendMessageCommand({
70
+ QueueUrl: queueUrl || this.queueUrl,
71
+ MessageBody: JSON.stringify(message),
72
+ MessageAttributes: options.attributes || {},
73
+ DelaySeconds: options.delaySeconds || 0,
74
+ });
75
+
76
+ const response = await this.sqsClient.send(command);
77
+ return {
78
+ messageId: response.MessageId,
79
+ md5OfBody: response.MD5OfMessageBody,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Subscribe to SQS queue (long polling)
85
+ * @param {string} queueUrl - SQS queue URL
86
+ * @param {Function} handler - Message handler function
87
+ * @param {Object} options - Subscription options
88
+ */
89
+ async subscribe(queueUrl, handler, options = {}) {
90
+ if (!this.isConnected) {
91
+ await this.connect();
92
+ }
93
+
94
+ const targetQueueUrl = queueUrl || this.queueUrl;
95
+ this._registerHandler(targetQueueUrl, handler);
96
+
97
+ // Start polling
98
+ const poll = async () => {
99
+ if (!this.isConnected) {
100
+ return;
101
+ }
102
+
103
+ try {
104
+ const command = new this.ReceiveMessageCommand({
105
+ QueueUrl: targetQueueUrl,
106
+ MaxNumberOfMessages: options.maxMessages || 1,
107
+ WaitTimeSeconds: options.waitTimeSeconds || 20, // Long polling
108
+ VisibilityTimeout: options.visibilityTimeout || 30,
109
+ });
110
+
111
+ const response = await this.sqsClient.send(command);
112
+
113
+ if (response.Messages && response.Messages.length > 0) {
114
+ for (const message of response.Messages) {
115
+ try {
116
+ const body = JSON.parse(message.Body);
117
+ const handlers = this._getHandlers(targetQueueUrl);
118
+
119
+ for (const h of handlers) {
120
+ await h(body, {
121
+ messageId: message.MessageId,
122
+ receiptHandle: message.ReceiptHandle,
123
+ attributes: message.Attributes,
124
+ });
125
+ }
126
+
127
+ // Delete message after successful processing
128
+ if (options.autoDelete !== false) {
129
+ await this.sqsClient.send(new this.DeleteMessageCommand({
130
+ QueueUrl: targetQueueUrl,
131
+ ReceiptHandle: message.ReceiptHandle,
132
+ }));
133
+ }
134
+ } catch (error) {
135
+ console.error('Error processing SQS message:', error);
136
+ // Message will become visible again after visibility timeout
137
+ }
138
+ }
139
+ }
140
+ } catch (error) {
141
+ console.error('Error receiving SQS messages:', error);
142
+ }
143
+
144
+ // Continue polling
145
+ if (this.isConnected) {
146
+ setTimeout(poll, options.pollInterval || 0);
147
+ }
148
+ };
149
+
150
+ // Start polling
151
+ poll();
152
+
153
+ return { queueUrl: targetQueueUrl, handler };
154
+ }
155
+
156
+ /**
157
+ * Unsubscribe from queue
158
+ */
159
+ async unsubscribe(queueUrl, handler = null) {
160
+ const targetQueueUrl = queueUrl || this.queueUrl;
161
+
162
+ if (handler) {
163
+ const handlers = this._getHandlers(targetQueueUrl);
164
+ const index = handlers.indexOf(handler);
165
+ if (index > -1) {
166
+ handlers.splice(index, 1);
167
+ }
168
+ } else {
169
+ this.subscribers.delete(targetQueueUrl);
170
+ }
171
+ }
172
+ }
173
+
174
+ module.exports = SQSMessaging;
175
+
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Structured Logger
3
+ * v3: Logging with levels and structured output
4
+ */
5
+
6
+ const LOG_LEVELS = {
7
+ DEBUG: 0,
8
+ INFO: 1,
9
+ WARN: 2,
10
+ ERROR: 3,
11
+ FATAL: 4,
12
+ };
13
+
14
+ class Logger {
15
+ constructor(options = {}) {
16
+ const level = options.level || process.env.LOG_LEVEL || 'INFO';
17
+ this.level = level;
18
+ this.levelValue = LOG_LEVELS[level.toUpperCase()] || LOG_LEVELS.INFO;
19
+ this.format = options.format || 'json'; // 'json' or 'text'
20
+ this.enableColors = options.enableColors !== false;
21
+ this.context = options.context || {};
22
+ }
23
+
24
+ /**
25
+ * Log a message
26
+ * @private
27
+ */
28
+ _log(level, message, data = {}) {
29
+ const levelValue = LOG_LEVELS[level];
30
+ if (levelValue < this.levelValue) {
31
+ return;
32
+ }
33
+
34
+ const logEntry = {
35
+ timestamp: new Date().toISOString(),
36
+ level,
37
+ message,
38
+ ...this.context,
39
+ ...data,
40
+ };
41
+
42
+ if (this.format === 'json') {
43
+ console.log(JSON.stringify(logEntry));
44
+ } else {
45
+ const color = this._getColor(level);
46
+ const reset = this.enableColors ? '\x1b[0m' : '';
47
+ const coloredLevel = this.enableColors ? `${color}${level}${reset}` : level;
48
+ console.log(`[${logEntry.timestamp}] ${coloredLevel}: ${message}`, data);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Get color for log level
54
+ * @private
55
+ */
56
+ _getColor(level) {
57
+ if (!this.enableColors) return '';
58
+
59
+ const colors = {
60
+ DEBUG: '\x1b[36m', // Cyan
61
+ INFO: '\x1b[32m', // Green
62
+ WARN: '\x1b[33m', // Yellow
63
+ ERROR: '\x1b[31m', // Red
64
+ FATAL: '\x1b[35m', // Magenta
65
+ };
66
+ return colors[level] || '';
67
+ }
68
+
69
+ /**
70
+ * Debug log
71
+ */
72
+ debug(message, data = {}) {
73
+ this._log('DEBUG', message, data);
74
+ }
75
+
76
+ /**
77
+ * Info log
78
+ */
79
+ info(message, data = {}) {
80
+ this._log('INFO', message, data);
81
+ }
82
+
83
+ /**
84
+ * Warning log
85
+ */
86
+ warn(message, data = {}) {
87
+ this._log('WARN', message, data);
88
+ }
89
+
90
+ /**
91
+ * Error log
92
+ */
93
+ error(message, error = null, data = {}) {
94
+ const errorData = error ? {
95
+ error: {
96
+ message: error.message,
97
+ stack: error.stack,
98
+ name: error.name,
99
+ },
100
+ } : {};
101
+ this._log('ERROR', message, { ...errorData, ...data });
102
+ }
103
+
104
+ /**
105
+ * Fatal log
106
+ */
107
+ fatal(message, error = null, data = {}) {
108
+ const errorData = error ? {
109
+ error: {
110
+ message: error.message,
111
+ stack: error.stack,
112
+ name: error.name,
113
+ },
114
+ } : {};
115
+ this._log('FATAL', message, { ...errorData, ...data });
116
+ }
117
+
118
+ /**
119
+ * Create child logger with additional context
120
+ */
121
+ child(context) {
122
+ return new Logger({
123
+ level: this.level,
124
+ format: this.format,
125
+ enableColors: this.enableColors,
126
+ context: { ...this.context, ...context },
127
+ });
128
+ }
129
+
130
+ /**
131
+ * Set log level
132
+ */
133
+ setLevel(level) {
134
+ const upperLevel = level.toUpperCase();
135
+ this.level = upperLevel;
136
+ this.levelValue = LOG_LEVELS[upperLevel] !== undefined ? LOG_LEVELS[upperLevel] : LOG_LEVELS.INFO;
137
+ }
138
+ }
139
+
140
+ module.exports = Logger;
141
+
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Metrics Collector
3
+ * v3: Collect and expose application metrics
4
+ */
5
+
6
+ class Metrics {
7
+ constructor(options = {}) {
8
+ this.metrics = {
9
+ counters: new Map(),
10
+ gauges: new Map(),
11
+ histograms: new Map(),
12
+ };
13
+ this.enabled = options.enabled !== false;
14
+ }
15
+
16
+ /**
17
+ * Increment a counter
18
+ * @param {string} name - Metric name
19
+ * @param {number} value - Increment value (default: 1)
20
+ * @param {Object} labels - Metric labels
21
+ */
22
+ increment(name, value = 1, labels = {}) {
23
+ if (!this.enabled) return;
24
+
25
+ const key = this._getKey(name, labels);
26
+ const current = this.metrics.counters.get(key) || 0;
27
+ this.metrics.counters.set(key, current + value);
28
+ }
29
+
30
+ /**
31
+ * Decrement a counter
32
+ * @param {string} name - Metric name
33
+ * @param {number} value - Decrement value (default: 1)
34
+ * @param {Object} labels - Metric labels
35
+ */
36
+ decrement(name, value = 1, labels = {}) {
37
+ this.increment(name, -value, labels);
38
+ }
39
+
40
+ /**
41
+ * Set a gauge value
42
+ * @param {string} name - Metric name
43
+ * @param {number} value - Gauge value
44
+ * @param {Object} labels - Metric labels
45
+ */
46
+ gauge(name, value, labels = {}) {
47
+ if (!this.enabled) return;
48
+
49
+ const key = this._getKey(name, labels);
50
+ this.metrics.gauges.set(key, value);
51
+ }
52
+
53
+ /**
54
+ * Record a histogram value
55
+ * @param {string} name - Metric name
56
+ * @param {number} value - Value to record
57
+ * @param {Object} labels - Metric labels
58
+ */
59
+ histogram(name, value, labels = {}) {
60
+ if (!this.enabled) return;
61
+
62
+ const key = this._getKey(name, labels);
63
+ if (!this.metrics.histograms.has(key)) {
64
+ this.metrics.histograms.set(key, []);
65
+ }
66
+ this.metrics.histograms.get(key).push({
67
+ value,
68
+ timestamp: Date.now(),
69
+ });
70
+
71
+ // Keep only last 1000 values per histogram
72
+ const values = this.metrics.histograms.get(key);
73
+ if (values.length > 1000) {
74
+ values.shift();
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Record request duration
80
+ * @param {string} method - HTTP method
81
+ * @param {string} path - Request path
82
+ * @param {number} duration - Duration in milliseconds
83
+ * @param {number} statusCode - HTTP status code
84
+ */
85
+ recordRequest(method, path, duration, statusCode) {
86
+ this.histogram('http_request_duration_ms', duration, {
87
+ method,
88
+ path,
89
+ status: statusCode,
90
+ });
91
+ this.increment('http_requests_total', 1, {
92
+ method,
93
+ path,
94
+ status: statusCode,
95
+ });
96
+ }
97
+
98
+ /**
99
+ * Get all metrics
100
+ * @returns {Object} - All collected metrics
101
+ */
102
+ getAll() {
103
+ return {
104
+ counters: Object.fromEntries(this.metrics.counters),
105
+ gauges: Object.fromEntries(this.metrics.gauges),
106
+ histograms: Object.fromEntries(
107
+ Array.from(this.metrics.histograms.entries()).map(([key, values]) => [
108
+ key,
109
+ {
110
+ count: values.length,
111
+ sum: values.reduce((sum, v) => sum + v.value, 0),
112
+ avg: values.length > 0 ? values.reduce((sum, v) => sum + v.value, 0) / values.length : 0,
113
+ min: values.length > 0 ? Math.min(...values.map(v => v.value)) : 0,
114
+ max: values.length > 0 ? Math.max(...values.map(v => v.value)) : 0,
115
+ values: values.slice(-100), // Last 100 values
116
+ },
117
+ ])
118
+ ),
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Get metrics in Prometheus format
124
+ * @returns {string} - Prometheus metrics format
125
+ */
126
+ toPrometheus() {
127
+ const lines = [];
128
+
129
+ // Counters
130
+ for (const [key, value] of this.metrics.counters.entries()) {
131
+ const { name, labels } = this._parseKey(key);
132
+ const labelStr = Object.entries(labels)
133
+ .map(([k, v]) => `${k}="${v}"`)
134
+ .join(',');
135
+ lines.push(`${name}{${labelStr}} ${value}`);
136
+ }
137
+
138
+ // Gauges
139
+ for (const [key, value] of this.metrics.gauges.entries()) {
140
+ const { name, labels } = this._parseKey(key);
141
+ const labelStr = Object.entries(labels)
142
+ .map(([k, v]) => `${k}="${v}"`)
143
+ .join(',');
144
+ lines.push(`${name}{${labelStr}} ${value}`);
145
+ }
146
+
147
+ // Histograms
148
+ for (const [key, histogram] of this.metrics.histograms.entries()) {
149
+ const { name, labels } = this._parseKey(key);
150
+ const labelStr = Object.entries(labels)
151
+ .map(([k, v]) => `${k}="${v}"`)
152
+ .join(',');
153
+ lines.push(`${name}_count{${labelStr}} ${histogram.length}`);
154
+ lines.push(`${name}_sum{${labelStr}} ${histogram.reduce((sum, v) => sum + v.value, 0)}`);
155
+ lines.push(`${name}_avg{${labelStr}} ${histogram.length > 0 ? histogram.reduce((sum, v) => sum + v.value, 0) / histogram.length : 0}`);
156
+ }
157
+
158
+ return lines.join('\n');
159
+ }
160
+
161
+ /**
162
+ * Reset all metrics
163
+ */
164
+ reset() {
165
+ this.metrics.counters.clear();
166
+ this.metrics.gauges.clear();
167
+ this.metrics.histograms.clear();
168
+ }
169
+
170
+ /**
171
+ * Get key for metric storage
172
+ * @private
173
+ */
174
+ _getKey(name, labels) {
175
+ const labelStr = JSON.stringify(labels);
176
+ return `${name}::${labelStr}`;
177
+ }
178
+
179
+ /**
180
+ * Parse key back to name and labels
181
+ * @private
182
+ */
183
+ _parseKey(key) {
184
+ const [name, labelStr] = key.split('::');
185
+ return {
186
+ name,
187
+ labels: JSON.parse(labelStr || '{}'),
188
+ };
189
+ }
190
+ }
191
+
192
+ module.exports = Metrics;
193
+