Skip to content

Minor Fixes: Spaces commands#185

Open
sacOO7 wants to merge 3 commits intomainfrom
fix/spaces-commands
Open

Minor Fixes: Spaces commands#185
sacOO7 wants to merge 3 commits intomainfrom
fix/spaces-commands

Conversation

@sacOO7
Copy link
Contributor

@sacOO7 sacOO7 commented Mar 24, 2026

  1. pnpm cli spaces subscribe dashboard =>
[2026-03-25T07:45:55.713Z]
Type: member
Action: enter
Client ID: ably-cli-191eff49
Connection ID: ynYWwwfiPb
Connected: true

[2026-03-25T07:46:03.825Z]
Type: member
Action: enter
Client ID: ably-cli-2d0fec87
Connection ID: i_cTXfAvYY
Connected: true

[2026-03-25T07:46:03.886Z]
Type: location
Client ID: ably-cli-2d0fec87
Connection ID: i_cTXfAvYY
Current Location: {"slide":1}
  1. pnpm cli spaces subscribe dashboard --pretty-json =>
{
  "type": "event",
  "command": "spaces:subscribe",
  "eventType": "member",
  "member": {
    "clientId": "ably-cli-b5eb207e",
    "connectionId": "kL975RlDGe",
    "isConnected": true,
    "profileData": null,
    "location": null,
    "lastEvent": {
      "name": "enter",
      "timestamp": 1774424945175
    }
  }
}
{
  "type": "event",
  "command": "spaces:subscribe",
  "eventType": "member",
  "member": {
    "clientId": "ably-cli-2d0fec87",
    "connectionId": "Q6pw8oq5d0",
    "isConnected": true,
    "profileData": null,
    "location": null,
    "lastEvent": {
      "name": "enter",
      "timestamp": 1774424948263
    }
  }
}
{
  "type": "event",
  "command": "spaces:subscribe",
  "eventType": "location",
  "location": {
    "member": {
      "clientId": "ably-cli-2d0fec87",
      "connectionId": "Q6pw8oq5d0"
    },
    "currentLocation": {
      "slide": 1
    },
    "previousLocation": null,
    "timestamp": "2026-03-25T07:49:08.319Z"
  }
}
  • None of the four spaces * get-all commands are eligible for collectPaginatedResults. The @ably/spaces SDK does not expose PaginatedResult for any of its getAll() methods. All pagination (where it exists) is handled internally by the SDK, invisible to the consumer.

Summary by CodeRabbit

  • New Features

    • Added warnings indicating spaces are ephemeral and backed by Ably channels
  • Bug Fixes

    • Fixed error messages to include correct space names in command guidance
    • Improved cursor simulation output formatting in both JSON and text modes
    • Enhanced event handling for location and member updates

@vercel
Copy link

vercel bot commented Mar 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cli-web-cli Ready Ready Preview, Comment Mar 25, 2026 9:03am

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Mar 24, 2026

Walkthrough

Multiple space-related commands were refactored to improve event handling, error management, and output formatting. Changes include adding warning messages to initialization, restructuring cursor output for simulate mode, updating error messages, and converting event listeners from class-level fields to inline handlers with enhanced error handling and deduplication logic.

Changes

Cohort / File(s) Summary
Output Messages
src/commands/spaces/create.ts, src/commands/spaces/cursors/set.ts
Added warning message after space creation indicating Ably channel backing and ephemeral nature. Restructured simulate-mode cursor output to emit JSON cursor events and status records, with separate formatting for JSON and human-readable modes.
Event Subscription Refactoring
src/commands/spaces/locations/subscribe.ts, src/commands/spaces/members/subscribe.ts, src/commands/spaces/subscribe.ts
Converted class-level listener fields to inline handler functions with enhanced error handling. Flattened try/catch structures in locations handler. Added deduplication logic keyed by clientId:connectionId in member and space subscribers. Refactored subscribe.ts to separately handle member and location event streams with updated imports and formatting helpers.
Error Messages
src/commands/spaces/get.ts
Updated failure message to include target space name (${spaceName}) in the command instruction when presence is empty.
Test Coverage
test/unit/commands/spaces/cursors/set.test.ts, test/unit/commands/spaces/subscribe.test.ts
Added test cases for simulated cursor output with --data fields and JSON mode. Expanded subscribe tests to verify separate member and location subscription handling, added location event assertions, and updated mocking to simulate per-stream subscription failures.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 With listeners now inline, clean and bright,
Events flow separate—members and locations in sight,
Deduplication dances in five-hundred-millisecond grace,
Warnings and messages find their proper place,
The refactored code hops forth with care!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Minor Fixes: Spaces commands' is overly vague and generic, using non-descriptive terms that don't convey specific information about the actual changes in the changeset. Use a more specific title that highlights the primary change, such as 'Add ephemeral warning to spaces create and update subscribe to show member/location updates' or similar.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/spaces-commands

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an informational warning to ably spaces create to clarify that Spaces are ephemeral (channel-backed) and that create initializes without entering/activating the space.

