Skip to content

Conversation

@sacOO7
Copy link
Collaborator

@sacOO7 sacOO7 commented Aug 11, 2025

Fixed #1145

Summary by CodeRabbit

  • New Features

    • New unified channel retrieval API that returns a channel or signals a not-found error.
  • Bug Fixes

    • Returns empty channel-mode list instead of crashing when modes are absent.
    • Ensures channels are attached before accessing Live Objects root.
  • Refactor

    • Removed legacy per-channel/per-message methods and consolidated adapter/channel helper flows; added coroutine-friendly attachment helpers.
  • Tests

    • Added and updated unit/integration tests to cover new helper flows and behaviors.

@coderabbitai
Copy link

coderabbitai bot commented Aug 11, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Reworks LiveObjects adapter to surface ChannelBase via getChannel, removes several per-message/channel APIs, adds coroutine-based attach helpers and implicit attach before getRoot, adds ChannelMode null-safety, and updates tests to the new flows (no public API signature changes besides getChannel).

Changes

Cohort / File(s) Summary
Adapter / Interface
lib/src/main/java/io/ably/lib/objects/Adapter.java, lib/src/main/java/io/ably/lib/objects/LiveObjectsAdapter.java
Removed legacy methods (send, setChannelSerial, maxMessageSizeLimit, getChannelModes, getChannelState). Added @NotNull ChannelBase getChannel(String) that throws 404 AblyException when channel missing. Updated imports and error handling to use ChannelBase and ErrorInfo.
Channel null-safety
lib/src/main/java/io/ably/lib/realtime/ChannelBase.java
Added null-guard in getModes() to return an empty ChannelMode[] when internal modes are null, preventing NPE.
LiveObjects core + helpers
live-objects/src/main/kotlin/io/ably/lib/objects/DefaultLiveObjects.kt, live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt
getRootAsync() now ensures channel attached via adapter.ensureAttached(channelName) before sync/root retrieval. Added suspend helpers: attachAsync, ensureAttached, and getChannelModes. Replaced internal send/size calls to use connectionManager, refactored validations, and set channelSerial directly on channel properties.
Integration tests
live-objects/src/test/kotlin/.../integration/DefaultLiveObjectsTest.kt, .../integration/setup/IntegrationTest.kt, .../integration/setup/Sandbox.kt
Simplified getRealtimeChannel (removed autoAttach), removed channel-level ensureAttached helper; tests updated to rely on adapter helpers/implicit attach semantics.
Unit tests added/updated
live-objects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt, .../ObjectMessageSizeTest.kt, .../TestHelpers.kt, .../UtilsTest.kt
Added HelpersTest exercising new helpers and validation; replaced maxMessageSizeLimit() stubs with connectionManager.maxMessageSize; stubbed getChannel in test helpers; increased a test delay in UtilsTest.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant DefaultLiveObjects
  participant Helpers
  participant Adapter
  participant Channel

  Client->>DefaultLiveObjects: getRootAsync(channelName)
  DefaultLiveObjects->>Adapter: getChannel(channelName)
  Adapter-->>DefaultLiveObjects: ChannelBase
  DefaultLiveObjects->>Helpers: ensureAttached(channelName)
  Helpers->>Adapter: getChannel(channelName)
  Adapter-->>Helpers: ChannelBase
  alt channel.state == initialized
    Helpers->>Channel: attach(CompletionListener)
    Channel-->>Helpers: attached / error
  else channel.state == attaching
    Helpers-->>Helpers: await attach completion
  else other state
    Helpers-->>DefaultLiveObjects: throw error
  end
  Helpers-->>DefaultLiveObjects: attached
  DefaultLiveObjects-->>Client: return Root object
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Assessment against linked issues

