@hotmeshio/hotmesh
Version:
Permanent-Memory Workflows & AI Agents
162 lines (161 loc) • 6.48 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.IORedisSubService = void 0;
const key_1 = require("../../../../modules/key");
const index_1 = require("../../index");
class IORedisSubService extends index_1.SubService {
constructor(eventClient, storeClient) {
super(eventClient, storeClient);
this.subscriptions = new Map();
this.messageHandlerSet = false;
this.pmessageHandlerSet = false;
}
async init(namespace = key_1.HMNS, appId, engineId, logger) {
this.namespace = namespace;
this.logger = logger;
this.appId = appId;
this.engineId = engineId;
}
transact() {
return this.eventClient.multi();
}
mintKey(type, params) {
if (!this.namespace)
throw new Error('namespace not set');
return key_1.KeyService.mintKey(this.namespace, type, params);
}
async subscribe(keyType, callback, appId, engineId) {
const topic = this.mintKey(keyType, { appId, engineId });
// Add callback to set (supports multiple subscribers per topic)
let callbacks = this.subscriptions.get(topic);
const isFirstSubscription = !callbacks;
if (!callbacks) {
callbacks = new Set();
this.subscriptions.set(topic, callbacks);
}
callbacks.add(callback);
this.logger.debug('ioredis-subscribe', {
topic,
isFirstSubscription,
totalCallbacks: callbacks.size,
messageHandlerSet: this.messageHandlerSet,
});
// Set up message handler only once
if (!this.messageHandlerSet) {
this.eventClient.on('message', (channel, message) => {
try {
const payload = JSON.parse(message);
const cbs = this.subscriptions.get(channel);
this.logger.debug('ioredis-message-received', {
channel,
callbackCount: cbs?.size || 0,
payload,
});
if (cbs) {
// Call all callbacks for this channel
cbs.forEach(cb => {
try {
cb(channel, payload);
}
catch (err) {
this.logger.error(`Error in callback for ${channel}`, err);
}
});
}
}
catch (e) {
this.logger.error(`Error parsing message: ${message}`, e);
}
});
this.messageHandlerSet = true;
}
await this.eventClient.subscribe(topic, (err) => {
if (err) {
this.logger.error(`Error subscribing to: ${topic}`, err);
}
});
}
async unsubscribe(keyType, appId, engineId) {
const topic = this.mintKey(keyType, { appId, engineId });
const callbacks = this.subscriptions.get(topic);
this.logger.debug('ioredis-unsubscribe', {
topic,
hadCallbacks: !!callbacks,
callbackCount: callbacks?.size || 0,
});
// Only unsubscribe from Redis if no more callbacks exist
if (callbacks && callbacks.size > 0) {
this.subscriptions.delete(topic);
await this.eventClient.unsubscribe(topic);
}
}
async psubscribe(keyType, callback, appId, engineId) {
const topic = this.mintKey(keyType, { appId, engineId });
// Add callback to set (supports multiple subscribers per topic)
let callbacks = this.subscriptions.get(topic);
if (!callbacks) {
callbacks = new Set();
this.subscriptions.set(topic, callbacks);
}
callbacks.add(callback);
// Set up pmessage handler only once
if (!this.pmessageHandlerSet) {
this.eventClient.on('pmessage', (pattern, channel, message) => {
try {
const payload = JSON.parse(message);
const cbs = this.subscriptions.get(pattern);
if (cbs) {
// Call all callbacks for this pattern
cbs.forEach(cb => {
try {
cb(channel, payload);
}
catch (err) {
this.logger.error(`Error in callback for ${pattern}`, err);
}
});
}
}
catch (e) {
this.logger.error(`Error parsing message: ${message}`, e);
}
});
this.pmessageHandlerSet = true;
}
await this.eventClient.psubscribe(topic, (err) => {
if (err) {
this.logger.error(`Error subscribing to: ${topic}`, err);
}
});
}
async punsubscribe(keyType, appId, engineId) {
const topic = this.mintKey(keyType, { appId, engineId });
const callbacks = this.subscriptions.get(topic);
// Only unsubscribe from Redis if no more callbacks exist
if (callbacks && callbacks.size > 0) {
this.subscriptions.delete(topic);
await this.eventClient.punsubscribe(topic);
}
}
async publish(keyType, message, appId, engineId) {
const topic = this.mintKey(keyType, { appId, engineId });
//NOTE: `storeClient.publish` is used,
// because a Redis connection with subscriptions
// may not publish (is read only).
this.logger.debug('ioredis-publish', { topic, message });
try {
const status = await this.storeClient.publish(topic, JSON.stringify(message));
return status === 1;
}
catch (error) {
// Connection closed during test cleanup - log and continue gracefully
if (error?.message?.includes('Connection is closed')) {
this.logger.debug('ioredis-publish-connection-closed', { topic, message: message.type });
return false;
}
// Re-throw unexpected errors
throw error;
}
}
}
exports.IORedisSubService = IORedisSubService;