UNPKG

@hotmeshio/hotmesh

Version:

Permanent-Memory Workflows & AI Agents

162 lines (161 loc) 6.48 kB
"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;