Objective Addressed Explanation
Implement implicit attach in objects.getRoot when channel is initialized; error otherwise (GH-1145, ECO-5517)
Ensure attach flow similar to ensureAttached pattern (GH-1145, ECO-5517)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Removal of send, maxMessageSizeLimit, getChannelModes, getChannelState APIs (lib/src/main/java/io/ably/lib/objects/Adapter.java, lib/src/main/java/io/ably/lib/objects/LiveObjectsAdapter.java) These API removals are broader API surface changes not required by the implicit-attach objective.
ChannelBase.getModes null-guard (lib/src/main/java/io/ably/lib/realtime/ChannelBase.java) Defensive change unrelated to implementing implicit attach for getRoot; it addresses potential NPEs in mode access.
Switching send/size usage to ConnectionManager (live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt) Changing message send path and max-size source is outside the implicit-attach goal and affects message handling behavior.
Test timing increase in UtilsTest (live-objects/src/test/kotlin/io/ably/lib/objects/unit/UtilsTest.kt) Test-only timing adjustment unrelated to the implicit-attach implementation.

Poem

I’m a rabbit patching code tonight,
I hop and bind the channel right.
From initialized to attached we spring,
Root maps wake and tests all sing.
Carrots, logs, and green CI light 🥕🐇


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2394558 and a035ee7.

📒 Files selected for processing (4)
  • lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (1 hunks)
  • live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt (5 hunks)
  • live-objects/src/test/kotlin/io/ably/lib/objects/integration/setup/IntegrationTest.kt (2 hunks)
  • live-objects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • live-objects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt
  • live-objects/src/test/kotlin/io/ably/lib/objects/integration/setup/IntegrationTest.kt
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-12T11:48:24.317Z
Learnt from: sacOO7
PR: ably/ably-java#1146
File: live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt:32-46
Timestamp: 2025-08-12T11:48:24.317Z
Learning: In the Ably Java SDK, channel attach operations have internal timeout mechanisms that will report issues if the attach fails or times out, so external timeout wrapping with withTimeout is not needed for LiveObjectsAdapter.attachAsync calls.

Applied to files:

  • live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt
📚 Learning: 2025-05-27T12:11:25.084Z
Learnt from: sacOO7
PR: ably/ably-java#1087
File: lib/src/main/java/io/ably/lib/objects/LiveObjectsAdapter.java:32-34
Timestamp: 2025-05-27T12:11:25.084Z
Learning: In LiveObjects implementation (lib/src/main/java/io/ably/lib/objects/LiveObjectsAdapter.java), the send method intentionally hardcodes queueEvents to true rather than respecting ably.options.queueMessages. This is because LiveObjects requires reliable message delivery to ensure proper state synchronization and acknowledgment, unlike other realtime components that may allow configurable queuing behavior.

Applied to files:

  • live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt
🧬 Code Graph Analysis (1)
live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt (1)
live-objects/src/main/kotlin/io/ably/lib/objects/Utils.kt (2)
  • ablyException (10-18)
  • ablyException (20-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-liveobjects
  • GitHub Check: check-rest-okhttp
  • GitHub Check: check-rest
  • GitHub Check: check-realtime
  • GitHub Check: check
  • GitHub Check: check (19)
  • GitHub Check: check (21)
  • GitHub Check: check (29)
  • GitHub Check: check (24)
  • GitHub Check: build
🔇 Additional comments (9)
lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (1)

1289-1292: LGTM! Null safety improved

The change from returning null to returning an empty array when modes is null improves null safety and maintains backward compatibility with existing callers that expect a non-null array.

live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt (8)

8-8: LGTM!

Adding CompletableDeferred import is necessary for the new ensureAttached functionality.


16-30: LGTM!

The sendAsync implementation correctly uses the connection manager's send method with proper error handling and coroutine continuation management.


48-73: LGTM! Channel mode resolution logic is well-documented

The implementation correctly follows the specification by:

  1. First checking channel.modes (RTO2a - only populated on channel attachment)
  2. Falling back to channel.options.modes (RTO2b - user provided channel options)
  3. Returning null when neither is available

The logic and documentation are clear and appropriate.


79-79: LGTM!

Correctly updates to use connectionManager.maxMessageSize instead of the previous method.


91-92: LGTM!

Direct assignment to getChannel(channelName).properties.channelSerial is more straightforward than the previous delegation approach.


94-119: LGTM! Robust channel attachment handling

The ensureAttached implementation correctly handles all channel states:

  • initialized: Calls attachAsync to initiate attachment
  • attached: Returns immediately (already attached)
  • attaching: Waits for completion using CompletableDeferred with proper state checking
  • Other states: Throws appropriate exception

The double-check at Line 111-113 prevents race conditions where the channel might become attached between the listener registration and the state check.


141-141: LGTM!

Correctly reducing visibility of helper methods from internal to private since they're only used within this file.

Also applies to: 149-149


124-124: LGTM!

The reordered validation logic now correctly validates channel state first, then channel mode requirements, which is more logical since channel mode checking depends on having a valid channel state.

Also applies to: 130-130

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/getroot-implicit-attach

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot temporarily deployed to staging/pull/1146/features August 11, 2025 10:46 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1146/javadoc August 11, 2025 10:47 Inactive
@sacOO7 sacOO7 force-pushed the fix/getroot-implicit-attach branch from ceb246c to 5ad4aec Compare August 11, 2025 11:22
@github-actions github-actions bot temporarily deployed to staging/pull/1146/features August 11, 2025 11:23 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1146/javadoc August 11, 2025 11:26 Inactive
@sacOO7 sacOO7 force-pushed the fix/getroot-implicit-attach branch from 5ad4aec to 047b7a3 Compare August 11, 2025 11:27
@github-actions github-actions bot temporarily deployed to staging/pull/1146/features August 11, 2025 11:28 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1146/javadoc August 11, 2025 11:30 Inactive
- Adjusted channel state checks prior to channel modes for read
and write objects operations
- Added getChannel method to adapter interface, added relevant ensureAttached
method with relevant implementation
@sacOO7 sacOO7 force-pushed the fix/getroot-implicit-attach branch from 047b7a3 to a02b31a Compare August 11, 2025 11:34
@github-actions github-actions bot temporarily deployed to staging/pull/1146/features August 11, 2025 11:35 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1146/javadoc August 11, 2025 11:37 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1146/features August 11, 2025 12:51 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1146/javadoc August 11, 2025 12:53 Inactive
…lpers

- Removed unnecessary implementation from Adapter, made it clean
- Updated unit tests/mocks accordingly
@github-actions github-actions bot temporarily deployed to staging/pull/1146/features August 12, 2025 09:05 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1146/javadoc August 12, 2025 09:07 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1146/features August 12, 2025 10:32 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1146/javadoc August 12, 2025 10:35 Inactive
@sacOO7 sacOO7 force-pushed the fix/getroot-implicit-attach branch from 4b32a52 to 61cb6b2 Compare August 12, 2025 11:09
@github-actions github-actions bot temporarily deployed to staging/pull/1146/features August 12, 2025 11:09 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1146/javadoc August 12, 2025 11:11 Inactive
@sacOO7 sacOO7 requested review from ttypic and removed request for ttypic August 12, 2025 11:11
@sacOO7 sacOO7 marked this pull request as ready for review August 12, 2025 11:11
@sacOO7 sacOO7 requested a review from Copilot August 12, 2025 11:15
@sacOO7
Copy link
Collaborator Author

sacOO7 commented Aug 12, 2025

@coderabbitai review

Copy link
Contributor

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

This PR implements implicit channel attachment functionality for the getRoot method as part of ECO-5517. The main purpose is to ensure that channels are automatically attached when accessing live objects, eliminating the need for explicit attachment calls.

Key changes include:

  • Implementation of implicit attach in getRoot method via ensureAttached helper
  • Refactoring of the LiveObjectsAdapter interface to use direct channel access patterns
  • Addition of comprehensive test coverage for channel attachment and validation logic

Reviewed Changes

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

Show a summary per file
File Description
DefaultLiveObjects.kt Added ensureAttached call to getRoot method for implicit attachment
Helpers.kt Implemented ensureAttached, attachAsync, and refactored channel validation logic
LiveObjectsAdapter.java Simplified interface to focus on essential operations, removed redundant methods
Adapter.java Updated implementation to align with simplified interface
ChannelBase.java Added null safety check for channel modes
HelpersTest.kt Added comprehensive unit tests for new helper functions
TestHelpers.kt Enhanced mock setup to return proper channel instances
ObjectMessageSizeTest.kt Updated to use direct property access instead of removed adapter methods
UtilsTest.kt Increased delay timeout for test stability
Sandbox.kt Removed redundant ensureAttached extension function
IntegrationTest.kt Removed auto-attach parameter as attachment is now implicit
DefaultLiveObjectsTest.kt Updated test call to reflect removed auto-attach parameter

@coderabbitai
Copy link

coderabbitai bot commented Aug 12, 2025

✅ 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.

1 similar comment
@coderabbitai
Copy link

coderabbitai bot commented Aug 12, 2025

✅ 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: 5

♻️ Duplicate comments (3)
live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt (3)

111-113: Race condition issue has been addressed.

The previous review comment about the race condition between the when expression check and the additional check at Line 111-113 appears to have been addressed. The current implementation correctly handles the case where the channel might already be attached before the listener is registered.


142-144: Control flow comment is outdated.

The previous comment about confusing logic flow with early returns inside let blocks has been addressed. The current implementation uses clearer control flow with explicit null checks.


149-155: Logic inversion issue has been resolved.

The previous comment about inverted logic has been addressed. The method now correctly checks if the channel state is in one of the invalid states and throws an exception when found.

🧹 Nitpick comments (4)
live-objects/src/test/kotlin/io/ably/lib/objects/integration/DefaultLiveObjectsTest.kt (1)

36-36: LGTM; consider asserting implicit attach triggered by getRoot

This migration to the new channel helper looks good. To harden coverage for ECO-5517, add an assertion in testObjectsSync verifying that calling channel.objects.root from an INITIALIZED channel results in the channel becoming ATTACHED, ensuring the implicit attach behavior is exercised explicitly.

Example insertion after Line 71:

import io.ably.lib.realtime.ChannelState

// Ensure implicit attach occurred when fetching root
assertWaiter { channel.state == ChannelState.attached }

Would you like me to add negative-path tests (e.g., suspended/failed channel) to assert that getRoot returns an appropriate error?

live-objects/src/test/kotlin/io/ably/lib/objects/unit/UtilsTest.kt (1)

256-256: Use awaitCancellation() instead of an arbitrary long delay

For deterministic cancellation tests, prefer suspending until cancellation over a large delay. This avoids any risk of real-time waiting if the cancellation timing changes.

Apply this change:

-      delay(10000) // Long delay
+      kotlinx.coroutines.awaitCancellation() // Suspend until cancelled

And add this import outside the selected range if not present:

import kotlinx.coroutines.awaitCancellation
live-objects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt (1)

248-266: Fix the inconsistent spacing in the test method.

There's an extra space at the beginning of lines 248-266 that should be removed for consistency with the rest of the file.

-   @Test
-   fun testEnsureAttachedAttachingButReceivesNonAttachedEmitsError() = runTest {
-     val adapter = mockk<LiveObjectsAdapter>(relaxed = true)
-     val channel = mockk<Channel>(relaxed = true)
-     every { adapter.getChannel("ch") } returns channel
-     channel.state = ChannelState.attaching
-     every { channel.once(any()) } answers {
-       val listener = firstArg<ChannelStateListener>()
-       val stateChange = mockk<ChannelStateListener.ChannelStateChange>(relaxed = true) {
-         setPrivateField("current", ChannelState.suspended)
-         setPrivateField("reason", clientError("Not attached").errorInfo)
-       }
-       listener.onChannelStateChanged(stateChange)
-     }
-     val ex = assertFailsWith<AblyException> { adapter.ensureAttached("ch") }
-      assertEquals(ErrorCode.ChannelStateError.code, ex.errorInfo.code)
-      assertTrue(ex.errorInfo.message.contains("Not attached"))
-      verify(exactly = 1) { channel.once(any()) }
-   }
+  @Test
+  fun testEnsureAttachedAttachingButReceivesNonAttachedEmitsError() = runTest {
+    val adapter = mockk<LiveObjectsAdapter>(relaxed = true)
+    val channel = mockk<Channel>(relaxed = true)
+    every { adapter.getChannel("ch") } returns channel
+    channel.state = ChannelState.attaching
+    every { channel.once(any()) } answers {
+      val listener = firstArg<ChannelStateListener>()
+      val stateChange = mockk<ChannelStateListener.ChannelStateChange>(relaxed = true) {
+        setPrivateField("current", ChannelState.suspended)
+        setPrivateField("reason", clientError("Not attached").errorInfo)
+      }
+      listener.onChannelStateChanged(stateChange)
+    }
+    val ex = assertFailsWith<AblyException> { adapter.ensureAttached("ch") }
+    assertEquals(ErrorCode.ChannelStateError.code, ex.errorInfo.code)
+    assertTrue(ex.errorInfo.message.contains("Not attached"))
+    verify(exactly = 1) { channel.once(any()) }
+  }
lib/src/main/java/io/ably/lib/objects/Adapter.java (1)

36-44: Consider using a more specific error code for channel not found.

The method correctly implements the new getChannel API, but using a generic 404 status code might not align with Ably's error code conventions. Consider using a more specific error code from the Ably error code range.

 @Override
 public @NotNull ChannelBase getChannel(@NotNull String channelName) throws AblyException {
     if (ably.channels.containsKey(channelName)) {
         return ably.channels.get(channelName);
     } else {
         Log.e(TAG, "attachChannel(): channel not found: " + channelName);
-        ErrorInfo errorInfo = new ErrorInfo("Channel not found: " + channelName, 404);
+        ErrorInfo errorInfo = new ErrorInfo("Channel not found: " + channelName, 40400, 404);
         throw AblyException.fromErrorInfo(errorInfo);
     }
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 631a5dc and 61cb6b2.

📒 Files selected for processing (12)
  • lib/src/main/java/io/ably/lib/objects/Adapter.java (2 hunks)
  • lib/src/main/java/io/ably/lib/objects/LiveObjectsAdapter.java (2 hunks)
  • lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (1 hunks)
  • live-objects/src/main/kotlin/io/ably/lib/objects/DefaultLiveObjects.kt (1 hunks)
  • live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt (5 hunks)
  • live-objects/src/test/kotlin/io/ably/lib/objects/integration/DefaultLiveObjectsTest.kt (1 hunks)
  • live-objects/src/test/kotlin/io/ably/lib/objects/integration/setup/IntegrationTest.kt (2 hunks)
  • live-objects/src/test/kotlin/io/ably/lib/objects/integration/setup/Sandbox.kt (0 hunks)
  • live-objects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt (1 hunks)
  • live-objects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSizeTest.kt (2 hunks)
  • live-objects/src/test/kotlin/io/ably/lib/objects/unit/TestHelpers.kt (1 hunks)
  • live-objects/src/test/kotlin/io/ably/lib/objects/unit/UtilsTest.kt (1 hunks)
💤 Files with no reviewable changes (1)
  • live-objects/src/test/kotlin/io/ably/lib/objects/integration/setup/Sandbox.kt
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-08-07T07:17:33.340Z
Learnt from: sacOO7
PR: ably/ably-java#1137
File: live-objects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/LiveMapManagerTest.kt:6-6
Timestamp: 2025-08-07T07:17:33.340Z
Learning: In the ably-java LiveObjects test code, there are extension properties defined in TestHelpers.kt that provide access to private fields of classes for testing purposes. For example, `internal var DefaultLiveMap.LiveMapManager: LiveMapManager` allows tests to access the private `liveMapManager` field. These extension imports (like `import io.ably.lib.objects.unit.LiveMapManager`) should not be removed as they are necessary for test functionality and are not conflicting imports.

Applied to files:

  • live-objects/src/test/kotlin/io/ably/lib/objects/unit/TestHelpers.kt
  • live-objects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt
📚 Learning: 2025-08-01T09:53:16.730Z
Learnt from: sacOO7
PR: ably/ably-java#1120
File: live-objects/src/test/kotlin/io/ably/lib/objects/integration/DefaultLiveObjectsTest.kt:0-0
Timestamp: 2025-08-01T09:53:16.730Z
Learning: In the ably-java LiveObjects test code, extension properties with capital letter names (like `State`, `ObjectId`) are defined in live-objects/src/test/kotlin/io/ably/lib/objects/integration/helpers/Utils.kt to provide access to internal fields of concrete implementations through their public interfaces. For example, `LiveObjects.State` casts to `DefaultLiveObjects` to access the internal `state` field for testing purposes.

Applied to files:

  • live-objects/src/test/kotlin/io/ably/lib/objects/unit/TestHelpers.kt
  • live-objects/src/test/kotlin/io/ably/lib/objects/integration/DefaultLiveObjectsTest.kt
  • live-objects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt
📚 Learning: 2025-08-06T09:22:40.964Z
Learnt from: sacOO7
PR: ably/ably-java#1135
File: live-objects/src/main/kotlin/io/ably/lib/objects/DefaultLiveObjects.kt:100-134
Timestamp: 2025-08-06T09:22:40.964Z
Learning: In DefaultLiveObjects.kt, the object creation pattern using objectsPool.get() followed by withContext(sequentialScope.coroutineContext) { objectsPool.createZeroValueObjectIfNotExists() } is thread-safe because sequentialScope uses limitedParallelism(1) ensuring sequential execution, and createZeroValueObjectIfNotExists() performs an internal get() check before creating, preventing duplicate object creation even when multiple coroutines initially see null from the first get() call.

Applied to files:

  • live-objects/src/test/kotlin/io/ably/lib/objects/unit/TestHelpers.kt
📚 Learning: 2025-08-01T10:30:27.049Z
Learnt from: sacOO7
PR: ably/ably-java#1130
File: live-objects/src/test/kotlin/io/ably/lib/objects/integration/DefaultLiveMapTest.kt:241-241
Timestamp: 2025-08-01T10:30:27.049Z
Learning: In DefaultLiveMapTest.kt integration tests, operations are performed sequentially one after another, and subscription callback list updates happen one at a time, so thread-safe collections are not needed for collecting updates in test scenarios.

Applied to files:

  • live-objects/src/test/kotlin/io/ably/lib/objects/integration/DefaultLiveObjectsTest.kt
📚 Learning: 2025-06-23T14:18:25.315Z
Learnt from: sacOO7
PR: ably/ably-java#1106
File: live-objects/src/main/kotlin/io/ably/lib/objects/serialization/Serialization.kt:25-29
Timestamp: 2025-06-23T14:18:25.315Z
Learning: In the ably-java codebase, the DefaultLiveObjectSerializer class methods like writeMsgpackArray will have their type signatures updated to be non-nullable since the calling sites ensure objects are never null when passed to these methods.

Applied to files:

  • lib/src/main/java/io/ably/lib/objects/LiveObjectsAdapter.java
📚 Learning: 2025-05-27T12:11:25.084Z
Learnt from: sacOO7
PR: ably/ably-java#1087
File: lib/src/main/java/io/ably/lib/objects/LiveObjectsAdapter.java:32-34
Timestamp: 2025-05-27T12:11:25.084Z
Learning: In LiveObjects implementation (lib/src/main/java/io/ably/lib/objects/LiveObjectsAdapter.java), the send method intentionally hardcodes queueEvents to true rather than respecting ably.options.queueMessages. This is because LiveObjects requires reliable message delivery to ensure proper state synchronization and acknowledgment, unlike other realtime components that may allow configurable queuing behavior.

Applied to files:

  • lib/src/main/java/io/ably/lib/objects/LiveObjectsAdapter.java
  • live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-realtime
🔇 Additional comments (5)
live-objects/src/main/kotlin/io/ably/lib/objects/DefaultLiveObjects.kt (1)

94-99: LGTM! The implicit attach implementation correctly implements the PR objectives.

The addition of adapter.ensureAttached(channelName) before syncing state correctly implements the implicit attach behavior for LiveObject.objects.getRoot as specified in issue #1145. The implementation follows the expected pattern where channels in the initialized state are implicitly attached, and appropriate errors are thrown for invalid states.

live-objects/src/test/kotlin/io/ably/lib/objects/unit/TestHelpers.kt (1)

47-51: LGTM! The mock adapter setup correctly supports the new channel-based API.

The updated mock configuration properly stubs the new getChannel method to return a consistent mock channel, aligning with the refactored adapter interface that now provides channel access through getChannel(String) rather than per-message APIs.

live-objects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt (1)

1-18: LGTM! Comprehensive test coverage for the new helper functions.

The test file provides thorough coverage of the new helper functions introduced in Helpers.kt, including edge cases and error scenarios for channel attachment, mode validation, and state management.

live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt (1)

16-30: LGTM! The sendAsync implementation correctly respects queueMessages configuration.

The updated implementation properly uses clientOptions.queueMessages instead of hardcoding true, which aligns with the broader refactoring while maintaining the necessary queuing behavior for LiveObjects.

lib/src/main/java/io/ably/lib/objects/LiveObjectsAdapter.java (1)

3-3: Import of ChannelBase aligns with the new channel-centric adapter API

Using ChannelBase in the adapter interface is consistent with the PR’s direction to centralize channel operations. No issues here.

@sacOO7 sacOO7 force-pushed the fix/getroot-implicit-attach branch from 61cb6b2 to 2394558 Compare August 12, 2025 11:35
@github-actions github-actions bot temporarily deployed to staging/pull/1146/features August 12, 2025 11:35 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1146/javadoc August 12, 2025 11:38 Inactive
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: 1

♻️ Duplicate comments (2)
live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt (2)

149-154: Simplify channel state validation logic

The logic checks if the current state is null OR if it's in the invalid states array, which is confusing. A null state should probably be handled separately from checking invalid states.

Clarify the logic by separating null check from state validation:

 private fun LiveObjectsAdapter.throwIfInChannelState(channelName: String, channelStates: Array<ChannelState>) {
   val currentState = getChannel(channelName).state
-  if (currentState == null || channelStates.contains(currentState)) {
-    throw ablyException("Channel is in invalid state: $currentState", ErrorCode.ChannelStateError)
+  if (currentState == null) {
+    throw ablyException("Channel state is null", ErrorCode.ChannelStateError)
+  }
+  if (channelStates.contains(currentState)) {
+    throw ablyException("Channel is in invalid state: $currentState", ErrorCode.ChannelStateError)
   }
 }

111-113: Handle race condition more robustly

The current implementation has a potential race condition where the channel state could change between registering the listener and this check. While the check on line 111-113 attempts to handle this, there's still a window where the state could become attached after the listener is registered but before this check, potentially causing the deferred to complete twice.

Consider using completeExceptionally with a more defensive approach:

       }
     }
-    if (channel.state == ChannelState.attached) {
-      attachDeferred.complete(Unit)
-    }
+    // Use tryComplete to avoid completing twice
+    channel.state?.let { state ->
+      if (state == ChannelState.attached && !attachDeferred.isCompleted) {
+        attachDeferred.complete(Unit)
+      }
+    }
     attachDeferred.await()
🧹 Nitpick comments (2)
live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt (2)

56-73: Potential NPE when accessing channel.options

The code accesses channel.options?.let but doesn't handle the case where channel.options could be null. Additionally, the nested options.hasModes() check might not be necessary if you're already checking for null.

Consider simplifying the null checks:

 internal fun LiveObjectsAdapter.getChannelModes(channelName: String): Array<ChannelMode>? {
   val channel = getChannel(channelName)
 
   // RTO2a - channel.modes is only populated on channel attachment, so use it only if it is set
   channel.modes?.let { modes ->
     if (modes.isNotEmpty()) {
       return modes
     }
   }
 
   // RTO2b - otherwise as a best effort use user provided channel options
-  channel.options?.let { options ->
-    if (options.hasModes()) {
-      return options.modes
-    }
-  }
-  return null
+  return channel.options?.takeIf { it.hasModes() }?.modes
 }

94-119: Consider extracting the attaching state wait logic for better testability

The ensureAttached function handles multiple states with complex async logic. The attaching state handling is particularly complex with the CompletableDeferred pattern.

Consider extracting the attaching state wait logic into a separate helper function for better testability and readability:

private suspend fun LiveObjectsAdapter.waitForChannelAttach(channelName: String): Unit {
    val attachDeferred = CompletableDeferred<Unit>()
    val channel = getChannel(channelName)
    
    channel.once {
        when(it.current) {
            ChannelState.attached -> attachDeferred.complete(Unit)
            else -> {
                val exception = ablyException("Channel $channelName is in invalid state: ${it.current}, " +
                    "error: ${it.reason}", ErrorCode.ChannelStateError)
                attachDeferred.completeExceptionally(exception)
            }
        }
    }
    
    // Handle race condition where channel becomes attached before listener is set
    if (channel.state == ChannelState.attached) {
        attachDeferred.complete(Unit)
    }
    
    attachDeferred.await()
}

This would simplify the main function and make the attaching state logic independently testable.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 61cb6b2 and 2394558.

📒 Files selected for processing (2)
  • live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt (5 hunks)
  • live-objects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • live-objects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-27T12:11:25.084Z
Learnt from: sacOO7
PR: ably/ably-java#1087
File: lib/src/main/java/io/ably/lib/objects/LiveObjectsAdapter.java:32-34
Timestamp: 2025-05-27T12:11:25.084Z
Learning: In LiveObjects implementation (lib/src/main/java/io/ably/lib/objects/LiveObjectsAdapter.java), the send method intentionally hardcodes queueEvents to true rather than respecting ably.options.queueMessages. This is because LiveObjects requires reliable message delivery to ensure proper state synchronization and acknowledgment, unlike other realtime components that may allow configurable queuing behavior.

Applied to files:

  • live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt
🧬 Code Graph Analysis (1)
live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt (1)
live-objects/src/main/kotlin/io/ably/lib/objects/Utils.kt (2)
  • ablyException (10-18)
  • ablyException (20-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: check-liveobjects
  • GitHub Check: check-realtime
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-rest
  • GitHub Check: check-rest-okhttp
  • GitHub Check: check
  • GitHub Check: check (29)
  • GitHub Check: check (21)
  • GitHub Check: build
  • GitHub Check: check (24)
  • GitHub Check: check (19)
🔇 Additional comments (2)
live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt (2)

122-125: Verify state checks are performed before mode checks

The implementation correctly checks channel state before mode validation, which aligns with the error handling priority mentioned in the summary.


18-18: Ensure reliable queuing for LiveObjects sendAsync
The Kotlin adapter is now forwarding clientOptions.queueMessages to connectionManager.send, whereas the Java implementation hard-coded queuing to true to guarantee reliable state synchronization. If a user’s queueMessages is ever false, LiveObjects operations could fail rather than queue on disconnect.

• File: live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt
Line 18: connectionManager.send(message, clientOptions.queueMessages, …)

Please verify that allowing queueMessages = false won’t compromise LiveObjects reliability. If not, consider hard-coding this argument to true as before.

Copy link
Contributor

@ttypic ttypic left a comment

Choose a reason for hiding this comment

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

LGTM

@sacOO7 sacOO7 merged commit dac477f into main Aug 14, 2025
13 checks passed
@sacOO7 sacOO7 deleted the fix/getroot-implicit-attach branch August 14, 2025 10:47
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.

LiveObject: Implement implicit attach for objects.getRoot

3 participants