@hotmeshio/hotmesh
Version:
Permanent-Memory Workflows & AI Agents
149 lines (148 loc) • 6.12 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RedisSubService = void 0;
const key_1 = require("../../../../modules/key");
const index_1 = require("../../index");
class RedisSubService extends index_1.SubService {
constructor(eventClient, storeClient) {
super(eventClient, storeClient);
this.subscriptions = new Map();
}
async init(namespace = key_1.HMNS, appId, engineId, logger) {
this.namespace = namespace;
this.logger = logger;
this.appId = appId;
this.engineId = engineId;
}
transact() {
const multi = this.eventClient.multi();
return 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) {
if (this.eventClient) {
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('redis-subscribe', {
topic,
isFirstSubscription,
totalCallbacks: callbacks.size,
});
// Only subscribe to Redis once per topic
if (isFirstSubscription) {
await this.eventClient.subscribe(topic, (message) => {
try {
const payload = JSON.parse(message);
const cbs = this.subscriptions.get(topic);
this.logger.debug('redis-message-received', {
topic,
callbackCount: cbs?.size || 0,
payload,
});
if (cbs) {
// Call all callbacks for this topic
cbs.forEach(cb => {
try {
cb(topic, payload);
}
catch (err) {
this.logger.error(`Error in callback for ${topic}`, err);
}
});
}
}
catch (e) {
this.logger.error(`Error parsing message: ${message}`, e);
}
});
}
}
}
async unsubscribe(keyType, appId, engineId) {
const topic = this.mintKey(keyType, { appId, engineId });
const callbacks = this.subscriptions.get(topic);
this.logger.debug('redis-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) {
if (this.eventClient) {
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);
await this.eventClient.pSubscribe(topic, (message, channel) => {
try {
const payload = JSON.parse(message);
const cbs = this.subscriptions.get(topic);
if (cbs) {
// Call all callbacks for this topic
cbs.forEach(cb => {
try {
cb(channel, payload);
}
catch (err) {
this.logger.error(`Error in callback for ${topic}`, err);
}
});
}
}
catch (e) {
this.logger.error(`Error parsing message: ${message}`, e);
}
});
}
}
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('redis-publish', { topic, message });
try {
const status = await this.storeClient.publish(topic, JSON.stringify(message));
return status > 0;
}
catch (error) {
// Connection closed during test cleanup - log and continue gracefully
if (error?.message?.includes('Connection is closed') || error?.message?.includes('connection closed')) {
this.logger.debug('redis-publish-connection-closed', { topic, message: message.type });
return false;
}
// Re-throw unexpected errors
throw error;
}
}
}
exports.RedisSubService = RedisSubService;