UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

407 lines (406 loc) • 13.1 kB
/** * Message Bus - Inter-agent communication infrastructure * * Provides a publish-subscribe message bus for agent-to-agent communication * with support for: * - Topic-based messaging * - Request-response patterns * - Broadcast messaging * - Message persistence and replay */ import { EventEmitter } from "events"; import { randomUUID } from "crypto"; import { logger } from "../../utils/logger.js"; /** * Message Bus - Central hub for agent communication */ export class MessageBus { subscriptions = new Map(); messageHistory = []; pendingRequests = new Map(); deadLetterQueue = []; config; emitter; constructor(config) { this.config = { maxHistorySize: 1000, defaultTtl: 60000, enablePersistence: false, enableDeadLetterQueue: true, requestTimeout: 30000, ...config, }; this.emitter = new EventEmitter(); // Increase max listeners for large networks this.emitter.setMaxListeners(100); logger.debug("[MessageBus] Created with config", { maxHistorySize: this.config.maxHistorySize, enableDeadLetterQueue: this.config.enableDeadLetterQueue, }); } /** * Subscribe to a topic */ subscribe(topic, subscriberId, handler, options) { const subscriptionId = randomUUID(); const subscription = { id: subscriptionId, topic, handler, options: options ?? {}, messageCount: 0, subscriberId, }; const topicSubs = this.subscriptions.get(topic) ?? []; topicSubs.push(subscription); this.subscriptions.set(topic, topicSubs); logger.debug(`[MessageBus] Subscription created`, { subscriptionId, topic, subscriberId, }); this.emitter.emit("subscription:created", { subscriptionId, topic, subscriberId, }); return subscriptionId; } /** * Unsubscribe from a topic */ unsubscribe(subscriptionId) { for (const [topic, subs] of this.subscriptions) { const index = subs.findIndex((s) => s.id === subscriptionId); if (index !== -1) { subs.splice(index, 1); if (subs.length === 0) { this.subscriptions.delete(topic); } logger.debug(`[MessageBus] Subscription removed`, { subscriptionId, topic, }); return true; } } return false; } /** * Unsubscribe all subscriptions for an agent */ unsubscribeAll(subscriberId) { let count = 0; for (const [topic, subs] of this.subscriptions) { const filtered = subs.filter((s) => s.subscriberId !== subscriberId); count += subs.length - filtered.length; if (filtered.length === 0) { this.subscriptions.delete(topic); } else { this.subscriptions.set(topic, filtered); } } return count; } /** * Publish a message to a topic */ async publish(topic, senderId, payload, options) { const message = { id: randomUUID(), type: options?.type ?? "event", topic, senderId, payload, priority: options?.priority ?? "normal", timestamp: Date.now(), ttl: options?.ttl ?? this.config.defaultTtl, recipientId: options?.recipientId, correlationId: options?.correlationId, replyTo: options?.replyTo, metadata: options?.metadata, }; await this.deliverMessage(message); } /** * Send a direct message to a specific agent */ async sendDirect(senderId, recipientId, payload, options) { const topic = `direct:${recipientId}`; const message = { id: randomUUID(), type: "direct", topic, senderId, recipientId, payload, priority: options?.priority ?? "normal", timestamp: Date.now(), ttl: options?.ttl ?? this.config.defaultTtl, metadata: options?.metadata, }; await this.deliverMessage(message); } /** * Send a request and wait for response */ async request(topic, senderId, payload, timeout) { const correlationId = randomUUID(); const replyTo = `reply:${correlationId}`; // Create a promise that will resolve when we get the response const responsePromise = new Promise((resolve, reject) => { const timeoutMs = timeout ?? this.config.requestTimeout; const timeoutHandle = setTimeout(() => { this.pendingRequests.delete(correlationId); this.unsubscribeByTopic(replyTo, senderId); reject(new Error(`Request timed out after ${timeoutMs}ms`)); }, timeoutMs); this.pendingRequests.set(correlationId, { resolve, reject, timeout: timeoutHandle, }); }); // Subscribe to reply topic this.subscribe(replyTo, senderId, (msg) => { const pending = this.pendingRequests.get(correlationId); if (pending) { clearTimeout(pending.timeout); this.pendingRequests.delete(correlationId); this.unsubscribeByTopic(replyTo, senderId); pending.resolve(msg); } }); // Send the request await this.publish(topic, senderId, payload, { type: "request", correlationId, replyTo, }); return responsePromise; } /** * Reply to a request */ async reply(originalMessage, senderId, payload) { if (!originalMessage.replyTo) { throw new Error("Cannot reply to message without replyTo field"); } await this.publish(originalMessage.replyTo, senderId, payload, { type: "response", correlationId: originalMessage.correlationId, }); } /** * Broadcast a message to all subscribers */ async broadcast(senderId, payload, excludeTopics) { const message = { id: randomUUID(), type: "broadcast", topic: "broadcast", senderId, payload, priority: "normal", timestamp: Date.now(), ttl: this.config.defaultTtl, }; // Deliver to all topics except excluded ones for (const topic of this.subscriptions.keys()) { if (excludeTopics?.includes(topic)) { continue; } if (topic.startsWith("reply:") || topic.startsWith("direct:")) { continue; } await this.deliverMessage({ ...message, topic }); } } /** * Deliver a message to subscribers */ async deliverMessage(message) { // Add to history this.messageHistory.push(message); if (this.messageHistory.length > this.config.maxHistorySize) { this.messageHistory.shift(); } // Check if message has expired if (message.ttl && Date.now() - message.timestamp > message.ttl) { logger.debug(`[MessageBus] Message expired`, { messageId: message.id }); return; } const subs = this.subscriptions.get(message.topic) ?? []; const deliveryPromises = []; for (const sub of subs) { // Check if subscription should receive this message if (!this.shouldDeliver(message, sub)) { continue; } // Check max messages limit if (sub.options.maxMessages !== undefined && sub.options.maxMessages !== -1 && sub.messageCount >= sub.options.maxMessages) { continue; } sub.messageCount++; deliveryPromises.push(Promise.resolve(sub.handler(message)).catch((error) => { logger.error(`[MessageBus] Message delivery failed`, { messageId: message.id, subscriptionId: sub.id, error: error instanceof Error ? error.message : String(error), }); // Add to dead letter queue if enabled if (this.config.enableDeadLetterQueue) { this.deadLetterQueue.push(message); } })); } await Promise.all(deliveryPromises); this.emitter.emit("message:delivered", { messageId: message.id, topic: message.topic, }); } /** * Check if message should be delivered to subscription */ shouldDeliver(message, sub) { const opts = sub.options; // Filter by sender if (opts.filterBySender && !opts.filterBySender.includes(message.senderId)) { return false; } // Filter by type if (opts.filterByType && !opts.filterByType.includes(message.type)) { return false; } // Filter by priority if (opts.filterByPriority && !opts.filterByPriority.includes(message.priority)) { return false; } // Custom filter if (opts.customFilter && !opts.customFilter(message)) { return false; } // Direct messages: check recipient if (message.type === "direct" && message.recipientId !== sub.subscriberId) { return false; } return true; } /** * Unsubscribe by topic for a specific subscriber */ unsubscribeByTopic(topic, subscriberId) { const subs = this.subscriptions.get(topic); if (subs) { const filtered = subs.filter((s) => s.subscriberId !== subscriberId); if (filtered.length === 0) { this.subscriptions.delete(topic); } else { this.subscriptions.set(topic, filtered); } } } /** * Get message history for a topic */ getHistory(topic, limit) { let messages = topic ? this.messageHistory.filter((m) => m.topic === topic) : this.messageHistory; if (limit) { messages = messages.slice(-limit); } return messages; } /** * Get dead letter queue messages */ getDeadLetterQueue() { return [...this.deadLetterQueue]; } /** * Clear dead letter queue */ clearDeadLetterQueue() { this.deadLetterQueue = []; } /** * Replay messages from history */ async replayHistory(topic, subscriberId, since) { const messages = this.messageHistory.filter((m) => m.topic === topic && (!since || m.timestamp >= since)); const subs = this.subscriptions .get(topic) ?.filter((s) => s.subscriberId === subscriberId) ?? []; for (const message of messages) { for (const sub of subs) { if (this.shouldDeliver(message, sub)) { await Promise.resolve(sub.handler(message)).catch(() => { // Ignore replay errors }); } } } } /** * Get all topics */ getTopics() { return Array.from(this.subscriptions.keys()); } /** * Get subscriber count for a topic */ getSubscriberCount(topic) { return this.subscriptions.get(topic)?.length ?? 0; } /** * Get statistics */ getStats() { let totalSubscriptions = 0; for (const subs of this.subscriptions.values()) { totalSubscriptions += subs.length; } return { topicCount: this.subscriptions.size, totalSubscriptions, historySize: this.messageHistory.length, deadLetterQueueSize: this.deadLetterQueue.length, pendingRequests: this.pendingRequests.size, }; } /** * Subscribe to bus events */ on(event, handler) { this.emitter.on(event, handler); } /** * Unsubscribe from bus events */ off(event, handler) { this.emitter.off(event, handler); } /** * Shutdown the message bus */ shutdown() { // Clear pending requests for (const [, pending] of this.pendingRequests) { clearTimeout(pending.timeout); pending.reject(new Error("Message bus shutdown")); } this.pendingRequests.clear(); // Clear subscriptions this.subscriptions.clear(); logger.debug("[MessageBus] Shutdown complete"); } }