@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
JavaScript
/**
* 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");
}
}