Changes:

  • Import formatWarning and emit a new post-create warning in non-JSON output.
  • Emit a status JSON record with status: "warning" containing the same message in --json mode.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@sacOO7 sacOO7 marked this pull request as ready for review March 25, 2026 09:02
@sacOO7 sacOO7 requested a review from Copilot March 25, 2026 09:03
@sacOO7
Copy link
Contributor Author

sacOO7 commented Mar 25, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
test/unit/commands/spaces/subscribe.test.ts (1)

81-106: Replace the wall-clock sleeps with a deterministic subscribe handoff.

These two tests depend on a 50 ms timeout winning the race against command setup, which can flake on slower CI. Resolve a local promise when the mocked space.locations.subscribe captures the handler, and await that instead of sleeping.

🧪 Example pattern
-      let locationHandler: ((update: unknown) => void) | undefined;
+      let locationHandler: ((update: unknown) => void) | undefined;
+      let resolveSubscribed = () => {};
+      const subscribed = new Promise<void>((resolve) => {
+        resolveSubscribed = resolve;
+      });
       space.locations.subscribe.mockImplementation(
         (_event: string, handler: (update: unknown) => void) => {
           locationHandler = handler;
+          resolveSubscribed();
         },
       );
@@
-      await new Promise((resolve) => setTimeout(resolve, 50));
+      await subscribed;

Apply the same pattern in the JSON location test below.

Also applies to: 154-179

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/commands/spaces/subscribe.test.ts` around lines 81 - 106, The test
currently uses a 50ms setTimeout race to wait for the mocked
space.locations.subscribe to capture the handler; replace that fragile sleep
with a deterministic promise handshake: create a local promise (e.g.,
subscribeReady) and its resolver, have the mockImplementation for
space.locations.subscribe assign locationHandler and call the resolver
immediately, then await subscribeReady before invoking locationHandler and
awaiting runCommand; apply the same pattern for the JSON location test (the
second occurrence) so both tests wait deterministically for the subscribe
handoff instead of sleeping.
src/commands/spaces/subscribe.ts (1)

48-52: Expire the dedupe cache after the 500 ms window.

This map only helps for 500 ms, but stale keys are kept forever. On a long-lived subscription, every transient connection leaves a permanent entry even though it can no longer affect deduplication.

Also applies to: 78-96

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/subscribe.ts` around lines 48 - 52, The dedupe Map
lastSeenEvents currently stores entries indefinitely; modify the logic that sets
or updates lastSeenEvents (where entries are added in the subscription/event
handler) to expire entries after 500ms: either schedule a setTimeout to delete
the specific key when you insert/update it or implement a lightweight pruning
pass that removes keys whose stored timestamp + 500ms < Date.now() before doing
dedupe checks; update both places referencing lastSeenEvents (the declaration
and its use in the event handling block) so stale client keys are removed and
the map doesn't grow unbounded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands/spaces/create.ts`:
- Around line 59-65: The JSON branch currently emits a separate status record
via logJsonStatus; instead, include the warning on the one-shot result by
calling this.logJsonResult(...) with the result payload plus a warning field
(use ephemeralSpaceWarning content) when shouldOutputJson(flags) is true and
remove the logJsonStatus call; in the human-readable branch replace the quoted
spaceName in ephemeralSpaceWarning with formatResource(spaceName) and keep using
formatWarning(...) when logging; locate symbols ephemeralSpaceWarning,
shouldOutputJson(flags), logJsonStatus, logJsonResult, formatWarning,
formatResource, spaceName and flags to make these changes.

