@pulzar/core
Version:
Next-generation Node.js framework for ultra-fast web applications with zero-reflection DI, GraphQL, WebSockets, events, and edge runtime support
605 lines • 19.9 kB
JavaScript
import { EventError, } from "../types";
import { logger } from "../../utils/logger";
export class NATSEventAdapter {
name = "nats";
version = "1.0.0";
capabilities = {
persistence: true,
clustering: true,
partitioning: false,
consumerGroups: true,
deadLetterQueue: true,
exactly_once: false,
at_least_once: true,
ordering: true,
wildcards: true,
replay: true,
backpressure: true,
};
client;
jetStream;
jetStreamManager;
config;
connected = false;
subscriptions = new Map();
stats = {
published: 0,
delivered: 0,
acknowledged: 0,
failed: 0,
retries: 0,
dlqSize: 0,
activeSubscriptions: 0,
throughputPerSecond: 0,
averageLatency: 0,
errorRate: 0,
backpressureEvents: 0,
lastActivity: new Date(),
};
constructor(config = {}) {
this.config = {
servers: config.servers || "nats://localhost:4222",
user: config.user,
pass: config.pass,
token: config.token,
nkey: config.nkey,
jwt: config.jwt,
seed: config.seed,
timeout: config.timeout || 2000,
reconnect: config.reconnect !== false,
maxReconnectAttempts: config.maxReconnectAttempts || -1,
reconnectTimeWait: config.reconnectTimeWait || 2000,
jetstream: {
enabled: config.jetstream?.enabled !== false,
domain: config.jetstream?.domain,
apiPrefix: config.jetstream?.apiPrefix,
timeout: config.jetstream?.timeout || 5000,
},
};
}
/**
* Connect to NATS server
*/
async connect() {
if (this.connected) {
return;
}
try {
// Try to dynamically import NATS
const nats = await this.importNATS();
if (!nats) {
throw new EventError("NATS package not installed. Run: npm install nats", "NATS_NOT_INSTALLED");
}
// Connect to NATS
this.client = await nats.connect({
servers: this.config.servers,
user: this.config.user,
pass: this.config.pass,
token: this.config.token,
timeout: this.config.timeout,
reconnect: this.config.reconnect,
maxReconnectAttempts: this.config.maxReconnectAttempts,
reconnectTimeWait: this.config.reconnectTimeWait,
});
// Setup JetStream if enabled
if (this.config.jetstream.enabled) {
this.jetStream = this.client.jetstream({
domain: this.config.jetstream.domain,
apiPrefix: this.config.jetstream.apiPrefix,
timeout: this.config.jetstream.timeout,
});
this.jetStreamManager = await this.client.jetstreamManager();
}
// Setup connection event handlers
this.setupEventHandlers();
this.connected = true;
logger.info("NATS adapter connected", {
servers: this.config.servers,
jetstream: this.config.jetstream.enabled,
});
}
catch (error) {
logger.error("Failed to connect to NATS", { error });
throw new EventError(`NATS connection failed: ${error.message}`, "CONNECTION_FAILED", undefined, error);
}
}
/**
* Disconnect from NATS
*/
async disconnect() {
if (!this.connected || !this.client) {
return;
}
try {
// Close all subscriptions
for (const [id, subscription] of this.subscriptions) {
try {
await subscription.unsubscribe();
}
catch (error) {
logger.warn(`Failed to unsubscribe ${id}`, { error });
}
}
this.subscriptions.clear();
// Flush pending messages
await this.client.flush();
// Close connection
await this.client.close();
this.connected = false;
this.resetStats();
logger.info("NATS adapter disconnected");
}
catch (error) {
logger.error("Error disconnecting from NATS", { error });
throw error;
}
}
/**
* Check if connected
*/
isConnected() {
return this.connected && this.client && !this.client.isClosed();
}
/**
* Publish event to NATS/JetStream
*/
async publish(subject, event) {
if (!this.connected) {
throw new EventError("NATS not connected", "NOT_CONNECTED");
}
const startTime = Date.now();
try {
const payload = this.serialize(event);
let result;
if (this.jetStream) {
// Use JetStream for persistent messaging
result = await this.jetStream.publish(subject, payload, {
msgID: event.id,
headers: this.createHeaders(event),
});
}
else {
// Use core NATS for simple pub/sub
this.client.publish(subject, payload);
result = {
seq: 0,
duplicate: false,
stream: "",
};
}
this.updateStats("published", Date.now() - startTime);
return {
messageId: event.id,
partition: result.stream,
offset: result.seq?.toString(),
timestamp: new Date().toISOString(),
};
}
catch (error) {
this.updateStats("failed");
logger.error("Failed to publish to NATS", { subject, error });
throw new EventError(`NATS publish failed: ${error.message}`, "PUBLISH_FAILED", event, error);
}
}
/**
* Subscribe to events
*/
async subscribe(subject, handler, options = {}) {
if (!this.connected) {
throw new EventError("NATS not connected", "NOT_CONNECTED");
}
try {
let subscription;
const subscriptionId = this.generateId();
if (this.jetStream && options.durable) {
// JetStream durable consumer
subscription = await this.createJetStreamConsumer(subject, handler, options, subscriptionId);
}
else if (this.jetStream) {
// JetStream ephemeral consumer
subscription = await this.jetStream.subscribe(subject, {
callback: (err, msg) => {
if (err) {
logger.error("JetStream subscription error", { error: err });
return;
}
this.handleMessage(msg, handler, options);
},
max: options.maxMsgs,
queue: options.consumerGroup,
});
}
else {
// Core NATS subscription
subscription = this.client.subscribe(subject, {
callback: (err, msg) => {
if (err) {
logger.error("NATS subscription error", { error: err });
return;
}
this.handleMessage(msg, handler, options);
},
max: options.maxMsgs,
queue: options.consumerGroup,
});
}
const handle = {
id: subscriptionId,
subject,
active: true,
consumerGroup: options.consumerGroup,
createdAt: new Date(),
unsubscribe: async () => {
await this.unsubscribe(handle);
},
};
this.subscriptions.set(subscriptionId, subscription);
this.stats.activeSubscriptions = this.subscriptions.size;
logger.debug("NATS subscription created", {
subject,
subscriptionId,
durable: options.durable,
consumerGroup: options.consumerGroup,
});
return handle;
}
catch (error) {
logger.error("Failed to subscribe to NATS", { subject, error });
throw new EventError(`NATS subscribe failed: ${error.message}`, "SUBSCRIBE_FAILED", undefined, error);
}
}
/**
* Unsubscribe from events
*/
async unsubscribe(handle) {
const subscription = this.subscriptions.get(handle.id);
if (!subscription) {
return;
}
try {
await subscription.unsubscribe();
this.subscriptions.delete(handle.id);
this.stats.activeSubscriptions = this.subscriptions.size;
logger.debug("NATS subscription removed", { subscriptionId: handle.id });
}
catch (error) {
logger.error("Failed to unsubscribe from NATS", {
subscriptionId: handle.id,
error,
});
throw error;
}
}
/**
* Acknowledge message
*/
async ack(event) {
if (event._ackToken) {
try {
event._ackToken.ack();
this.stats.acknowledged++;
}
catch (error) {
logger.error("Failed to ack NATS message", {
eventId: event.id,
error,
});
throw error;
}
}
}
/**
* Negative acknowledge message
*/
async nack(event, requeue = false) {
if (event._ackToken) {
try {
if (requeue) {
event._ackToken.nak();
}
else {
event._ackToken.term();
}
}
catch (error) {
logger.error("Failed to nack NATS message", {
eventId: event.id,
error,
});
throw error;
}
}
}
/**
* Flush pending messages
*/
async flush() {
if (this.client) {
await this.client.flush();
}
}
/**
* Get adapter statistics
*/
async getStats() {
// Get JetStream account info if available
if (this.jetStreamManager) {
try {
const accountInfo = await this.jetStreamManager.getAccountInfo();
// Update stats based on JetStream info
}
catch (error) {
logger.debug("Failed to get JetStream account info", { error });
}
}
return { ...this.stats };
}
/**
* Health check
*/
async healthCheck() {
const checks = [
{
name: "connection",
status: this.isConnected() ? "pass" : "fail",
message: this.isConnected()
? "Connected to NATS"
: "Not connected to NATS",
},
];
// JetStream health check
if (this.jetStreamManager) {
try {
await this.jetStreamManager.getAccountInfo();
checks.push({
name: "jetstream",
status: "pass",
message: "JetStream is available",
});
}
catch (error) {
checks.push({
name: "jetstream",
status: "fail",
message: `JetStream error: ${error.message}`,
});
}
}
const hasFailures = checks.some((c) => c.status === "fail");
const hasWarnings = checks.some((c) => c.status === "warn");
return {
status: hasFailures ? "unhealthy" : hasWarnings ? "degraded" : "healthy",
checks,
timestamp: new Date(),
};
}
/**
* Create JetStream consumer
*/
async createJetStreamConsumer(subject, handler, options, subscriptionId) {
const streamName = this.getStreamName(subject);
// Ensure stream exists
await this.ensureStream(streamName, [subject]);
const consumerConfig = {
name: options.consumerGroup || subscriptionId,
durable: options.durable
? options.consumerGroup || subscriptionId
: undefined,
deliverPolicy: options.deliverPolicy || "all",
ackPolicy: options.ackPolicy === "auto" ? "all" : options.ackPolicy || "explicit",
ackWait: options.ackTimeout
? options.ackTimeout * 1_000_000
: 30_000_000_000, // nanoseconds
maxDeliver: options.maxDeliver || 5,
filterSubject: subject,
maxAckPending: options.maxAckPending || 1000,
};
// Create or get consumer
const consumer = await this.jetStreamManager.consumers.add(streamName, consumerConfig);
// Create subscription
const subscription = await this.jetStream.subscribe(subject, {
consumer: consumer.name,
stream: streamName,
callback: (err, msg) => {
if (err) {
logger.error("JetStream consumer error", { error: err });
return;
}
this.handleMessage(msg, handler, options);
},
});
return subscription;
}
/**
* Handle incoming message
*/
async handleMessage(msg, handler, options) {
const startTime = Date.now();
try {
const event = this.deserialize(msg.data, msg);
// Apply filter if provided
if (options.filter) {
const matches = await options.filter(event);
if (!matches) {
// Ack filtered messages
if (msg.ack)
msg.ack();
return;
}
}
// Store ack token for manual acknowledgment
event._ackToken = msg.ack ? msg : undefined;
await handler(event);
// Auto-ack if configured
if (options.ackPolicy === "auto" && msg.ack) {
msg.ack();
this.stats.acknowledged++;
}
this.updateStats("delivered", Date.now() - startTime);
}
catch (error) {
this.updateStats("failed");
logger.error("Failed to handle NATS message", { error });
// Handle message failure based on policy
if (msg.ack) {
if (options.maxDeliver &&
msg.info?.redeliveryCount >= options.maxDeliver) {
msg.term(); // Send to DLQ
}
else {
msg.nak(); // Requeue for retry
}
}
throw error;
}
}
/**
* Ensure stream exists
*/
async ensureStream(name, subjects) {
if (!this.jetStreamManager) {
return;
}
try {
// Try to get existing stream
await this.jetStreamManager.streams.info(name);
}
catch (error) {
// Create stream if it doesn't exist
const streamConfig = {
name,
subjects,
storage: "file",
retention: "limits",
maxAge: 7 * 24 * 60 * 60 * 1_000_000_000, // 7 days in nanoseconds
replicas: 1,
};
await this.jetStreamManager.streams.add(streamConfig);
logger.info("Created JetStream stream", { name, subjects });
}
}
/**
* Get stream name from subject
*/
getStreamName(subject) {
// Convert subject to stream name (replace dots with underscores)
return `STREAM_${subject.replace(/\./g, "_").replace(/\*/g, "STAR").replace(/>/g, "GT").toUpperCase()}`;
}
/**
* Create NATS headers
*/
createHeaders(event) {
const headers = {};
// Copy event headers
for (const [key, value] of Object.entries(event.headers)) {
headers[key] = String(value);
}
// Add metadata as headers
if (event.metadata.correlationId) {
headers["X-Correlation-ID"] = event.metadata.correlationId;
}
if (event.metadata.traceId) {
headers["X-Trace-ID"] = event.metadata.traceId;
}
if (event.metadata.userId) {
headers["X-User-ID"] = event.metadata.userId;
}
return headers;
}
/**
* Serialize event for transmission
*/
serialize(event) {
const payload = JSON.stringify(event);
return new TextEncoder().encode(payload);
}
/**
* Deserialize received message
*/
deserialize(data, msg) {
const payload = new TextDecoder().decode(data);
const event = JSON.parse(payload);
// Add internal fields
event._adapter = this.name;
if (msg.seq) {
event._offset = msg.seq;
}
if (msg.stream) {
event._partition = msg.stream;
}
return event;
}
/**
* Setup connection event handlers
*/
setupEventHandlers() {
if (!this.client)
return;
this.client.closed().then(() => {
this.connected = false;
logger.warn("NATS connection closed");
});
// Handle reconnection events
if (this.client.addEventListener) {
this.client.addEventListener("reconnect", () => {
logger.info("NATS reconnected");
});
this.client.addEventListener("error", (error) => {
logger.error("NATS connection error", { error });
});
}
}
/**
* Try to import NATS package
*/
async importNATS() {
try {
return await import("nats");
}
catch (error) {
logger.warn("NATS package not available", { error });
return null;
}
}
/**
* Update statistics
*/
updateStats(type, latency) {
this.stats[type]++;
this.stats.lastActivity = new Date();
if (latency !== undefined) {
// Update average latency (simple moving average)
this.stats.averageLatency = (this.stats.averageLatency + latency) / 2;
}
// Calculate error rate
const total = this.stats.published + this.stats.delivered;
this.stats.errorRate = total > 0 ? this.stats.failed / total : 0;
}
/**
* Reset statistics
*/
resetStats() {
this.stats = {
published: 0,
delivered: 0,
acknowledged: 0,
failed: 0,
retries: 0,
dlqSize: 0,
activeSubscriptions: 0,
throughputPerSecond: 0,
averageLatency: 0,
errorRate: 0,
backpressureEvents: 0,
lastActivity: new Date(),
};
}
/**
* Generate unique ID
*/
generateId() {
return `nats-${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
}
export default NATSEventAdapter;
//# sourceMappingURL=nats.js.map