UNPKG

@ably/cli

Version:

Ably CLI for Pub/Sub, Chat and Spaces

263 lines (262 loc) 13 kB
import { Args } from "@oclif/core"; import chalk from "chalk"; import { SpacesBaseCommand } from "../../../spaces-base-command.js"; export default class SpacesCursorsGetAll extends SpacesBaseCommand { static args = { spaceId: Args.string({ description: "Space ID to get cursors from", required: true, }), }; static description = "Get all current cursors in a space"; static examples = [ "$ ably spaces cursors get-all my-space", "$ ably spaces cursors get-all my-space --json", "$ ably spaces cursors get-all my-space --pretty-json", ]; static flags = { ...SpacesBaseCommand.globalFlags, }; // Declare class properties for clients and space realtimeClient = null; spacesClient = null; space = null; async run() { const { args, flags } = await this.parse(SpacesCursorsGetAll); let cleanupInProgress = false; const { spaceId } = args; try { // Create Spaces client using setupSpacesClient const setupResult = await this.setupSpacesClient(flags, spaceId); this.realtimeClient = setupResult.realtimeClient; this.spacesClient = setupResult.spacesClient; this.space = setupResult.space; if (!this.realtimeClient || !this.spacesClient || !this.space) { this.error("Failed to initialize clients or space"); return; } // Make sure we have a connection before proceeding await new Promise((resolve, reject) => { const checkConnection = () => { const { state } = this.realtimeClient.connection; if (state === "connected") { resolve(); } else if (state === "failed" || state === "closed" || state === "suspended") { reject(new Error(`Connection failed with state: ${state}`)); } else { // Still connecting, check again shortly setTimeout(checkConnection, 100); } }; checkConnection(); }); // Get the space if (!this.shouldOutputJson(flags)) { this.log(`Connecting to space: ${chalk.cyan(spaceId)}...`); } // Enter the space await this.space.enter(); // Wait for space to be properly entered before fetching cursors await new Promise((resolve, reject) => { // Set a reasonable timeout to avoid hanging indefinitely const timeout = setTimeout(() => { reject(new Error("Timed out waiting for space connection")); }, 5000); const checkSpaceStatus = () => { try { // Check realtime client state if (this.realtimeClient.connection.state === "connected") { clearTimeout(timeout); if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ connectionId: this.realtimeClient.connection.id, spaceId, status: "connected", success: true, }, flags)); } else { this.log(`${chalk.green("Successfully entered space:")} ${chalk.cyan(spaceId)}`); } resolve(); } else if (this.realtimeClient.connection.state === "failed" || this.realtimeClient.connection.state === "closed" || this.realtimeClient.connection.state === "suspended") { clearTimeout(timeout); reject(new Error(`Space connection failed with state: ${this.realtimeClient.connection.state}`)); } else { // Still connecting, check again shortly setTimeout(checkSpaceStatus, 100); } } catch (error) { clearTimeout(timeout); reject(error); } }; checkSpaceStatus(); }); // Subscribe to cursor updates to ensure we receive remote cursors let cursorUpdateReceived = false; const cursorMap = new Map(); // Show initial message if (!this.shouldOutputJson(flags)) { const waitSeconds = this.isTestMode() ? '0.5' : '5'; this.log(`Collecting cursor positions for ${waitSeconds} seconds...`); this.log(chalk.dim('─'.repeat(60))); } const cursorUpdateHandler = (cursor) => { cursorUpdateReceived = true; // Update the cursor map if (cursor.connectionId) { cursorMap.set(cursor.connectionId, cursor); // Show live update on one line if (!this.shouldOutputJson(flags) && this.shouldUseTerminalUpdates()) { const clientDisplay = cursor.clientId || 'Unknown'; const x = cursor.position.x; const y = cursor.position.y; // Clear the line and write the update process.stdout.write(`\r${chalk.gray('►')} ${chalk.blue(clientDisplay)}: (${chalk.yellow(x)}, ${chalk.yellow(y)})${' '.repeat(30)}`); } } }; await this.space.cursors.subscribe('update', cursorUpdateHandler); // Wait for 5 seconds (or shorter in test mode) const waitTime = this.isTestMode() ? 500 : 5000; await new Promise((resolve) => { setTimeout(() => { if (!this.shouldOutputJson(flags) && this.shouldUseTerminalUpdates()) { // Clear the last update line process.stdout.write('\r' + ' '.repeat(60) + '\r'); } resolve(); }, waitTime); }); // Unsubscribe from cursor updates this.space.cursors.unsubscribe('update', cursorUpdateHandler); // Now get all cursors (including locally cached ones) and merge with live updates const allCursors = await this.space.cursors.getAll(); // Add any cached cursors that we didn't see in live updates if (Array.isArray(allCursors)) { allCursors.forEach((cursor) => { if (cursor.connectionId && !cursorMap.has(cursor.connectionId)) { cursorMap.set(cursor.connectionId, cursor); } }); } const cursors = [...cursorMap.values()]; if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ cursors: cursors.map((cursor) => ({ clientId: cursor.clientId, connectionId: cursor.connectionId, data: cursor.data, position: cursor.position, })), spaceId, success: true, cursorUpdateReceived, }, flags)); } else { if (!cursorUpdateReceived && cursors.length === 0) { this.log(chalk.dim('─'.repeat(60))); this.log(chalk.yellow("No cursor updates are being sent in this space. Make sure other clients are actively setting cursor positions.")); cleanupInProgress = true; return; } if (cursors.length === 0) { this.log(chalk.dim('─'.repeat(60))); this.log(chalk.yellow("No active cursors found in space.")); cleanupInProgress = true; return; } // Show summary table this.log(chalk.dim('─'.repeat(60))); this.log(chalk.bold(`\nCursor Summary - ${cursors.length} cursor${cursors.length === 1 ? '' : 's'} found:\n`)); // Table header const colWidths = { client: 20, x: 8, y: 8, connection: 20 }; this.log(chalk.gray('┌' + '─'.repeat(colWidths.client + 2) + '┬' + '─'.repeat(colWidths.x + 2) + '┬' + '─'.repeat(colWidths.y + 2) + '┬' + '─'.repeat(colWidths.connection + 2) + '┐')); this.log(chalk.gray('│ ') + chalk.bold('Client ID'.padEnd(colWidths.client)) + chalk.gray(' │ ') + chalk.bold('X'.padEnd(colWidths.x)) + chalk.gray(' │ ') + chalk.bold('Y'.padEnd(colWidths.y)) + chalk.gray(' │ ') + chalk.bold('Connection'.padEnd(colWidths.connection)) + chalk.gray(' │')); this.log(chalk.gray('├' + '─'.repeat(colWidths.client + 2) + '┼' + '─'.repeat(colWidths.x + 2) + '┼' + '─'.repeat(colWidths.y + 2) + '┼' + '─'.repeat(colWidths.connection + 2) + '┤')); // Table rows cursors.forEach((cursor) => { const clientId = (cursor.clientId || 'Unknown').slice(0, colWidths.client); const x = cursor.position.x.toString().slice(0, colWidths.x); const y = cursor.position.y.toString().slice(0, colWidths.y); const connectionId = (cursor.connectionId || 'Unknown').slice(0, colWidths.connection); this.log(chalk.gray('│ ') + chalk.blue(clientId.padEnd(colWidths.client)) + chalk.gray(' │ ') + chalk.yellow(x.padEnd(colWidths.x)) + chalk.gray(' │ ') + chalk.yellow(y.padEnd(colWidths.y)) + chalk.gray(' │ ') + chalk.dim(connectionId.padEnd(colWidths.connection)) + chalk.gray(' │')); }); this.log(chalk.gray('└' + '─'.repeat(colWidths.client + 2) + '┴' + '─'.repeat(colWidths.x + 2) + '┴' + '─'.repeat(colWidths.y + 2) + '┴' + '─'.repeat(colWidths.connection + 2) + '┘')); // Show additional data if any cursor has it const cursorsWithData = cursors.filter(c => c.data); if (cursorsWithData.length > 0) { this.log(`\n${chalk.bold('Additional Data:')}`); cursorsWithData.forEach((cursor) => { this.log(` ${chalk.blue(cursor.clientId || 'Unknown')}: ${JSON.stringify(cursor.data)}`); }); } } // Mark that we're done cleanupInProgress = true; } catch (error) { if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ error: `Error getting cursors: ${error instanceof Error ? error.message : String(error)}`, spaceId: args.spaceId, status: "error", success: false, }, flags)); } else { this.log(chalk.red(`Error getting cursors: ${error instanceof Error ? error.message : String(error)}`)); } } finally { if (!cleanupInProgress) { cleanupInProgress = true; } // Always clean up connections try { if (this.space) { await this.space.leave(); } } catch { // Ignore cleanup errors } try { if (this.realtimeClient && this.realtimeClient.connection.state !== 'closed') { this.realtimeClient.close(); // Give the connection a moment to close await new Promise(resolve => setTimeout(resolve, 100)); } } catch { // Ignore cleanup errors } // Force exit if we're done and cleaned up if (cleanupInProgress) { // Allow any pending I/O to complete setImmediate(() => { process.exit(0); }); } } } }