UNPKG

@ably/cli

Version:

Ably CLI for Pub/Sub, Chat and Spaces

249 lines (248 loc) 13 kB
import { RoomStatus } from "@ably/chat"; import { Args } from "@oclif/core"; import chalk from "chalk"; import { ChatBaseCommand } from "../../../chat-base-command.js"; export default class RoomsReactionsSubscribe extends ChatBaseCommand { static args = { roomId: Args.string({ description: "Room ID to subscribe to reactions in", required: true, }), }; static description = "Subscribe to reactions in a chat room"; static examples = [ "$ ably rooms reactions subscribe my-room", "$ ably rooms reactions subscribe my-room --json", "$ ably rooms reactions subscribe my-room --pretty-json", ]; static flags = { ...ChatBaseCommand.globalFlags, }; // private clients: ChatClients | null = null; // Replace with chatClient and ablyClient chatClient = null; ablyClient = null; unsubscribeReactionsFn = null; unsubscribeStatusFn = null; // Override finally to ensure resources are cleaned up async finally(err) { if (this.unsubscribeReactionsFn) { try { this.unsubscribeReactionsFn.unsubscribe(); } catch { /* ignore */ } } if (this.unsubscribeStatusFn) { try { this.unsubscribeStatusFn(); } catch { /* ignore */ } } if (this.ablyClient && this.ablyClient.connection.state !== "closed" && this.ablyClient.connection.state !== "failed") { this.ablyClient.close(); } return super.finally(err); } async run() { const { args, flags } = await this.parse(RoomsReactionsSubscribe); try { // Create Chat client // this.clients = await this.createChatClient(flags) // Assign to chatClient this.chatClient = await this.createChatClient(flags); this.ablyClient = this._chatRealtimeClient; // Also create Ably client // if (!this.clients) return // Check both clients if (!this.chatClient || !this.ablyClient) { this.error("Failed to initialize clients"); return; } // const { chatClient, realtimeClient } = this.clients // Remove deconstruction const { roomId } = args; // Set up connection state logging this.setupConnectionStateLogging(this.ablyClient, flags, { includeUserFriendlyMessages: true }); this.logCliEvent(flags, "subscribe", "connecting", `Connecting to Ably and subscribing to reactions in room ${roomId}...`); if (!this.shouldOutputJson(flags)) { this.log(`Connecting to Ably and subscribing to reactions in room ${chalk.cyan(roomId)}...`); } // Get the room this.logCliEvent(flags, "room", "gettingRoom", `Getting room handle for ${roomId}`); const room = await this.chatClient.rooms.get(roomId, {}); this.logCliEvent(flags, "room", "gotRoom", `Got room handle for ${roomId}`); // Subscribe to room status changes this.logCliEvent(flags, "room", "subscribingToStatus", "Subscribing to room status changes"); const { off: unsubscribeStatus } = room.onStatusChange((statusChange) => { let reason; if (statusChange.current === RoomStatus.Failed) { reason = room.error; // Get reason from room.error on failure } const reasonMsg = reason instanceof Error ? reason.message : reason; this.logCliEvent(flags, "room", `status-${statusChange.current}`, `Room status changed to ${statusChange.current}`, { reason: reasonMsg }); switch (statusChange.current) { case RoomStatus.Attached: { if (!this.shouldOutputJson(flags)) { this.log(chalk.green("Successfully connected to Ably")); this.log(`Listening for reactions in room ${chalk.cyan(roomId)}. Press Ctrl+C to exit.`); } break; } case RoomStatus.Detached: { if (!this.shouldOutputJson(flags)) { this.log(chalk.yellow("Disconnected from Ably")); } break; } case RoomStatus.Failed: { if (!this.shouldOutputJson(flags)) { this.error(`${chalk.red("Connection failed:")} ${reasonMsg || "Unknown error"}`); } break; } // No default } }); this.unsubscribeStatusFn = unsubscribeStatus; this.logCliEvent(flags, "room", "subscribedToStatus", "Successfully subscribed to room status changes"); // Attach to the room this.logCliEvent(flags, "room", "attaching", `Attaching to room ${roomId}`); await room.attach(); // Successful attach logged by onStatusChange handler // Subscribe to room reactions this.logCliEvent(flags, "reactions", "subscribing", "Subscribing to reactions"); this.unsubscribeReactionsFn = room.reactions.subscribe((event) => { const reaction = event.reaction; const timestamp = new Date().toISOString(); // Chat SDK doesn't provide timestamp in event const eventData = { clientId: reaction.clientId, metadata: reaction.metadata, roomId, timestamp, name: reaction.name, }; this.logCliEvent(flags, "reactions", "received", "Reaction received", eventData); if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ success: true, ...eventData }, flags)); } else { this.log(`[${chalk.dim(timestamp)}] ${chalk.green("⚡")} ${chalk.blue(reaction.clientId || "Unknown")} reacted with ${chalk.yellow(reaction.name || "unknown")}`); // Show any additional metadata in the reaction if (reaction.metadata && Object.keys(reaction.metadata).length > 0) { this.log(` ${chalk.dim("Metadata:")} ${this.formatJsonOutput(reaction.metadata, flags)}`); } } }); this.logCliEvent(flags, "reactions", "subscribed", "Successfully subscribed to reactions"); this.logCliEvent(flags, "reactions", "listening", "Listening for reactions..."); // Keep the process running until interrupted await new Promise((resolve) => { let cleanupInProgress = false; const cleanup = async () => { if (cleanupInProgress) return; cleanupInProgress = true; this.logCliEvent(flags, "reactions", "cleanupInitiated", "Cleanup initiated (Ctrl+C pressed)"); if (!this.shouldOutputJson(flags)) { this.log(`\n${chalk.yellow("Unsubscribing and closing connection...")}`); } // Set a force exit timeout const forceExitTimeout = setTimeout(() => { const errorMsg = "Force exiting after timeout during cleanup"; this.logCliEvent(flags, "reactions", "forceExit", errorMsg, { roomId, }); if (!this.shouldOutputJson(flags)) { this.log(chalk.red("Force exiting after timeout...")); } }, 5000); // Unsubscribe from reactions if (this.unsubscribeReactionsFn) { try { this.logCliEvent(flags, "reactions", "unsubscribing", "Unsubscribing from reactions"); this.unsubscribeReactionsFn.unsubscribe(); this.logCliEvent(flags, "reactions", "unsubscribed", "Unsubscribed from reactions"); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); this.logCliEvent(flags, "reactions", "unsubscribeError", `Error unsubscribing from reactions: ${errorMsg}`, { error: errorMsg }); } } // Unsubscribe from status changes if (this.unsubscribeStatusFn) { try { this.logCliEvent(flags, "room", "unsubscribingStatus", "Unsubscribing from room status"); this.unsubscribeStatusFn(); this.logCliEvent(flags, "room", "unsubscribedStatus", "Unsubscribed from room status"); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); this.logCliEvent(flags, "room", "unsubscribeStatusError", `Error unsubscribing from status: ${errorMsg}`, { error: errorMsg }); } } try { this.logCliEvent(flags, "room", "releasing", `Releasing room ${roomId}`); // await chatClient.rooms.release(roomId); // Use this.chatClient if (this.chatClient) { await this.chatClient.rooms.release(roomId); this.logCliEvent(flags, "room", "released", `Room ${roomId} released`); } else { this.logCliEvent(flags, "room", "releaseError", "Chat client was null during cleanup", { error: "Chat client null" }); } } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); this.logCliEvent(flags, "room", "releaseError", `Error releasing room: ${errorMsg}`, { error: errorMsg }); if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ error: errorMsg, roomId, success: false }, flags)); } else { this.log(`Error releasing room: ${errorMsg}`); } } // if (this.clients?.realtimeClient) { // Use this.ablyClient if (this.ablyClient) { this.logCliEvent(flags, "connection", "closing", "Closing Realtime connection"); // this.clients.realtimeClient.close(); // Use this.ablyClient this.ablyClient.close(); this.logCliEvent(flags, "connection", "closed", "Realtime connection closed"); } if (!this.shouldOutputJson(flags)) { this.log(chalk.green("Successfully disconnected.")); } clearTimeout(forceExitTimeout); resolve(); // Allow natural process exit after cleanup without forcing termination. }; process.on("SIGINT", () => void cleanup()); process.on("SIGTERM", () => void cleanup()); }); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); this.logCliEvent(flags, "reactions", "fatalError", `Error: ${errorMsg}`, { error: errorMsg, roomId: args.roomId, }); if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ error: errorMsg, roomId: args.roomId, success: false }, flags)); } else { this.error(`Error: ${errorMsg}`); } } finally { // Ensure client is closed even if cleanup promise didn't resolve if (this.ablyClient && this.ablyClient.connection.state !== "closed" && this.ablyClient.connection.state !== "failed") { this.logCliEvent(flags || {}, "connection", "finalCloseAttempt", "Ensuring connection is closed in finally block."); this.ablyClient.close(); } } } }