In `@src/commands/spaces/members/subscribe.ts`:
- Around line 119-127: The member subscribe callback (memberListener) currently
calls this.fail(...) inside the callback and inconsistently accesses
member.lastEvent (both direct and with optional chaining); change the callback
to propagate errors via reject(new Error(...)) instead of calling this.fail
directly so the outer await/catch can call this.fail, and make the lastEvent
access consistent across the handler (either use member.lastEvent?.name and
member.lastEvent?.timestamp everywhere for defensive coding or remove the
optional chaining so both places assume lastEvent is present); update the code
around formatMessageTimestamp/formatTimestamp/formatMemberEventBlock usage and
replace the inner catch's this.fail call with reject(...) so errors bubble to
the outer catch.

In `@src/commands/spaces/subscribe.ts`:
- Around line 73-75: member.lastEvent is treated as optional earlier (action =
member.lastEvent?.name) but later code dereferences it unconditionally causing a
crash (this.fail) when it's missing; update the output path to consistently
guard accesses to lastEvent by using the already-computed action and
clientId/connectionId fallbacks or use optional chaining/defaults (e.g.,
member.lastEvent?.name, member.lastEvent?.actor?.id ?? 'Unknown',
member.lastEvent?.someField ?? 'Unknown') before reading properties, and ensure
any conditional logic that relied on lastEvent checks for its existence first so
the subscribe command no longer throws.

---

Nitpick comments:
In `@src/commands/spaces/subscribe.ts`:
- Around line 48-52: The dedupe Map lastSeenEvents currently stores entries
indefinitely; modify the logic that sets or updates lastSeenEvents (where
entries are added in the subscription/event handler) to expire entries after
500ms: either schedule a setTimeout to delete the specific key when you
insert/update it or implement a lightweight pruning pass that removes keys whose
stored timestamp + 500ms < Date.now() before doing dedupe checks; update both
places referencing lastSeenEvents (the declaration and its use in the event
handling block) so stale client keys are removed and the map doesn't grow
unbounded.

In `@test/unit/commands/spaces/subscribe.test.ts`:
- Around line 81-106: The test currently uses a 50ms setTimeout race to wait for
the mocked space.locations.subscribe to capture the handler; replace that
fragile sleep with a deterministic promise handshake: create a local promise
(e.g., subscribeReady) and its resolver, have the mockImplementation for
space.locations.subscribe assign locationHandler and call the resolver
immediately, then await subscribeReady before invoking locationHandler and
awaiting runCommand; apply the same pattern for the JSON location test (the
second occurrence) so both tests wait deterministically for the subscribe
handoff instead of sleeping.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4eb0c113-425e-4137-950b-66404a1c4ee7

📥 Commits

Reviewing files that changed from the base of the PR and between f67c148 and 9bedfa6.

📒 Files selected for processing (8)
  • src/commands/spaces/create.ts
  • src/commands/spaces/cursors/set.ts
  • src/commands/spaces/get.ts
  • src/commands/spaces/locations/subscribe.ts
  • src/commands/spaces/members/subscribe.ts
  • src/commands/spaces/subscribe.ts
  • test/unit/commands/spaces/cursors/set.test.ts
  • test/unit/commands/spaces/subscribe.test.ts

Comment on lines +59 to +65
const ephemeralSpaceWarning = `Space: ${spaceName} is backed by ably channel '${spaceName}::$space' and is ephemeral — it will become active when at least one member enters. This command initializes the space without entering it. To add a member to the space, use 'ably spaces members enter ${spaceName}'`;

