@gravityai-dev/gravity-server
Version:
Integration SDK for the Gravity AI orchestration platform - Connect any AI platform in minutes
1 lines • 81.2 kB
Source Map (JSON)
{"version":3,"sources":["../src/types.ts","../src/RedisManager.ts","../src/messaging/Publisher.ts","../src/messaging/SimpleEventBus.ts","../src/messaging/publishers/base.ts","../src/messaging/publishers/progressUpdate.ts","../src/messaging/publishers/messageChunk.ts","../src/messaging/publishers/text.ts","../src/messaging/publishers/jsonData.ts","../src/messaging/publishers/audioChunk.ts","../src/messaging/publishers/state.ts","../src/messaging/publishers/system.ts","../src/messaging/publishers/cards.ts","../src/messaging/publishers/questions.ts","../src/messaging/publishers/forms.ts","../src/messaging/publishers/batch.ts","../src/messaging/publishers/nodeExecution.ts"],"sourcesContent":["/**\n * Shared Types\n *\n * Core types shared between client and server\n *\n * @module shared/types\n */\n\nimport { v4 as uuid } from \"uuid\";\n\n// Re-export message types from messaging module\nexport type {\n BaseMessage,\n Text,\n JsonData,\n ActionSuggestion,\n Metadata,\n ImageResponse,\n ToolOutput,\n AudioChunk,\n MessageChunk,\n ProgressUpdate,\n SystemMessage,\n State,\n GravityMessage,\n Card,\n Questions,\n Form,\n} from \"./messaging/types\";\n\n// Import BaseMessage for ServerMessage interface\nimport type { BaseMessage } from \"./messaging/types\";\n\n// Message type enum\nexport enum MessageType {\n TEXT = \"TEXT\",\n JSON_DATA = \"JSON_DATA\",\n IMAGE_RESPONSE = \"IMAGE_RESPONSE\",\n TOOL_OUTPUT = \"TOOL_OUTPUT\",\n ACTION_SUGGESTION = \"ACTION_SUGGESTION\",\n METADATA = \"METADATA\",\n AUDIO_CHUNK = \"AUDIO_CHUNK\",\n SYSTEM_MESSAGE = \"SYSTEM_MESSAGE\",\n PROGRESS_UPDATE = \"PROGRESS_UPDATE\",\n MESSAGE_CHUNK = \"MESSAGE_CHUNK\",\n STATE = \"STATE\",\n CARD = \"CARD\",\n QUESTIONS = \"QUESTIONS\",\n FORM = \"FORM\",\n NODE_EXECUTION_EVENT = \"NODE_EXECUTION_EVENT\",\n}\n\n// Chat state enum\nexport enum ChatState {\n IDLE = \"IDLE\",\n ACTIVE = \"ACTIVE\",\n COMPLETE = \"COMPLETE\",\n THINKING = \"THINKING\",\n RESPONDING = \"RESPONDING\",\n WAITING = \"WAITING\",\n ERROR = \"ERROR\",\n CANCELLED = \"CANCELLED\",\n}\n\n// Server-side message format (includes both type and __typename)\nexport interface ServerMessage extends Omit<BaseMessage, \"timestamp\"> {\n id: string;\n providerId: string;\n timestamp: number; // Server uses number timestamps\n type: MessageType;\n __typename: string;\n}\n\n// Channel constants\nexport const SYSTEM_CHANNEL = \"gravity:system\";\nexport const AI_RESULT_CHANNEL = \"gravity:output\";\nexport const QUERY_MESSAGE_CHANNEL = \"gravity:query\";\nexport const INTERNAL_REQUEST_CHANNEL = \"gravity:internal\"; // For internal service-to-service requests\nexport const WORKFLOW_EXECUTION_CHANNEL = \"workflow:execution\"; // For workflow execution events\n// WORKFLOW_NODE_COMPLETION_CHANNEL removed - now using unified AI_RESULT_CHANNEL\nexport const WORKFLOW_STATE_CHANNEL = \"gravity:workflow:state\"; // For workflow state debug updates\n\n// Timeout constants\nexport const TIMEOUTS = {\n DEFAULT: 5000,\n REQUEST: 10000,\n} as const;\n\n// Mapping from MessageType to GraphQL __typename\nexport const TYPE_TO_TYPENAME: Record<MessageType, string> = {\n [MessageType.TEXT]: \"Text\",\n [MessageType.MESSAGE_CHUNK]: \"MessageChunk\",\n [MessageType.JSON_DATA]: \"JsonData\",\n [MessageType.ACTION_SUGGESTION]: \"ActionSuggestion\",\n [MessageType.METADATA]: \"Metadata\",\n [MessageType.IMAGE_RESPONSE]: \"ImageResponse\",\n [MessageType.TOOL_OUTPUT]: \"ToolOutput\",\n [MessageType.AUDIO_CHUNK]: \"AudioChunk\",\n [MessageType.STATE]: \"State\",\n [MessageType.SYSTEM_MESSAGE]: \"SystemMessage\",\n [MessageType.PROGRESS_UPDATE]: \"ProgressUpdate\",\n [MessageType.CARD]: \"Card\",\n [MessageType.QUESTIONS]: \"Questions\",\n [MessageType.FORM]: \"Form\",\n [MessageType.NODE_EXECUTION_EVENT]: \"NodeExecutionEvent\",\n};\n","/**\n * Redis Connection Manager\n *\n * Centralizes Redis connection management to avoid redundant connections.\n * Maintains separate pools for standard and pub/sub connections.\n */\n\nimport Redis from \"ioredis\";\n\n// Connection pools\nconst standardConnections = new Map<string, Redis>();\nconst pubsubConnections = new Map<string, Redis>();\n\n/**\n * Redis connection options with standardized fields\n */\nexport interface RedisOptions {\n host: string;\n port: number;\n password?: string;\n username?: string;\n db?: number;\n // Add any other common Redis options here\n}\n\n/**\n * Get a unique key based on connection options\n */\nfunction getConnectionKey(options: RedisOptions): string {\n return `${options.host}:${options.port}:${options.db || 0}:${options.username || \"\"}`;\n}\n\n/**\n * Create a Redis configuration with sensible defaults\n */\nfunction createConfig(options: RedisOptions): any {\n return {\n host: options.host,\n port: options.port,\n password: options.password,\n username: options.username,\n db: options.db || 0,\n retryStrategy: (times: number) => Math.min(times * 50, 2000),\n maxRetriesPerRequest: 3,\n enableOfflineQueue: true,\n // Add other common options here\n };\n}\n\n/**\n * Get a Redis connection for standard commands\n * Reuses existing connections when possible\n */\nexport function getStandardConnection(options: RedisOptions): Redis {\n // Create a unique key for the connection pool based on connection parameters\n const connectionKey = `${options.host}:${options.port}:${options.db || 0}:${options.username || 'default'}`;\n \n if (standardConnections.has(connectionKey)) {\n return standardConnections.get(connectionKey)!;\n }\n\n // Create config object for ioredis\n const config = {\n host: options.host,\n port: options.port,\n username: options.username,\n password: options.password,\n db: options.db || 0,\n retryStrategy: (times: number) => Math.min(times * 50, 2000),\n maxRetriesPerRequest: 3,\n enableOfflineQueue: true,\n };\n\n const client = new Redis(config);\n standardConnections.set(connectionKey, client);\n \n return client;\n}\n\n/**\n * Get a dedicated Redis connection for pub/sub operations\n * Always creates a new connection for pub/sub to avoid conflicts\n */\nexport function getPubSubConnection(options: RedisOptions): Redis {\n const connectionKey = `${options.host}:${options.port}:${options.db || 0}:${options.username || 'default'}`;\n \n if (pubsubConnections.has(connectionKey)) {\n return pubsubConnections.get(connectionKey)!;\n }\n\n const config = createConfig(options);\n const client = new Redis(config);\n pubsubConnections.set(connectionKey, client);\n \n return client;\n}\n\n/**\n * Get Redis options from environment variables\n */\nexport function getRedisOptions(): RedisOptions {\n return {\n host: process.env.REDIS_HOST!,\n port: parseInt(process.env.REDIS_PORT!, 10),\n username: process.env.REDIS_USERNAME!,\n password: process.env.REDIS_PASSWORD!,\n };\n}\n\n/**\n * Create RedisOptions from server config values\n * Preferred method for proper Redis configuration\n */\nexport function getOptionsFromConfig(\n host: string,\n port: number,\n username?: string | null,\n password?: string | null\n): RedisOptions {\n return {\n host,\n port,\n username: username || undefined,\n password: password || undefined,\n };\n}\n\n/**\n * Close all connections in the pool\n * Useful for cleanup or tests\n */\nexport async function closeAllConnections(): Promise<void> {\n const closePromises: Promise<string>[] = [];\n\n standardConnections.forEach((client, key) => {\n closePromises.push(\n client.quit().then(() => {\n standardConnections.delete(key);\n return `Standard connection ${key}`;\n })\n );\n });\n\n pubsubConnections.forEach((client, key) => {\n closePromises.push(\n client.quit().then(() => {\n pubsubConnections.delete(key);\n return `PubSub connection ${key}`;\n })\n );\n });\n\n await Promise.all(closePromises);\n}\n\n// Re-export Redis class for convenience\nexport { Redis };\n","/**\n * Publisher for Gravity AI\n *\n * Handles publishing messages to Redis channels.\n * This is the client-facing publisher that external services use.\n *\n * Key features:\n * - Simple API for publishing to channels\n * - Automatic connection management\n * - Support for Redis credentials-based initialization\n *\n * Usage:\n * ```typescript\n * const publisher = Publisher.fromRedisCredentials({\n * host: 'localhost',\n * port: 6379,\n * password: 'your-password'\n * }, 'my-service');\n * await publisher.publishEvent('channel', { data: 'hello' });\n * ```\n */\n\nimport Redis from \"ioredis\";\nimport { RedisOptions, getStandardConnection, getOptionsFromConfig } from \"../RedisManager\";\nimport { SYSTEM_CHANNEL } from \"../types\";\n\nexport class Publisher {\n private redis: Redis;\n private providerId: string;\n\n constructor(options: RedisOptions, providerId: string) {\n // Use connection pooling from RedisManager\n this.redis = getStandardConnection(options);\n this.providerId = providerId;\n }\n\n static fromRedisCredentials(redisOptions: RedisOptions, providerId: string): Publisher {\n return new Publisher(redisOptions, providerId);\n }\n\n static fromConfig(\n host: string,\n port: number,\n password: string | undefined,\n providerId: string,\n username?: string,\n db?: number\n ): Publisher {\n const redisOptions = getOptionsFromConfig(host, port, username, password);\n\n return new Publisher(redisOptions, providerId);\n }\n\n getProviderId(): string {\n return this.providerId;\n }\n\n getRedisConnection(): Redis {\n return this.redis;\n }\n\n /**\n * Publish system-level events\n * Used by EventBus and system services\n */\n async publishSystem(message: any): Promise<void> {\n await this.redis.publish(SYSTEM_CHANNEL, JSON.stringify(message));\n }\n\n /**\n * Publish to arbitrary event channels\n * Used by EventBus, n8n resolver, and health monitor\n */\n async publishEvent(eventType: string, payload: any): Promise<void> {\n await this.redis.publish(eventType, JSON.stringify(payload));\n }\n\n async disconnect(): Promise<void> {\n // Don't close shared connections - they're managed by RedisManager\n // Just clear our reference\n this.redis = null as any;\n }\n}\n","/**\n * Simple Event Bus for Gravity AI\n */\n\nimport { v4 as uuid } from \"uuid\";\nimport Redis from \"ioredis\";\nimport { RedisOptions, getPubSubConnection } from \"../RedisManager\";\nimport { GravityMessage } from \"../types\";\nimport { Publisher } from \"./Publisher\";\n\nexport type EventHandler<T = any> = (event: T) => void | Promise<void>;\n\nexport class EventBus {\n private publisher: Publisher;\n private subscriber: Redis;\n private handlers = new Map<string, Set<EventHandler>>();\n\n constructor(private options: RedisOptions, private serviceId: string) {\n this.publisher = new Publisher(options, serviceId);\n this.subscriber = getPubSubConnection(options);\n this.setupSubscriber();\n }\n\n static fromRedisConfig(host: string, port: number, password: string | undefined, serviceId: string, username?: string, db?: number): EventBus {\n const options = {\n host,\n port,\n password,\n username,\n db: db || 0,\n };\n return new EventBus(options, serviceId);\n }\n\n static fromCredentials(host: string, port: number, password: string | undefined, serviceId: string): EventBus {\n return EventBus.fromRedisConfig(host, port, password, serviceId);\n }\n\n private setupSubscriber(): void {\n this.subscriber.on(\"message\", (channel: string, message: string) => {\n const handlers = this.handlers.get(channel);\n if (!handlers || handlers.size === 0) return;\n\n try {\n const event = JSON.parse(message);\n handlers.forEach(handler => {\n try {\n handler(event);\n } catch (error) {\n console.error(`[EventBus] Handler error on channel ${channel}:`, error);\n }\n });\n } catch (error) {\n console.error(`[EventBus] Failed to parse message on channel ${channel}:`, error);\n }\n });\n }\n\n async publish(channel: string, payload: any): Promise<void> {\n // Use the event channel pattern from Publisher\n await this.publisher.publishEvent(channel, payload);\n }\n\n async subscribe<T = any>(channel: string, handler: EventHandler<T>): Promise<() => Promise<void>> {\n if (!this.handlers.has(channel)) {\n this.handlers.set(channel, new Set());\n await this.subscriber.subscribe(channel);\n }\n\n this.handlers.get(channel)!.add(handler);\n\n return async () => {\n const handlers = this.handlers.get(channel);\n if (!handlers) return;\n\n handlers.delete(handler);\n if (handlers.size === 0) {\n this.handlers.delete(channel);\n await this.subscriber.unsubscribe(channel);\n }\n };\n }\n\n async disconnect(): Promise<void> {\n await Promise.all([\n this.publisher.disconnect(),\n this.subscriber.quit()\n ]);\n }\n}\n","/**\n * Base publisher functionality and types\n * \n * This module provides the foundation for all publisher classes in the messaging system.\n * It includes the BasePublisher abstract class which handles core Redis connection\n * management and message publishing functionality.\n * \n * @module messaging/publishers/base\n */\n\nimport Redis from \"ioredis\";\nimport { v4 as uuid } from \"uuid\";\nimport { GravityMessage, MessageType, AI_RESULT_CHANNEL, SYSTEM_CHANNEL, BaseMessage } from \"../../types\";\n\n/**\n * Options for publishing messages\n * \n * @interface PublishOptions\n * @property {string} [channel] - Optional Redis channel to publish to. If not provided,\n * the publisher will use a default channel based on context.\n * @property {boolean} [useStream] - Whether to use Redis Streams for guaranteed delivery\n * \n * @example\n * ```typescript\n * // Publish to a specific channel\n * const options: PublishOptions = {\n * channel: \"gravity:output\"\n * };\n * ```\n */\nexport interface PublishOptions {\n channel?: string;\n useStream?: boolean;\n}\n\n/**\n * Base publisher class with core functionality\n * \n * This abstract class provides the foundation for all specialized publishers.\n * It manages the Redis connection, provider ID, and core publishing logic.\n * \n * Key responsibilities:\n * - Maintain Redis connection for publishing\n * - Store and provide access to provider ID\n * - Create base messages with required fields\n * - Publish messages to Redis channels\n * \n * @abstract\n * @class BasePublisher\n * \n * @example\n * ```typescript\n * // Extend BasePublisher to create a specialized publisher\n * class MyPublisher extends BasePublisher {\n * async publishCustomMessage(data: any): Promise<void> {\n * const message = {\n * ...this.createBaseMessage({ \n * chatId: \"chat123\",\n * conversationId: \"conv456\",\n * userId: \"user789\"\n * }),\n * data\n * };\n * await this.publish(message, \"my:channel\");\n * }\n * }\n * \n * // Use the publisher\n * const redis = new Redis();\n * const publisher = new MyPublisher(redis, \"my-service\");\n * await publisher.publishCustomMessage({ foo: \"bar\" });\n * ```\n */\nexport abstract class BasePublisher {\n /**\n * Redis connection used for publishing messages\n * @protected\n */\n protected redis: Redis;\n\n /**\n * Provider ID identifying the service using this publisher\n * @protected\n */\n protected providerId: string;\n\n /**\n * Creates a new BasePublisher instance\n * \n * @param {Redis} redis - Redis connection instance for publishing\n * @param {string} providerId - Unique identifier for the service/provider\n * \n * @example\n * ```typescript\n * const redis = new Redis({\n * host: \"localhost\",\n * port: 6379\n * });\n * const publisher = new MyPublisher(redis, \"my-service\");\n * ```\n */\n constructor(redis: Redis, providerId: string) {\n this.redis = redis;\n this.providerId = providerId;\n }\n\n /**\n * Gets the provider ID for the publisher\n * \n * This method is used by other components (like EventBus) to access\n * the provider ID when creating related instances.\n * \n * @returns {string} The provider ID\n * \n * @example\n * ```typescript\n * const providerId = publisher.getProviderId();\n * console.log(`Publisher provider: ${providerId}`);\n * ```\n */\n getProviderId(): string {\n return this.providerId;\n }\n\n /**\n * Gets the Redis connection for the publisher\n * \n * This method exposes the Redis connection for use by other components\n * that need to create their own connections with the same configuration.\n * \n * @returns {Redis} The Redis connection instance\n * \n * @example\n * ```typescript\n * const redis = publisher.getRedisConnection();\n * const options = {\n * host: redis.options.host,\n * port: redis.options.port\n * };\n * ```\n */\n getRedisConnection(): Redis {\n return this.redis;\n }\n\n /**\n * Creates a base message with the given partial data\n * \n * This method constructs a complete BaseMessage by merging provided fields\n * with defaults. It ensures all required fields are present and validates\n * that chatId, conversationId, and userId are provided.\n * \n * @protected\n * @param {Partial<BaseMessage>} partial - Partial message data to merge with defaults\n * @returns {BaseMessage} Complete base message with all required fields\n * @throws {Error} If chatId, conversationId, or userId are missing\n * \n * @example\n * ```typescript\n * const baseMessage = this.createBaseMessage({\n * chatId: \"chat123\",\n * conversationId: \"conv456\",\n * userId: \"user789\",\n * type: MessageType.TEXT\n * });\n * // Result: {\n * // id: \"generated-uuid\",\n * // chatId: \"chat123\",\n * // conversationId: \"conv456\", \n * // userId: \"user789\",\n * // providerId: \"my-service\",\n * // timestamp: \"2023-12-08T10:30:00.000Z\",\n * // type: MessageType.TEXT\n * // }\n * ```\n */\n protected createBaseMessage(partial: Partial<BaseMessage>): BaseMessage {\n if (!partial.chatId || !partial.conversationId || !partial.userId) {\n throw new Error(\"chatId, conversationId, and userId are required\");\n }\n return {\n id: partial.id || uuid(),\n chatId: partial.chatId,\n conversationId: partial.conversationId,\n userId: partial.userId,\n providerId: partial.providerId || this.providerId,\n timestamp: partial.timestamp || new Date().toISOString(),\n type: partial.type || MessageType.TEXT,\n };\n }\n\n /**\n * Publishes a message to the given channel\n * \n * This method serializes the message to JSON and publishes it to the\n * specified Redis channel. This is the core publishing mechanism used\n * by all specialized publishers.\n * \n * @protected\n * @param {GravityMessage} message - The message object to publish\n * @param {PublishOptions} [options] - Optional publishing options\n * @returns {Promise<void>} Promise that resolves when message is published\n * \n * @example\n * ```typescript\n * const message = {\n * __typename: \"Text\",\n * text: \"Hello, world!\",\n * ...baseMessage\n * };\n * await this.publish(message, { channel: \"custom:channel\" });\n * ```\n */\n protected async publish(\n message: GravityMessage,\n options?: PublishOptions\n ): Promise<void> {\n const channel = options?.channel || AI_RESULT_CHANNEL;\n \n // Always use Redis Streams for reliability\n await this.publishToStream(channel, message);\n }\n\n /**\n * Publishes a message to Redis Streams for guaranteed delivery\n * \n * @protected\n * @param {string} channel - The channel name\n * @param {GravityMessage} message - The message to publish\n * @returns {Promise<string>} The stream entry ID\n */\n private async publishToStream(channel: string, message: any): Promise<string> {\n // Use the unified workflow stream for all messages\n const streamKey = \"workflow:events:stream\";\n \n try {\n // Extract conversationId for efficient filtering\n const conversationId = message.conversationId || \"\";\n \n // Publish to Redis Stream\n const entryId = await this.redis.xadd(\n streamKey,\n \"*\", // Auto-generate ID\n \"channel\", channel,\n \"conversationId\", conversationId, // Add for Redis Streams filtering\n \"message\", JSON.stringify(message),\n \"timestamp\", Date.now().toString(),\n \"providerId\", this.providerId\n );\n \n if (!entryId) {\n console.error(`[BasePublisher] Failed to add entry to stream ${streamKey} - no entry ID returned`);\n }\n \n // Also publish to regular pub/sub for backward compatibility\n // This ensures existing subscribers still receive messages\n await this.redis.publish(channel, JSON.stringify(message));\n \n return entryId || '';\n } catch (error) {\n console.error(`[BasePublisher] Error publishing to stream ${streamKey}:`, error);\n \n // Fallback to pub/sub only\n try {\n await this.redis.publish(channel, JSON.stringify(message));\n console.warn(`[BasePublisher] Fell back to pub/sub only for channel ${channel}`);\n } catch (pubsubError) {\n console.error(`[BasePublisher] Failed to publish to pub/sub as well:`, pubsubError);\n throw pubsubError;\n }\n \n return '';\n }\n }\n}\n","/**\n * Progress update publisher\n *\n * Simple, focused publisher for progress update messages.\n *\n * @module messaging/publishers/progress\n */\n\nimport { BaseMessage } from \"../types\";\nimport { BasePublisher, PublishOptions } from \"./base\";\nimport { Publisher } from \"../Publisher\";\n\n/**\n * Progress update message type\n */\nexport interface ProgressUpdate extends BaseMessage {\n __typename: \"ProgressUpdate\";\n component: {\n type: \"ProgressUpdate\";\n props: {\n message: string;\n progress?: number;\n };\n };\n}\n\n/**\n * ProgressPublisher - Handles progress update messages\n */\nexport class ProgressPublisher extends BasePublisher {\n /**\n * Publishes a progress update\n *\n * @param message - The progress message text\n * @param progress - Optional progress percentage (0-100)\n * @param baseMessage - Base message with required fields (chatId, conversationId, userId)\n * @param options - Optional publishing options (e.g., custom channel)\n */\n async publishProgressUpdate(\n message: string,\n progress: number | undefined,\n baseMessage: Partial<BaseMessage>,\n options?: PublishOptions\n ): Promise<void> {\n const progressUpdate: ProgressUpdate = {\n ...this.createBaseMessage(baseMessage),\n __typename: \"ProgressUpdate\",\n component: {\n type: \"ProgressUpdate\",\n props: {\n message,\n progress,\n },\n },\n };\n\n await this.publish(progressUpdate as any, options);\n }\n}\n\n// Singleton instance for maximum performance\nlet progressPublisherInstance: ProgressPublisher | null = null;\n\n/**\n * Get singleton ProgressPublisher instance\n * Maximum performance - no new objects created after first call\n * \n * @param host - Redis host (required on first call)\n * @param port - Redis port (required on first call)\n * @param password - Redis password (required on first call)\n * @param providerId - Provider ID (required on first call)\n * @param username - Redis username (optional)\n * @param db - Redis database number (optional)\n * @returns Singleton ProgressPublisher instance\n */\nexport function getProgressPublisher(\n host?: string, \n port?: number, \n password?: string, \n providerId?: string, \n username?: string, \n db?: number\n): ProgressPublisher {\n if (!progressPublisherInstance) {\n if (!host || !port || password === undefined || !providerId) {\n throw new Error('ProgressPublisher requires host, port, password, and providerId on first call');\n }\n \n const publisher = Publisher.fromConfig(host, port, password, providerId, username, db);\n progressPublisherInstance = new ProgressPublisher(\n publisher.getRedisConnection(),\n publisher.getProviderId()\n );\n }\n\n return progressPublisherInstance;\n}\n","/**\n * MessageChunk publisher\n *\n * Handles publishing of message chunk updates to Redis channels\n */\n\nimport { BasePublisher, PublishOptions } from \"./base\";\nimport { BaseMessage } from \"../types\";\nimport { Publisher } from \"../Publisher\";\n\n/**\n * MessageChunk message type\n */\nexport interface MessageChunk extends BaseMessage {\n __typename: \"MessageChunk\";\n component: {\n type: \"MessageChunk\";\n props: {\n text: string;\n index?: number;\n };\n };\n}\n\n/**\n * Publisher for message chunks\n */\nexport class MessageChunkPublisher extends BasePublisher {\n /**\n * Publishes a message chunk\n *\n * @param text - The text content of the chunk\n * @param baseMessage - Base message with required fields (chatId, conversationId, userId)\n * @param index - Optional sequence index for ordering\n * @param options - Optional publishing options (e.g., custom channel)\n */\n async publishMessageChunk(\n text: string,\n baseMessage: Partial<BaseMessage>,\n index?: number,\n options?: PublishOptions\n ): Promise<void> {\n // No text validation - allow all characters including spaces, newlines, and markdown formatting\n // Only skip if text is null or undefined (TypeScript should prevent this anyway)\n\n const messageChunk: MessageChunk = {\n ...this.createBaseMessage(baseMessage),\n __typename: \"MessageChunk\",\n component: {\n type: \"MessageChunk\",\n props: {\n text,\n index,\n },\n },\n };\n\n await this.publish(messageChunk as any, options);\n }\n}\n\n// Singleton instance for maximum performance\nlet messageChunkPublisherInstance: MessageChunkPublisher | null = null;\n\n/**\n * Get singleton MessageChunkPublisher instance\n * Maximum performance - no new objects created after first call\n *\n * @param host - Redis host (required on first call)\n * @param port - Redis port (required on first call)\n * @param password - Redis password (required on first call)\n * @param providerId - Provider ID (required on first call)\n * @param username - Redis username (optional)\n * @param db - Redis database number (optional)\n * @returns Singleton MessageChunkPublisher instance\n */\nexport function getMessageChunkPublisher(\n host?: string,\n port?: number,\n password?: string,\n providerId?: string,\n username?: string,\n db?: number\n): MessageChunkPublisher {\n if (!messageChunkPublisherInstance) {\n if (!host || !port || password === undefined || !providerId) {\n console.error(\"[ERROR] MessageChunkPublisher missing required parameters:\", {\n hasHost: !!host,\n hasPort: !!port,\n hasPassword: password !== undefined,\n hasProviderId: !!providerId,\n });\n throw new Error(\"MessageChunkPublisher requires host, port, password, and providerId on first call\");\n }\n\n console.log(\"[DEBUG] Creating new MessageChunkPublisher instance with Redis config:\", {\n host,\n port,\n providerId,\n });\n\n const publisher = Publisher.fromConfig(host, port, password, providerId, username, db);\n messageChunkPublisherInstance = new MessageChunkPublisher(\n publisher.getRedisConnection(),\n publisher.getProviderId()\n );\n } else {\n console.log(\"[DEBUG] Returning existing MessageChunkPublisher instance\");\n }\n\n return messageChunkPublisherInstance;\n}\n","/**\n * Text message publisher\n *\n * @module messaging/publishers/text\n */\n\nimport { BaseMessage } from \"../types\";\nimport { BasePublisher, PublishOptions } from \"./base\";\nimport { Publisher } from \"../Publisher\";\n\n/**\n * Text message type\n */\nexport interface Text extends BaseMessage {\n __typename: \"Text\";\n component: {\n type: \"Text\";\n props: {\n text: string;\n };\n };\n}\n\n/**\n * TextPublisher - Handles text messages\n */\nexport class TextPublisher extends BasePublisher {\n /**\n * Publishes a text message\n *\n * @param text - The text content\n * @param baseMessage - Base message with required fields (chatId, conversationId, userId)\n * @param options - Optional publishing options (e.g., custom channel)\n */\n async publishText(\n text: string,\n baseMessage: Partial<BaseMessage>,\n options?: PublishOptions\n ): Promise<void> {\n const textMessage: Text = {\n ...this.createBaseMessage(baseMessage),\n __typename: \"Text\",\n component: {\n type: \"Text\",\n props: {\n text,\n },\n },\n };\n\n await this.publish(textMessage, options);\n }\n}\n\n// Singleton instance for maximum performance\nlet textPublisherInstance: TextPublisher | null = null;\n\n/**\n * Get singleton TextPublisher instance\n * Maximum performance - no new objects created after first call\n * \n * @param host - Redis host (required on first call)\n * @param port - Redis port (required on first call)\n * @param password - Redis password (required on first call)\n * @param providerId - Provider ID (required on first call)\n * @param username - Redis username (optional)\n * @param db - Redis database number (optional)\n * @returns Singleton TextPublisher instance\n */\nexport function getTextPublisher(\n host?: string, \n port?: number, \n password?: string, \n providerId?: string, \n username?: string, \n db?: number\n): TextPublisher {\n if (!textPublisherInstance) {\n if (!host || !port || password === undefined || !providerId) {\n throw new Error('TextPublisher requires host, port, password, and providerId on first call');\n }\n \n const publisher = Publisher.fromConfig(host, port, password, providerId, username, db);\n textPublisherInstance = new TextPublisher(\n publisher.getRedisConnection(),\n publisher.getProviderId()\n );\n }\n\n return textPublisherInstance;\n}\n","/**\n * JSON data message publisher\n *\n * @module messaging/publishers/jsonData\n */\n\nimport { BaseMessage } from \"../types\";\nimport { BasePublisher, PublishOptions } from \"./base\";\nimport { Publisher } from \"../Publisher\";\n\n/**\n * JSON data message type\n */\nexport interface JsonData extends BaseMessage {\n __typename: \"JsonData\";\n component: {\n type: \"JsonData\";\n props: {\n data: any;\n };\n };\n}\n\n/**\n * JsonDataPublisher - Handles JSON data messages\n */\nexport class JsonDataPublisher extends BasePublisher {\n /**\n * Publishes a JSON data message\n *\n * @param data - The JSON data to publish\n * @param baseMessage - Base message with required fields (chatId, conversationId, userId)\n * @param options - Optional publishing options (e.g., custom channel)\n */\n async publishJsonData(\n data: any,\n baseMessage: Partial<BaseMessage>,\n options?: PublishOptions\n ): Promise<void> {\n const jsonMessage: JsonData = {\n ...this.createBaseMessage(baseMessage),\n __typename: \"JsonData\",\n component: {\n type: \"JsonData\",\n props: {\n data,\n },\n },\n };\n\n await this.publish(jsonMessage as any, options);\n }\n}\n\n// Singleton instance for maximum performance\nlet jsonDataPublisherInstance: JsonDataPublisher | null = null;\n\n/**\n * Get singleton JsonDataPublisher instance\n * Maximum performance - no new objects created after first call\n * \n * @param host - Redis host (required on first call)\n * @param port - Redis port (required on first call)\n * @param password - Redis password (required on first call)\n * @param providerId - Provider ID (required on first call)\n * @param username - Redis username (optional)\n * @param db - Redis database number (optional)\n * @returns Singleton JsonDataPublisher instance\n */\nexport function getJsonDataPublisher(\n host?: string, \n port?: number, \n password?: string, \n providerId?: string, \n username?: string, \n db?: number\n): JsonDataPublisher {\n if (!jsonDataPublisherInstance) {\n if (!host || !port || password === undefined || !providerId) {\n throw new Error('JsonDataPublisher requires host, port, password, and providerId on first call');\n }\n \n const publisher = Publisher.fromConfig(host, port, password, providerId, username, db);\n jsonDataPublisherInstance = new JsonDataPublisher(\n publisher.getRedisConnection(),\n publisher.getProviderId()\n );\n }\n\n return jsonDataPublisherInstance;\n}\n","/**\n * Audio chunk message publisher\n *\n * @module messaging/publishers/audioChunk\n */\n\nimport { BaseMessage } from \"../types\";\nimport { BasePublisher, PublishOptions } from \"./base\";\nimport { Publisher } from \"../Publisher\";\n\n/**\n * Audio chunk message type\n */\nexport interface AudioChunk extends BaseMessage {\n __typename: \"AudioChunk\";\n component: {\n type: \"AudioChunk\";\n props: {\n audioData: string; // Base64 encoded audio data\n format: string; // Audio format (mp3, wav, etc)\n duration?: number; // Duration in seconds\n textReference: string; // The text this audio represents\n sourceType: string; // \"MessageChunk\" or \"ProgressUpdate\"\n index?: number; // Optional index for ordering\n };\n };\n}\n\n/**\n * Helper function to create an AudioChunk object without publishing it\n */\nexport function createAudioChunk(\n base: BaseMessage,\n audioData: string,\n format: string,\n textReference: string,\n sourceType: string,\n duration?: number,\n index?: number\n): AudioChunk {\n return {\n ...base,\n __typename: \"AudioChunk\",\n component: {\n type: \"AudioChunk\",\n props: {\n audioData,\n format,\n textReference,\n sourceType,\n duration,\n index,\n },\n },\n };\n}\n\n/**\n * AudioChunkPublisher - Handles audio chunk messages\n */\nexport class AudioChunkPublisher extends BasePublisher {\n /**\n * Publishes an audio chunk message\n *\n * @param audioData - Base64 encoded audio data\n * @param format - Audio format (e.g., 'mp3', 'wav')\n * @param textReference - The text this audio represents\n * @param sourceType - Source type (\"MessageChunk\" or \"ProgressUpdate\")\n * @param duration - Optional duration in seconds\n * @param index - Optional index for ordering\n * @param baseMessage - Base message with required fields (chatId, conversationId, userId)\n * @param options - Optional publishing options (e.g., custom channel)\n */\n async publishAudioChunk(\n audioData: string,\n format: string,\n textReference: string,\n sourceType: string,\n duration: number | undefined,\n index: number | undefined,\n baseMessage: Partial<BaseMessage>,\n options?: PublishOptions\n ): Promise<void> {\n const audioChunk: AudioChunk = {\n ...this.createBaseMessage(baseMessage),\n __typename: \"AudioChunk\",\n component: {\n type: \"AudioChunk\",\n props: {\n audioData,\n format,\n textReference,\n sourceType,\n duration,\n index,\n },\n },\n };\n\n await this.publish(audioChunk as any, options);\n }\n}\n\n// Singleton instance for maximum performance\nlet audioChunkPublisherInstance: AudioChunkPublisher | null = null;\n\n/**\n * Get singleton AudioChunkPublisher instance\n * Maximum performance - no new objects created after first call\n * \n * @param host - Redis host (required on first call)\n * @param port - Redis port (required on first call)\n * @param password - Redis password (required on first call)\n * @param providerId - Provider ID (required on first call)\n * @param username - Redis username (optional)\n * @param db - Redis database number (optional)\n * @returns Singleton AudioChunkPublisher instance\n */\nexport function getAudioChunkPublisher(\n host?: string, \n port?: number, \n password?: string, \n providerId?: string, \n username?: string, \n db?: number\n): AudioChunkPublisher {\n if (!audioChunkPublisherInstance) {\n if (!host || !port || password === undefined || !providerId) {\n throw new Error('AudioChunkPublisher requires host, port, password, and providerId on first call');\n }\n \n const publisher = Publisher.fromConfig(host, port, password, providerId, username, db);\n audioChunkPublisherInstance = new AudioChunkPublisher(\n publisher.getRedisConnection(),\n publisher.getProviderId()\n );\n }\n\n return audioChunkPublisherInstance;\n}\n","/**\n * State publisher for chat state updates\n */\n\nimport { BaseMessage } from \"../types\";\nimport { BasePublisher, PublishOptions } from \"./base\";\nimport { Publisher } from \"../Publisher\";\n\n/**\n * State message type for chat state updates\n */\nexport interface StateMessage extends BaseMessage {\n __typename: \"State\";\n component?: {\n type: string;\n props: {\n state: string;\n label?: string;\n [key: string]: any;\n };\n };\n data?: any;\n label?: string;\n variables?: any;\n}\n\n/**\n * StatePublisher - Handles chat state update messages\n */\nexport class StatePublisher extends BasePublisher {\n /**\n * Publishes a chat state update\n *\n * @param state - The chat state (e.g., THINKING, RESPONDING, etc.)\n * @param baseMessage - Base message with required fields (chatId, conversationId, userId)\n * @param label - Optional human-readable label for the state\n * @param data - Optional additional data for the state\n * @param variables - Optional variables associated with the state\n * @param options - Optional publishing options (e.g., custom channel)\n */\n async publishState(\n state: string,\n baseMessage: Partial<BaseMessage>,\n label?: string,\n data?: any,\n variables?: any,\n options?: PublishOptions\n ): Promise<void> {\n const stateMessage: StateMessage = {\n ...this.createBaseMessage(baseMessage),\n __typename: \"State\",\n component: {\n type: \"State\",\n props: {\n state,\n ...(label && { label }),\n },\n },\n ...(data && { data }),\n ...(label && { label }),\n ...(variables && { variables }),\n };\n\n if (!stateMessage.conversationId) {\n throw new Error(\"conversationId is required for publishing state messages\");\n }\n\n await this.publish(stateMessage, options);\n }\n\n /**\n * Convenience method to publish common state transitions\n */\n async publishThinking(baseMessage: Partial<BaseMessage>, options?: PublishOptions): Promise<void> {\n await this.publishState(\"THINKING\", baseMessage, \"Thinking...\", undefined, undefined, options);\n }\n\n async publishResponding(baseMessage: Partial<BaseMessage>, options?: PublishOptions): Promise<void> {\n await this.publishState(\"RESPONDING\", baseMessage, \"Responding...\", undefined, undefined, options);\n }\n\n async publishWaiting(baseMessage: Partial<BaseMessage>, options?: PublishOptions): Promise<void> {\n await this.publishState(\"WAITING\", baseMessage, \"Waiting for input...\", undefined, undefined, options);\n }\n\n async publishComplete(baseMessage: Partial<BaseMessage>, options?: PublishOptions): Promise<void> {\n await this.publishState(\"COMPLETE\", baseMessage, \"Complete\", undefined, undefined, options);\n }\n\n async publishError(error: string, baseMessage: Partial<BaseMessage>, options?: PublishOptions): Promise<void> {\n await this.publishState(\"ERROR\", baseMessage, \"Error occurred\", { error }, undefined, options);\n }\n}\n\n// Singleton instance\nlet statePublisherInstance: StatePublisher | null = null;\n\n/**\n * Get singleton StatePublisher instance\n * Maximum performance - no new objects created after first call\n * \n * @param host - Redis host (required on first call)\n * @param port - Redis port (required on first call)\n * @param password - Redis password (required on first call)\n * @param providerId - Provider ID (required on first call)\n * @param username - Redis username (optional)\n * @param db - Redis database number (optional)\n * @returns Singleton StatePublisher instance\n */\nexport function getStatePublisher(\n host?: string, \n port?: number, \n password?: string, \n providerId?: string, \n username?: string, \n db?: number\n): StatePublisher {\n if (!statePublisherInstance) {\n if (!host || !port || password === undefined || !providerId) {\n throw new Error('StatePublisher requires host, port, password, and providerId on first call');\n }\n \n const publisher = Publisher.fromConfig(host, port, password, providerId, username, db);\n statePublisherInstance = new StatePublisher(\n publisher.getRedisConnection(),\n publisher.getProviderId()\n );\n }\n\n return statePublisherInstance;\n}\n","/**\n * System message publisher\n *\n * @module messaging/publishers/system\n */\n\nimport { BaseMessage } from \"../types\";\nimport { BasePublisher, PublishOptions } from \"./base\";\nimport { Publisher } from \"../Publisher\";\n\n/**\n * System message interface\n */\nexport interface SystemMessage extends BaseMessage {\n __typename: \"SystemMessage\";\n message: string;\n level: \"info\" | \"warning\" | \"error\";\n}\n\n/**\n * SystemPublisher - Handles system messages\n * \n * System messages are used for service-level notifications, errors, and warnings\n * that need to be communicated to users or other services.\n * \n * @example\n * ```typescript\n * const publisher = new SystemPublisher(redis);\n * \n * // Publish a system message\n * await publisher.publishSystemMessage(\"Service started\", \"info\", {\n * chatId: \"chat123\",\n * conversationId: \"conv123\",\n * userId: \"user456\"\n * });\n * ```\n */\nexport class SystemPublisher extends BasePublisher {\n /**\n * Publishes a system message\n * \n * System messages are used for service notifications, errors, and warnings.\n * \n * @param message - The system message text\n * @param level - The message level (info, warning, or error)\n * @param baseMessage - Base message with required fields (chatId, conversationId, userId)\n * @param options - Optional publishing options (e.g., custom channel)\n * @returns Promise that resolves when message is published\n * \n * @example\n * ```typescript\n * // Service startup notification\n * await publisher.publishSystemMessage(\"Service started\", \"info\", {\n * chatId: \"chat123\",\n * conversationId: \"conv123\",\n * userId: \"user456\"\n * });\n * \n * // Error notification\n * await publisher.publishSystemMessage(\"Database connection failed\", \"error\", {\n * chatId: \"chat123\",\n * conversationId: \"conv123\",\n * userId: \"user456\"\n * });\n * ```\n */\n async publishSystemMessage(\n message: string,\n level: \"info\" | \"warning\" | \"error\",\n baseMessage: Partial<BaseMessage>,\n options?: PublishOptions\n ): Promise<void> {\n const systemMessage: SystemMessage = {\n ...this.createBaseMessage(baseMessage),\n __typename: \"SystemMessage\",\n message,\n level,\n };\n\n await this.publish(systemMessage, options);\n }\n}\n\n// Singleton instance for maximum performance\nlet systemPublisherInstance: SystemPublisher | null = null;\n\n/**\n * Get singleton SystemPublisher instance\n * Maximum performance - no new objects created after first call\n * \n * @param host - Redis host (required on first call)\n * @param port - Redis port (required on first call)\n * @param password - Redis password (required on first call)\n * @param providerId - Provider ID (required on first call)\n * @param username - Redis username (optional)\n * @param db - Redis database number (optional)\n * @returns Singleton SystemPublisher instance\n */\nexport function getSystemPublisher(\n host?: string, \n port?: number, \n password?: string, \n providerId?: string, \n username?: string, \n db?: number\n): SystemPublisher {\n if (!systemPublisherInstance) {\n if (!host || !port || password === undefined || !providerId) {\n throw new Error('SystemPublisher requires host, port, password, and providerId on first call');\n }\n \n const publisher = Publisher.fromConfig(host, port, password, providerId, username, db);\n systemPublisherInstance = new SystemPublisher(\n publisher.getRedisConnection(),\n publisher.getProviderId()\n );\n }\n \n return systemPublisherInstance;\n}\n","/**\n * Card Publisher - Publisher for card component data\n * \n * This publisher accepts flexible card JSON structures and publishes them as card components.\n * The client renders these as card components with the provided data structure.\n */\n\nimport { BasePublisher, PublishOptions } from \"./base\";\nimport type { BaseMessage } from \"../types\";\nimport { Publisher } from \"../Publisher\";\n\nexport interface Card extends BaseMessage {\n __typename: \"Cards\";\n component: {\n type: \"cards\";\n props: any; // Accept any JSON structure for card data\n };\n}\n\nexport class CardPublisher extends BasePublisher {\n /**\n * Publish card data with flexible JSON structure\n * @param cardData - Any JSON structure for card data\n * @param baseMessage - Base message properties\n * @param options - Optional publishing options (e.g., custom channel)\n */\n async publishCard(\n cardData: any,\n baseMessage: Partial<BaseMessage>,\n options?: PublishOptions\n ): Promise<void> {\n const message: Card = {\n ...this.createBaseMessage(baseMessage),\n __typename: \"Cards\",\n component: {\n type: \"cards\",\n props: cardData, // Pass through any JSON structure\n },\n };\n\n await this.publish(message as any, options);\n }\n\n /**\n * Publish multiple cards (for workflow service compatibility)\n * @param cardsData - Array of card data or single card data\n * @param baseMessage - Base message properties\n * @param options - Optional publishing options (e.g., custom channel)\n */\n async publishCards(\n cardsData: any[] | any,\n baseMessage: Partial<BaseMessage>,\n options?: PublishOptions\n ): Promise<void> {\n const cardsArray = Array.isArray(cardsData) ? cardsData : [cardsData];\n \n // Publish all cards as a single event with array data\n const message: Card = {\n ...this.createBaseMessage(baseMessage),\n __typename: \"Cards\",\n component: {\n type: \"cards\",\n props: cardsArray, // Pass the entire array\n },\n };\n\n await this.publish(message as any, options);\n }\n}\n\n// Singleton instance for performance\nlet cardPublisherInstance: CardPublisher | null = null;\n\n/**\n * Get singleton CardPublisher instance\n * Maximum performance - no new objects created after first call\n * \n * @param host - Redis host (required on first call)\n * @param port - Redis port (required on first call)\n * @param password - Redis password (required on first call)\n * @param providerId - Provider ID (required on first call)\n * @param username - Redis username (optional)\n * @param db - Redis database number (optional)\n * @returns Singleton CardPublisher instance\n */\nexport function getCardPublisher(\n host?: string, \n port?: number, \n password?: string, \n providerId?: string, \n username?: string, \n db?: number\n): CardPublisher {\n if (!cardPublisherInstance) {\n if (!host || !port || password === undefined || !providerId) {\n throw new Error('CardPublisher requires host, port, password, and providerId on first call');\n }\n \n const publisher = Publisher.fromConfig(host, port, password, providerId, username, db);\n cardPublisherInstance = new CardPublisher(\n publisher.getRedisConnection(),\n publisher.getProviderId()\n );\n }\n\n return cardPublisherInstance;\n}\n","/**\n * Questions Publisher - Publisher for follow-up questions data\n * \n * This publisher accepts an array of question strings and publishes them as follow-up questions.\n * The client renders these as interactive question buttons or suggestions.\n */\n\nimport { BasePublisher, PublishOptions } from \"./base\";\nimport type { BaseMessage } from \"../types\";\nimport { Publisher } from \"../Publisher\";\n\nexport interface Questions extends BaseMessage {\n __typename: \"Questions\";\n component: {\n type: \"questions\";\n props: string[]; // Array of question strings\n };\n}\n\nexport class QuestionsPublisher extends BasePublisher {\n /**\n * Publish follow-up questions\n * @param questions - Array of question strings\n * @param baseMessage - Base message properties\n * @param options - Optional publishing options (e.g., custom channel)\n */\n async publishQuestions(\n questions: string[],\n baseMessage: Partial<BaseMessage>,\n options?: PublishOptions\n ): Promise<void> {\n const message: Questions = {\n ...this.createBaseMessage(baseMessage),\n __typename: \"Questions\",\n component: {\n type: \"questions\",\n props: questions, // Pass the array of question strings\n },\n };\n\n await this.publish(message as