@ably/cli
Version:
Ably CLI for Pub/Sub, Chat and Spaces
226 lines (225 loc) • 11.4 kB
JavaScript
import { Args, Flags } from "@oclif/core";
import chalk from "chalk";
import { SpacesBaseCommand } from "../../../spaces-base-command.js";
export default class SpacesLocksAcquire extends SpacesBaseCommand {
static args = {
spaceId: Args.string({
description: "Space ID to acquire lock in",
required: true,
}),
lockId: Args.string({
description: "ID of the lock to acquire",
required: true,
}),
};
static description = "Acquire a lock in a space";
static examples = [
"$ ably spaces locks acquire my-space my-lock-id",
'$ ably spaces locks acquire my-space my-lock-id --data \'{"type":"editor"}\'',
];
static flags = {
...SpacesBaseCommand.globalFlags,
data: Flags.string({
description: "Optional data to associate with the lock (JSON format)",
required: false,
}),
};
cleanupInProgress = false;
realtimeClient = null;
spacesClient = null;
lockId = null;
space = null;
// Override finally to ensure resources are cleaned up
async finally(err) {
// Attempt to release lock and leave space if not already done
if (!this.cleanupInProgress && this.space && this.lockId) {
// Check if space and lockId are available
try {
this.logCliEvent({}, "lock", "finalReleaseAttempt", "Attempting final lock release", { lockId: this.lockId });
await this.space.locks.release(this.lockId);
}
catch (error) {
this.logCliEvent({}, "lock", "finalReleaseError", "Error in final lock release", {
error: error instanceof Error ? error.message : String(error),
lockId: this.lockId,
});
}
try {
this.logCliEvent({}, "spaces", "finalLeaveAttempt", "Attempting final space leave");
await this.space.leave();
}
catch (error) {
this.logCliEvent({}, "spaces", "finalLeaveError", "Error in final space leave", { error: error instanceof Error ? error.message : String(error) });
}
}
if (this.realtimeClient &&
this.realtimeClient.connection.state !== "closed" &&
this.realtimeClient.connection.state !== "failed") {
this.realtimeClient.close();
}
return super.finally(err);
}
async run() {
const { args, flags } = await this.parse(SpacesLocksAcquire);
const { spaceId } = args;
this.lockId = args.lockId;
const { lockId } = this;
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;
}
// Set up connection state logging
this.setupConnectionStateLogging(this.realtimeClient, flags, {
includeUserFriendlyMessages: true
});
// Parse lock data if provided
let lockData;
if (flags.data) {
try {
lockData = JSON.parse(flags.data);
this.logCliEvent(flags, "lock", "dataParsed", "Lock data parsed successfully", { data: lockData });
}
catch (error) {
const errorMsg = `Invalid lock data JSON: ${error instanceof Error ? error.message : String(error)}`;
this.logCliEvent(flags, "lock", "dataParseError", errorMsg, {
error: errorMsg,
});
this.error(errorMsg);
return;
}
}
// Get the space
this.logCliEvent(flags, "spaces", "gettingSpace", `Getting space: ${spaceId}...`);
this.logCliEvent(flags, "spaces", "gotSpace", `Successfully got space handle: ${spaceId}`);
// Enter the space first
this.logCliEvent(flags, "spaces", "entering", "Entering space...");
await this.space.enter();
this.logCliEvent(flags, "spaces", "entered", "Successfully entered space", { clientId: this.realtimeClient.auth.clientId });
// Try to acquire the lock
try {
this.logCliEvent(flags, "lock", "acquiring", `Attempting to acquire lock: ${lockId}`, { data: lockData, lockId });
const lock = await this.space.locks.acquire(lockId, lockData);
const lockDetails = {
lockId: lock.id,
member: lock.member
? {
clientId: lock.member.clientId,
connectionId: lock.member.connectionId,
}
: null,
reason: lock.reason,
status: lock.status,
timestamp: lock.timestamp,
};
this.logCliEvent(flags, "lock", "acquired", `Successfully acquired lock: ${lockId}`, lockDetails);
if (this.shouldOutputJson(flags)) {
this.log(this.formatJsonOutput({ lock: lockDetails, success: true }, flags));
}
else {
this.log(`${chalk.green("Successfully acquired lock:")} ${chalk.cyan(lockId)}`);
this.log(`${chalk.dim("Lock details:")} ${this.formatJsonOutput(lockDetails, { ...flags, "pretty-json": true })}`);
this.log(`\n${chalk.dim("Holding lock. Press Ctrl+C to release and exit.")}`);
}
}
catch (error) {
const errorMsg = `Failed to acquire lock: ${error instanceof Error ? error.message : String(error)}`;
this.logCliEvent(flags, "lock", "acquireFailed", errorMsg, {
error: errorMsg,
lockId,
});
if (this.shouldOutputJson(flags)) {
this.log(this.formatJsonOutput({ error: errorMsg, lockId, success: false }, flags));
}
else {
this.error(errorMsg);
}
return; // Exit if lock acquisition fails
}
this.logCliEvent(flags, "lock", "holding", `Holding lock ${lockId}. Press Ctrl+C to release.`);
// Keep the process running until interrupted
await new Promise((resolve, _reject) => {
const cleanup = async () => {
if (this.cleanupInProgress)
return;
this.cleanupInProgress = true;
this.logCliEvent(flags, "lock", "cleanupInitiated", "Cleanup initiated (Ctrl+C pressed)");
if (!this.shouldOutputJson(flags)) {
this.log(`\n${chalk.yellow("Releasing lock and closing connection...")}`);
}
// Set a force exit timeout
const forceExitTimeout = setTimeout(() => {
const errorMsg = "Force exiting after timeout during cleanup";
this.logCliEvent(flags, "lock", "forceExit", errorMsg, {
lockId,
spaceId,
});
if (!this.shouldOutputJson(flags)) {
this.log(chalk.red("Force exiting after timeout..."));
}
this.exit(1); // Use oclif's exit method instead of process.exit
}, 5000);
try {
if (this.space) {
try {
// Release the lock
this.logCliEvent(flags, "lock", "releasing", `Releasing lock ${lockId}`);
await this.space.locks.release(lockId);
this.logCliEvent(flags, "lock", "released", `Successfully released lock ${lockId}`);
if (!this.shouldOutputJson(flags)) {
this.log(chalk.green("Successfully released lock."));
}
// Leave the space
this.logCliEvent(flags, "spaces", "leaving", "Leaving space...");
await this.space.leave();
this.logCliEvent(flags, "spaces", "left", "Successfully left space");
if (!this.shouldOutputJson(flags)) {
this.log(chalk.green("Successfully left the space."));
}
}
catch (error) {
const errorMsg = `Error during lock release/leave: ${error instanceof Error ? error.message : String(error)}`;
this.logCliEvent(flags, "lock", "cleanupReleaseError", errorMsg, { error: errorMsg });
if (!this.shouldOutputJson(flags)) {
this.log(`Error during cleanup: ${errorMsg}`);
}
}
}
if (this.realtimeClient &&
this.realtimeClient.connection.state !== "closed") {
this.logCliEvent(flags, "connection", "closing", "Closing Realtime connection");
this.realtimeClient.close();
this.logCliEvent(flags, "connection", "closed", "Realtime connection closed");
}
this.logCliEvent(flags, "lock", "cleanupComplete", "Cleanup complete");
if (!this.shouldOutputJson(flags)) {
this.log(chalk.green("Successfully disconnected."));
}
clearTimeout(forceExitTimeout);
resolve();
// The command will naturally end after the promise resolves
}
catch (error) {
const errorMsg = `Error during cleanup: ${error instanceof Error ? error.message : String(error)}`;
this.logCliEvent(flags, "lock", "cleanupError", errorMsg, {
error: errorMsg,
});
if (!this.shouldOutputJson(flags)) {
this.log(`Error during cleanup: ${errorMsg}`);
}
}
};
process.on("SIGINT", cleanup);
process.on("SIGTERM", cleanup);
});
}
catch (error) {
this.error(error instanceof Error ? error.message : String(error));
}
}
}