if (this.shouldOutputJson(flags)) {
this.logJsonStatus("warning", ephemeralSpaceWarning, flags);
} else {
this.log(formatWarning(ephemeralSpaceWarning));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep one-shot JSON output as a single result record.

This currently emits two JSON records (logJsonResult then logJsonStatus) for a one-shot command. Prefer a single logJsonResult payload that includes warning metadata, and reserve logJsonStatus for long-running status signals. Also use formatResource(...) for resource names in the human-readable warning branch.

Proposed adjustment
-      if (this.shouldOutputJson(flags)) {
-        this.logJsonResult({ space: { name: spaceName } }, flags);
-      } else {
+      const ephemeralSpaceWarning = `Space ${spaceName} is backed by Ably channel '${spaceName}::$space' and is ephemeral — it will become active when at least one member enters. This command initializes the space without entering it. To add a member to the space, use 'ably spaces members enter ${spaceName}'.`;
+
+      if (this.shouldOutputJson(flags)) {
+        this.logJsonResult(
+          {
+            space: { name: spaceName },
+            warning: ephemeralSpaceWarning,
+          },
+          flags,
+        );
+      } else {
         this.log(
           formatSuccess(
             `Space ${formatResource(spaceName)} initialized. Use "ably spaces members enter" to activate it.`,
           ),
         );
+        this.log(
+          formatWarning(
+            `Space ${formatResource(spaceName)} is backed by Ably channel ${formatResource(`${spaceName}::$space`)} and is ephemeral — it will become active when at least one member enters. This command initializes the space without entering it. To add a member to the space, use 'ably spaces members enter ${spaceName}'.`,
+          ),
+        );
       }
-
-      const ephemeralSpaceWarning = `Space: ${spaceName} is backed by ably channel '${spaceName}::$space' and is ephemeral — it will become active when at least one member enters. This command initializes the space without entering it. To add a member to the space, use 'ably spaces members enter ${spaceName}'`;
-
-      if (this.shouldOutputJson(flags)) {
-        this.logJsonStatus("warning", ephemeralSpaceWarning, flags);
-      } else {
-        this.log(formatWarning(ephemeralSpaceWarning));
-      }

As per coding guidelines: "Use this.logJsonResult(data, flags) for one-shot results ... this.logJsonStatus(status, message, flags) for hold/status signals in long-running commands" and "Always use formatResource(name) (cyan) for resource names instead of quoted strings."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/create.ts` around lines 59 - 65, The JSON branch
currently emits a separate status record via logJsonStatus; instead, include the
warning on the one-shot result by calling this.logJsonResult(...) with the
result payload plus a warning field (use ephemeralSpaceWarning content) when
shouldOutputJson(flags) is true and remove the logJsonStatus call; in the
human-readable branch replace the quoted spaceName in ephemeralSpaceWarning with
formatResource(spaceName) and keep using formatWarning(...) when logging; locate
symbols ephemeralSpaceWarning, shouldOutputJson(flags), logJsonStatus,
logJsonResult, formatWarning, formatResource, spaceName and flags to make these
changes.

Comment on lines +119 to +127
formatTimestamp(
formatMessageTimestamp(member.lastEvent.timestamp),
),
);
this.log(formatMemberEventBlock(member, action));
this.log("");
}
} catch (error) {
this.fail(error, flags, "memberSubscribe", { spaceName });
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In the @ably/spacesTypeScript SDK, what is the declared type ofSpaceMember.lastEvent, and can it be undefined on members.subscribe("update", ...) callbacks?

💡 Result:

In the @ably/spaces TypeScript SDK, SpaceMember.lastEvent is declared as an object with properties { name: string; timestamp: number }, based on consistent examples across official documentation and SDK README. It is always present and populated in member objects received in members.subscribe("update", ...) callbacks, as shown in all payload examples for enter, leave, update events (e.g., {"name": "enter", "timestamp": 1677595689759}). No examples show it as undefined or absent, indicating it cannot be undefined in these callbacks.

Citations:


🏁 Script executed:

cat -n src/commands/spaces/members/subscribe.ts | head -150

Repository: ably/ably-cli

Length of output: 5568


🏁 Script executed:

find node_modules/@ably/spaces -name "*.d.ts" -o -name "*.ts" | head -20

Repository: ably/ably-cli

Length of output: 117


🏁 Script executed:

cat package.json | grep -A 5 "@ably/spaces"

Repository: ably/ably-cli

Length of output: 288


Move error handling from callback to outer catch block and use consistent optional chaining.

According to @ably/spaces SDK documentation, SpaceMember.lastEvent is always present in members.subscribe callbacks with { name: string; timestamp: number }, so the direct access on line 120 is safe. However, line 76 treats it as optional with ?.name, creating inconsistency.

More importantly, calling this.fail() inside the memberListener callback (lines 126-127) violates the coding guidelines. Per the guidelines: "In Promise callbacks (e.g., connection event handlers), use reject(new Error(...)) for errors, which propagates to await where the catch block calls this.fail()". Replace the try-catch with reject(new Error(...)) and let the outer catch block handle the error consistently.

If keeping the try-catch for defensive programming, either make both accesses use optional chaining (member.lastEvent?.timestamp) or remove the optional chaining on line 76 to reflect that the property is always defined.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/members/subscribe.ts` around lines 119 - 127, The member
subscribe callback (memberListener) currently calls this.fail(...) inside the
callback and inconsistently accesses member.lastEvent (both direct and with
optional chaining); change the callback to propagate errors via reject(new
Error(...)) instead of calling this.fail directly so the outer await/catch can
call this.fail, and make the lastEvent access consistent across the handler
(either use member.lastEvent?.name and member.lastEvent?.timestamp everywhere
for defensive coding or remove the optional chaining so both places assume
lastEvent is present); update the code around
formatMessageTimestamp/formatTimestamp/formatMemberEventBlock usage and replace
the inner catch's this.fail call with reject(...) so errors bubble to the outer
catch.

Comment on lines +73 to +75
const action = member.lastEvent?.name || "unknown";
const clientId = member.clientId || "Unknown";
const connectionId = member.connectionId || "Unknown";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard lastEvent consistently in the text output path.

Line 73 already treats member.lastEvent as optional, but Line 114 dereferences it unconditionally. If lastEvent is absent, this throws and terminates the whole subscribe command via this.fail().

🐛 Proposed fix
-            this.log(
-              formatTimestamp(
-                formatMessageTimestamp(member.lastEvent.timestamp),
-              ),
-            );
+            this.log(
+              formatTimestamp(
+                formatMessageTimestamp(member.lastEvent?.timestamp),
+              ),
+            );

Also applies to: 112-115

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/spaces/subscribe.ts` around lines 73 - 75, member.lastEvent is
treated as optional earlier (action = member.lastEvent?.name) but later code
dereferences it unconditionally causing a crash (this.fail) when it's missing;
update the output path to consistently guard accesses to lastEvent by using the
already-computed action and clientId/connectionId fallbacks or use optional
chaining/defaults (e.g., member.lastEvent?.name, member.lastEvent?.actor?.id ??
'Unknown', member.lastEvent?.someField ?? 'Unknown') before reading properties,
and ensure any conditional logic that relied on lastEvent checks for its
existence first so the subscribe command no longer throws.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +112 to +116
this.log(
formatTimestamp(
formatMessageTimestamp(member.lastEvent.timestamp),
),
);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

member.lastEvent?.name is treated as optional above, but here member.lastEvent.timestamp is accessed without guarding. If lastEvent is ever missing this will throw from the listener and terminate the command; if it’s guaranteed to exist, consider removing the optional chain/defaults for consistency. Suggest normalizing via a const lastEvent = member.lastEvent and using lastEvent?.timestamp (and lastEvent?.name) or asserting non-null once.

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +52
// Keep track of the last event we've seen for each client to avoid duplicates
const lastSeenEvents = new Map<
string,
{ action: string; timestamp: number }
>();
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lastSeenEvents is keyed per client+connection and never pruned. On long-running subscriptions with many transient connections this can grow without bound. Consider periodically deleting entries older than the dedup window (e.g., when the map exceeds a threshold) or storing only the most recent N keys.

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +122
this.log(
formatTimestamp(
formatMessageTimestamp(member.lastEvent.timestamp),
),
);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as spaces:subscribe: the listener treats member.lastEvent?.name as optional but later reads member.lastEvent.timestamp without guarding. Either treat lastEvent as required consistently (no optional chaining/defaults) or guard both name and timestamp via a single local lastEvent variable to avoid potential runtime errors if the SDK ever emits a member without lastEvent.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants