@ably/cli
Version:
Ably CLI for Pub/Sub, Chat and Spaces
204 lines (203 loc) • 10.6 kB
JavaScript
import { RoomStatus, } from "@ably/chat";
import { Args } from "@oclif/core";
import chalk from "chalk";
import { ChatBaseCommand } from "../../../chat-base-command.js";
export default class TypingSubscribe extends ChatBaseCommand {
static args = {
roomId: Args.string({
description: "The room ID to subscribe to typing indicators from",
required: true,
}),
};
static description = "Subscribe to typing indicators in an Ably Chat room";
static examples = [
"$ ably rooms typing subscribe my-room",
'$ ably rooms typing subscribe --api-key "YOUR_API_KEY" my-room',
"$ ably rooms typing subscribe my-room --json",
"$ ably rooms typing subscribe my-room --pretty-json",
];
static flags = {
...ChatBaseCommand.globalFlags,
};
chatClient = null;
ablyClient = null;
unsubscribeStatusFn = null;
unsubscribeTypingFn = null;
// Override finally to ensure resources are cleaned up
async finally(err) {
if (this.unsubscribeTypingFn) {
try {
this.unsubscribeTypingFn.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(TypingSubscribe);
try {
// Create Chat client
this.chatClient = await this.createChatClient(flags);
this.ablyClient = this._chatRealtimeClient;
if (!this.chatClient || !this.ablyClient) {
this.error("Failed to initialize clients");
return;
}
const { roomId } = args;
// Set up connection state logging
this.setupConnectionStateLogging(this.ablyClient, flags, {
includeUserFriendlyMessages: true
});
// Get the room with typing enabled
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 });
if (statusChange.current === RoomStatus.Attached) {
if (!this.shouldOutputJson(flags)) {
this.log(`${chalk.green("Connected to room:")} ${chalk.bold(roomId)}`);
this.log(`${chalk.dim("Listening for typing indicators. Press Ctrl+C to exit.")}`);
}
}
else if (statusChange.current === RoomStatus.Failed &&
!this.shouldOutputJson(flags)) {
this.error(`Failed to attach to room: ${reasonMsg || "Unknown error"}`);
}
});
this.unsubscribeStatusFn = unsubscribeStatus;
this.logCliEvent(flags, "room", "subscribedToStatus", "Successfully subscribed to room status changes");
// Set up typing indicators
this.logCliEvent(flags, "typing", "subscribing", "Subscribing to typing indicators");
this.unsubscribeTypingFn = room.typing.subscribe((typingSetEvent) => {
const timestamp = new Date().toISOString();
const currentlyTyping = [...(typingSetEvent.currentlyTyping || [])];
const eventData = {
currentlyTyping,
roomId,
timestamp,
};
this.logCliEvent(flags, "typing", "update", "Typing status update received", eventData);
if (this.shouldOutputJson(flags)) {
this.log(this.formatJsonOutput({ success: true, ...eventData }, flags));
}
else {
// Clear-line updates are helpful in an interactive TTY but they make
// the mocha output hard to read when the CLI is invoked from unit
// tests (ABLY_CLI_TEST_MODE=true) or when stdout is not a TTY (CI).
const shouldInlineUpdate = this.shouldUseTerminalUpdates();
if (shouldInlineUpdate) {
// Clear the current line and rewrite it in-place.
process.stdout.write("\r\u001B[K");
if (currentlyTyping.length > 0) {
const memberNames = currentlyTyping.join(", ");
process.stdout.write(chalk.yellow(`${memberNames} ${currentlyTyping.length === 1 ? "is" : "are"} typing...`));
}
}
else if (currentlyTyping.length > 0) {
// Fallback: just log a new line so that test output remains intact.
const memberNames = currentlyTyping.join(", ");
this.log(chalk.yellow(`${memberNames} ${currentlyTyping.length === 1 ? "is" : "are"} typing...`));
}
}
});
this.logCliEvent(flags, "typing", "subscribed", "Successfully subscribed to typing indicators");
// Attach to the room
this.logCliEvent(flags, "room", "attaching", `Attaching to room ${roomId}`);
await room.attach();
// Successful attach logged by onStatusChange handler
this.logCliEvent(flags, "typing", "listening", "Listening for typing indicators...");
// Keep the process running until Ctrl+C
await new Promise(() => {
// This promise intentionally never resolves
process.on("SIGINT", async () => {
this.logCliEvent(flags, "typing", "cleanupInitiated", "Cleanup initiated (Ctrl+C pressed)");
if (!this.shouldOutputJson(flags)) {
// Move to a new line to not override typing status
this.log("\n");
this.log(`${chalk.yellow("Disconnecting from room...")}`);
}
// Clean up subscriptions
if (this.unsubscribeTypingFn) {
try {
this.logCliEvent(flags, "typing", "unsubscribing", "Unsubscribing from typing indicators");
this.unsubscribeTypingFn.unsubscribe();
this.logCliEvent(flags, "typing", "unsubscribed", "Unsubscribed from typing indicators");
}
catch (error) {
this.logCliEvent(flags, "typing", "unsubscribeError", "Error unsubscribing typing", {
error: error instanceof Error ? error.message : String(error),
});
}
}
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) {
this.logCliEvent(flags, "room", "unsubscribeStatusError", "Error unsubscribing status", {
error: error instanceof Error ? error.message : String(error),
});
}
}
// Release the room and close connection
try {
this.logCliEvent(flags, "room", "releasing", `Releasing room ${roomId}`);
await this.chatClient?.rooms.release(roomId);
this.logCliEvent(flags, "room", "released", `Room ${roomId} released`);
}
catch (error) {
this.logCliEvent(flags, "room", "releaseError", "Error releasing room", { error: error instanceof Error ? error.message : String(error) });
}
if (this.ablyClient) {
this.logCliEvent(flags, "connection", "closing", "Closing Realtime connection");
this.ablyClient.close();
this.logCliEvent(flags, "connection", "closed", "Realtime connection closed");
}
if (!this.shouldOutputJson(flags)) {
this.log(`${chalk.green("Successfully disconnected.")}`);
}
// Graceful exit without forcing process termination.
});
});
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
this.logCliEvent(flags, "typing", "fatalError", `Failed to subscribe to typing indicators: ${errorMsg}`, { error: errorMsg, roomId: args.roomId });
// Close the connection in case of error
if (this.ablyClient) {
this.ablyClient.close();
}
if (this.shouldOutputJson(flags)) {
this.log(this.formatJsonOutput({ error: errorMsg, roomId: args.roomId, success: false }, flags));
}
else {
this.error(`Failed to subscribe to typing indicators: ${errorMsg}`);
}
}
}
}