Skip to content

Conversation

@ttypic
Copy link
Contributor

@ttypic ttypic commented Jan 6, 2026

Replaced rest message edits and deletes with realtime for realtime client. Spec PR https://github.com/ably/specification/pull/406/files

Summary by CodeRabbit

  • New Features

    • Publish and presence operations now return per-message publish results (including serials) via callbacks; publish results are exposed to callers.
  • Bug Fixes

    • Message update/delete/append now funnel through a single validated async path with improved queuing and per-item acknowledgement delivery.
    • Presence and connection send flows updated to deliver per-message results and error details reliably.
  • Tests

    • Added end-to-end tests for publish, update, append, delete and message version/history workflows.

✏️ Tip: You can customize this high-level summary in your review settings.

@ttypic ttypic requested a review from sacOO7 January 6, 2026 11:17
@coderabbitai
Copy link

coderabbitai bot commented Jan 6, 2026

Walkthrough

Centralizes update/delete/append message handling into ChannelBase.updateDeleteImpl, replaces CompletionListener with typed Callback across send/ack paths, embeds per-message PublishResult[] in ProtocolMessage.res, adds PublishResult type and listener adapters, and updates tests for realtime message edits and versions.

Changes

Cohort / File(s) Summary
Channel API
lib/src/main/java/io/ably/lib/realtime/ChannelBase.java
Public update/delete/append overloads converted to void + Callback and delegate to new private updateDeleteImpl(...); publish/send now wrap listeners via Listeners.fromCompletionListener(...).
Transport / Ack flow
lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
send/sendImpl and queued message listeners changed from CompletionListener to Callback<PublishResult>; ack now accepts optional PublishResult[] results and delivers per-item results using index-based iteration.
Protocol model
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java
Added nullable public PublishResult[] res and integrated MsgPack serialization/deserialization for res.
PublishResult type
lib/src/main/java/io/ably/lib/types/PublishResult.java
New PublishResult class with @Nullable String[] serials, JSON/MsgPack (de)serialization, MsgPack array helpers, and an HTTP body handler.
Listener utilities
lib/src/main/java/io/ably/lib/util/Listeners.java
Added toPublishResultListener(Callback<UpdateDeleteResult>), unwrap(Callback<T>), and adapter UpdateResultToPublishAdapter to map PublishResultUpdateDeleteResult; added unwrap helper.
Presence path
lib/src/main/java/io/ably/lib/realtime/Presence.java
Queued presence and send logic switched to Callback<PublishResult>; queued presence stores Listeners.unwrap(listener) and sending uses Listeners.fromCompletionListener(...).
Tests — new / updated
lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java, lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java, lib/src/test/java/io/ably/lib/test/realtime/RealtimeSuite.java
Added realtime message edit/versioning tests; updated tests to use Callback<PublishResult> and included new test in suite.
Kotlin helper & tests
liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt, liveobjects/src/test/.../HelpersTest.kt
Coroutines and unit tests switched from CompletionListener/Unit to Callback<PublishResult> and now resume/verify with PublishResult.
Misc (imports/logging)
multiple files lib/src/...
Added imports for PublishResult, Listeners, MessageAction; added logging in update/delete/append/publish paths; wrapped/unwrapped listeners at send sites.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Channel as ChannelBase
    participant Validator as updateDeleteImpl
    participant ListenersUtil as Listeners
    participant ConnMgr as ConnectionManager
    participant Server

    Client->>Channel: updateMessage(message, op, Callback<UpdateDeleteResult>)
    Channel->>Validator: updateDeleteImpl(message, op, action, listener)
    Validator->>Validator: validate serial, clientId, version, connection/queue rules
    Validator->>ListenersUtil: toPublishResultListener(listener)
    Validator->>ConnMgr: send(ProtocolMessage{action, version, ...}, Callback<PublishResult>)
    ConnMgr->>Server: transmit ProtocolMessage
    Server-->>ConnMgr: ACK with PublishResult[] (ProtocolMessage.res)
    ConnMgr->>ConnMgr: onAck -> map per-message PublishResult[] to queued callbacks
    ConnMgr->>ListenersUtil: invoke Callback<PublishResult>.onSuccess(result)
    ListenersUtil->>Channel: adapter converts PublishResult -> UpdateDeleteResult
    Channel->>Client: original Callback<UpdateDeleteResult>.onSuccess(result)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 I nudged the serials, hopped them in rows,

Wrapped callbacks snug where the publish-result flows,
ACKs now carry res tucked tight in their chest,
Updates, appends, deletes all travel one quest,
A rabbit's cheer for the async request.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'AIT-98 feat: realtime edits and deletes' accurately describes the main change—converting message edit/delete operations from REST to realtime for the realtime client.
Linked Issues check ✅ Passed The PR implements realtime message edits and deletes as indicated by the linked AIT-98 issue, with changes across ChannelBase, Presence, ConnectionManager, and supporting types to enable callback-based async operations.
Out of Scope Changes check ✅ Passed All changes are scoped to realtime message edits/deletes implementation: ChannelBase refactoring, callback integration in ConnectionManager/Presence, PublishResult type addition, and related test coverage.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch AIT-98/message-edits-deletes

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.

@github-actions github-actions bot temporarily deployed to staging/pull/1183/features January 6, 2026 11:17 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: 5

🤖 Fix all issues with AI Agents
In @lib/src/main/java/io/ably/lib/realtime/ChannelBase.java:
- Around line 1357-1396: The ProtocolMessage is sending the original Message
object instead of the constructed updatedMessage, so the action, version and
encoded data are lost; change the send call to use updatedMessage (i.e., set
msg.messages = new Message[] { updatedMessage }) so the ProtocolMessage includes
the modified action/version/encoded payload before connectionManager.send(...).
- Around line 1374-1381: The code in ChannelBase constructs an updatedMessage
(setting action and version fields) but then sends the original message, and
updatedMessage also never copies the original message.serial; fix by sending the
updatedMessage and ensuring its serial is copied from the original message:
populate updatedMessage.serial = message.serial after building updatedMessage,
and replace msg.messages = new Message[] { message } with msg.messages = new
Message[] { updatedMessage } (or remove updatedMessage construction if you
intend to send the original message instead).

In @lib/src/main/java/io/ably/lib/types/PublishResult.java:
- Around line 52-63: In PublishResult msgpack deserialization the inner loop
declares j but mistakenly tests and increments i, corrupting the outer loop and
leaving serials mostly unset; change the inner loop to iterate on j (e.g., for
(int j = 0; j < count; j++) ) and use j++ so unpacker results are stored into
serials[j] while leaving the outer variable i untouched, keeping the existing
nil check/unpack logic and final return new PublishResult(serials).

In
@lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java:
- Around line 309-324: The test getMessageVersions_async currently only
publishes and reads history synchronously; update it to call the asynchronous
API getMessageVersionsAsync for the published message (using the
publishedMessage.serial from waitForMessageAppearInHistory) and wait for the
async result (future/callback) to complete with a timeout; then assert that the
returned versions array is non-empty and contains an entry whose serial equals
publishedMessage.serial (and any other expected fields). Ensure you handle
errors by failing the test on exception and cleanly await the async completion
before finishing the test.
- Around line 39-45: Add an @After cleanup method (e.g., tearDownAfter) to close
the AblyRealtime instance created in setUpBefore: implement a method annotated
with @After that checks if the field ably is non-null, calls ably.close() (or
the appropriate shutdown method on AblyRealtime), catches/logs any exceptions,
and sets ably to null to avoid resource leaks between tests.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9c5ac4a and 2846cde.

📒 Files selected for processing (8)
  • lib/src/main/java/io/ably/lib/realtime/ChannelBase.java
  • lib/src/main/java/io/ably/lib/realtime/Presence.java
  • lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
  • lib/src/main/java/io/ably/lib/types/ProtocolMessage.java
  • lib/src/main/java/io/ably/lib/types/PublishResult.java
  • lib/src/main/java/io/ably/lib/util/Listeners.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java
🧰 Additional context used
🧬 Code graph analysis (8)
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (2)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-129)
liveobjects/src/main/kotlin/io/ably/lib/objects/serialization/DefaultSerialization.kt (1)
  • writeMsgpackArray (23-27)
lib/src/main/java/io/ably/lib/types/PublishResult.java (2)
lib/src/main/java/io/ably/lib/util/Serialisation.java (1)
  • Serialisation (40-299)
lib/src/main/java/io/ably/lib/types/AblyException.java (1)
  • AblyException (12-67)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (9)
lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java (1)
  • AblyRealtime (31-319)
lib/src/main/java/io/ably/lib/types/ChannelOptions.java (1)
  • ChannelOptions (13-131)
lib/src/main/java/io/ably/lib/types/ClientOptions.java (1)
  • ClientOptions (19-392)
lib/src/main/java/io/ably/lib/types/ErrorInfo.java (1)
  • ErrorInfo (17-197)
lib/src/main/java/io/ably/lib/types/Message.java (1)
  • Message (25-443)
lib/src/main/java/io/ably/lib/types/MessageOperation.java (1)
  • MessageOperation (14-100)
lib/src/main/java/io/ably/lib/types/Param.java (1)
  • Param (6-88)
lib/src/main/java/io/ably/lib/util/Crypto.java (1)
  • Crypto (25-410)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)
  • Listeners (9-72)
lib/src/main/java/io/ably/lib/transport/ConnectionManager.java (2)
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (1)
  • ProtocolMessage (34-375)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-129)
lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (2)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)
  • Listeners (9-72)
lib/src/main/java/io/ably/lib/types/AblyException.java (1)
  • AblyException (12-67)
lib/src/main/java/io/ably/lib/util/Listeners.java (2)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-129)
lib/src/main/java/io/ably/lib/types/UpdateDeleteResult.java (1)
  • UpdateDeleteResult (14-81)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java (1)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-129)
lib/src/main/java/io/ably/lib/realtime/Presence.java (2)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-129)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)
  • Listeners (9-72)
⏰ 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). (5)
  • GitHub Check: check (21)
  • GitHub Check: check (29)
  • GitHub Check: check (24)
  • GitHub Check: check (19)
  • GitHub Check: check (29)
🔇 Additional comments (24)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java (1)

397-407: LGTM!

The test correctly adapts to the new Callback<PublishResult> interface for the connectionManager.send() method. The callback signature change from parameterless onSuccess() to onSuccess(PublishResult result) aligns with the broader refactoring to support per-message publish results.

lib/src/main/java/io/ably/lib/realtime/Presence.java (3)

125-130: LGTM!

The method signature change to accept Callback<PublishResult> and the use of Listeners.unwrap(listener) to extract the underlying CompletionListener for storage maintains backward compatibility with the existing QueuedPresence structure while adapting to the new callback model.


768-768: LGTM!

Correctly wraps the CompletionListener using Listeners.fromCompletionListener(listener) to match the updated connectionManager.send() signature.


897-897: LGTM!

Consistent with the pattern used in updatePresence - correctly wraps the listener for the send operation.

lib/src/main/java/io/ably/lib/util/Listeners.java (2)

15-25: LGTM!

The new adapter methods are well-designed:

  • toPublishResultListener bridges the UpdateDeleteResult callback to PublishResult callback
  • unwrap safely extracts the underlying CompletionListener with proper null handling

49-71: LGTM!

The UpdateResultToPublishAdapter correctly:

  • Handles null listener, result, result.serials, and empty arrays
  • Extracts only the first serial for UpdateDeleteResult (appropriate for single-message operations)
  • Properly delegates error handling
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (3)

140-141: LGTM!

The new res field follows the established pattern for optional array fields in ProtocolMessage and is properly annotated with @Nullable.


215-218: LGTM!

The msgpack serialization for the res field correctly follows the pattern used for other array fields (e.g., messages, annotations).


290-292: LGTM!

The deserialization case for the res field is correctly integrated into the existing switch statement.

lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (4)

51-74: LGTM!

The getMessage_retrieveBySerial test correctly validates the publish-and-retrieve-by-serial flow.


79-111: LGTM!

The updateMessage_updateData test covers the core update flow with proper assertions for data, name, and action.


154-185: LGTM!

The async update test correctly uses Listeners.fromCompletionListener and validates the callback-based flow.


386-426: LGTM!

Comprehensive workflow test covering publish → update → verify → delete → verify deletion → verify version history. Well-structured end-to-end test.

lib/src/main/java/io/ably/lib/types/PublishResult.java (3)

15-27: LGTM!

The class structure and serials field documentation clearly explain the 1:1 correspondence with published messages and the null case for conflation.


71-90: LGTM!

The array serialization/deserialization methods follow standard patterns used elsewhere in the codebase.


113-128: LGTM!

The body handler correctly handles both JSON and msgpack content types and wraps decode exceptions appropriately.

lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (3)

1128-1128: LGTM!

The publish method correctly uses Listeners.fromCompletionListener(listener) to adapt to the new callback model.


1217-1257: LGTM!

The updateMessage method overloads properly delegate to the centralized implementation, maintaining a clean API surface.


1714-1714: LGTM!

Consistent with the pattern used in publish - correctly wraps the listener for the send operation.

lib/src/main/java/io/ably/lib/transport/ConnectionManager.java (5)

31-42: LGTM!

The added imports are appropriate for the new Callback<PublishResult> type and null safety annotations.


1408-1410: LGTM!

The message.res (per-message publish results) is correctly propagated to the ack handler.


1728-1735: LGTM!

The QueuedMessage class correctly updated to use Callback<PublishResult>. Note that this is a breaking change for the public class, but the AI summary indicates that callers have been updated to use adapter wrappers.


1737-1751: LGTM!

The send method signature correctly updated to accept Callback<PublishResult>. This is a breaking public API change that enables per-message publish result handling.


1831-1885: LGTM!

The ack method implementation correctly handles per-message results:

  • Defensive bounds checking on the results array
  • Index-based iteration properly correlates results with acknowledged messages
  • Callbacks are invoked outside the synchronized block, avoiding potential deadlocks

The design gracefully handles cases where results is null or shorter than expected by passing null to the callback. The Callback<PublishResult> interface supports null values (no @nonnull annotation), and the codebase already establishes this pattern in implementations like Listeners.java and CompletionListener.java that handle null defensively.

@ttypic ttypic force-pushed the AIT-98/message-edits-deletes branch from 2846cde to 9f0407f Compare January 12, 2026 11:29
@github-actions github-actions bot temporarily deployed to staging/pull/1183/features January 12, 2026 11:30 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: 4

🤖 Fix all issues with AI agents
In @lib/src/main/java/io/ably/lib/realtime/ChannelBase.java:
- Around line 1374-1381: The updatedMessage is missing the original message
serial so server cannot identify which message is targeted; set
updatedMessage.serial = message.serial after creating updatedMessage (in the
block where Message updatedMessage = new Message(message.name, message.data,
message.extras); is constructed) so the serial is preserved for
update/delete/append operations, keeping the rest of the version/client metadata
logic unchanged.
- Around line 1374-1395: The code prepares and encodes updatedMessage but then
sends the original message and checks clientId on the wrong object; change the
ProtocolMessage creation to use updatedMessage (msg.messages = new Message[] {
updatedMessage }) and call ably.auth.checkClientId(updatedMessage, true,
connected) before encoding; also add the same channel state guard used in
publish() (reject when channel is failed or suspended) so this method behaves
consistently with publish() and doesn't send messages when the channel is in an
invalid state.

In @lib/src/main/java/io/ably/lib/types/PublishResult.java:
- Around line 52-62: The inner array-unpacking loop for SERIALS in PublishResult
uses the wrong loop variable: it checks and increments outer variable i while
indexing with j (which is never incremented), causing an infinite loop and wrong
writes to serials[0]; change the loop to use j as the loop counter (for (int j =
0; j < count; j++)) and ensure the increment and condition reference j, leaving
unpacker logic and serials[j] writes intact so each element is populated
correctly via
unpacker.getNextFormat()/unpacker.unpackNil()/unpacker.unpackString().

In
@lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java:
- Around line 318-333: The test getMessageVersions_async only publishes and
reads history but never exercises the async message-versions API; update the
test to call the channel's async versions retrieval (e.g.,
channel.getMessageVersions or getMessageVersionsAsync) using the
publishedMessage.serial, register a callback/future to wait for the async
result, and assert the returned versions array/collection is non-empty and
contains expected data (timestamps/contents) and that there are no errors;
ensure you use the existing publishedMessage variable and verify callback
invocation before finishing the test.
🧹 Nitpick comments (2)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)

71-81: Consider wrapping IOException in AblyException for consistency.

Other serialization methods in the codebase throw AblyException for IO errors. Throwing RuntimeException here may cause inconsistent error handling behavior upstream.

♻️ Proposed improvement
     public static void writeMsgpackArray(PublishResult[] results, MessagePacker packer) {
         try {
             int count = results.length;
             packer.packArrayHeader(count);
             for (PublishResult result : results) {
                 result.writeMsgpack(packer);
             }
         } catch (IOException e) {
-            throw new RuntimeException(e.getMessage(), e);
+            throw new AblyException.fromThrowable(e);
         }
     }

Note: This would require changing the method signature to declare throws AblyException, which may require updates to callers.

lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (1)

41-42: Consider reducing the test timeout.

A 5-minute (300 seconds) timeout seems excessive for these tests. The helper methods already use 5-second polling timeouts. A timeout of 60-120 seconds would likely be sufficient and would fail faster on CI if something goes wrong.

♻️ Suggested change
     @Rule
-    public Timeout testTimeout = Timeout.seconds(300);
+    public Timeout testTimeout = Timeout.seconds(120);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2846cde and 9f0407f.

📒 Files selected for processing (8)
  • lib/src/main/java/io/ably/lib/realtime/ChannelBase.java
  • lib/src/main/java/io/ably/lib/realtime/Presence.java
  • lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
  • lib/src/main/java/io/ably/lib/types/ProtocolMessage.java
  • lib/src/main/java/io/ably/lib/types/PublishResult.java
  • lib/src/main/java/io/ably/lib/util/Listeners.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/src/main/java/io/ably/lib/util/Listeners.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java
🧰 Additional context used
🧬 Code graph analysis (6)
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (1)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-129)
lib/src/main/java/io/ably/lib/types/PublishResult.java (2)
lib/src/main/java/io/ably/lib/http/HttpCore.java (1)
  • HttpCore (39-583)
lib/src/main/java/io/ably/lib/types/AblyException.java (1)
  • AblyException (12-67)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (10)
lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java (1)
  • AblyRealtime (31-319)
lib/src/main/java/io/ably/lib/types/AblyException.java (1)
  • AblyException (12-67)
lib/src/main/java/io/ably/lib/types/ChannelOptions.java (1)
  • ChannelOptions (13-131)
lib/src/main/java/io/ably/lib/types/ClientOptions.java (1)
  • ClientOptions (19-392)
lib/src/main/java/io/ably/lib/types/ErrorInfo.java (1)
  • ErrorInfo (17-197)
lib/src/main/java/io/ably/lib/types/Message.java (1)
  • Message (25-443)
lib/src/main/java/io/ably/lib/types/MessageOperation.java (1)
  • MessageOperation (14-100)
lib/src/main/java/io/ably/lib/types/Param.java (1)
  • Param (6-88)
lib/src/main/java/io/ably/lib/util/Crypto.java (1)
  • Crypto (25-410)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)
  • Listeners (9-72)
lib/src/main/java/io/ably/lib/realtime/Presence.java (2)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-129)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)
  • Listeners (9-72)
lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (3)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)
  • Listeners (9-72)
lib/src/main/java/io/ably/lib/util/Log.java (1)
  • Log (21-157)
lib/src/main/java/io/ably/lib/types/AblyException.java (1)
  • AblyException (12-67)
lib/src/main/java/io/ably/lib/transport/ConnectionManager.java (2)
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (1)
  • ProtocolMessage (34-375)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-129)
⏰ 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). (12)
  • GitHub Check: check
  • GitHub Check: check (21)
  • GitHub Check: check (19)
  • GitHub Check: check (29)
  • GitHub Check: build
  • GitHub Check: check (24)
  • GitHub Check: check-rest-httpurlconnection
  • GitHub Check: check-realtime-httpurlconnection
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-liveobjects
  • GitHub Check: check-rest-okhttp
  • GitHub Check: check (29)
🔇 Additional comments (16)
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (1)

140-141: LGTM!

The res field addition and its MessagePack serialization/deserialization follow the established patterns in this class. The null-safe handling is consistent with other array fields like messages, presence, and annotations.

Also applies to: 166-166, 215-218, 290-292

lib/src/main/java/io/ably/lib/realtime/Presence.java (2)

125-130: LGTM!

The callback type change from CompletionListener to Callback<PublishResult> integrates correctly with the new PublishResult flow. Using Listeners.unwrap() to extract the underlying CompletionListener for storage in QueuedPresence maintains backward compatibility with the internal queue processing.


768-768: LGTM!

The Listeners.fromCompletionListener(listener) wrapping is applied consistently in both updatePresence and sendQueuedMessages, ensuring that presence operations correctly propagate through the new Callback<PublishResult> pipeline.

Also applies to: 897-897

lib/src/main/java/io/ably/lib/types/PublishResult.java (1)

113-128: LGTM!

The PublishResultBodyHandler correctly handles both JSON and MessagePack content types and provides a sensible fallback (empty array) when parsing fails or returns null.

lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (2)

481-518: LGTM!

The polling helper methods are well-structured with reasonable 5-second timeouts and 200ms sleep intervals. The predicate-based approach for waitForMessageAppearInVersionHistory provides good flexibility for different test scenarios.


184-185: No action required—callback types are compatible.

Listeners.fromCompletionListener() is a generic method (public static <T> Callback<T> fromCompletionListener(...)) that infers its type parameter from context. When passed to channel.updateMessage() and channel.deleteMessage(), which both expect Callback<UpdateDeleteResult>, the compiler correctly infers T as UpdateDeleteResult. The test code is type-safe and correct.

lib/src/main/java/io/ably/lib/transport/ConnectionManager.java (4)

1728-1735: LGTM!

The QueuedMessage class update to use Callback<PublishResult> is clean and straightforward. The generic callback type enables proper per-message result propagation.


1737-1737: LGTM!

The send() and sendImpl() method signature changes to accept Callback<PublishResult> are consistent with the broader refactoring to support per-message publish results.

Also applies to: 1753-1753


1831-1831: LGTM!

The ack() method correctly propagates per-message PublishResult to listeners. The bounds check results != null && results.length > i safely handles cases where the results array might be shorter than expected or null.

Also applies to: 1873-1883


1408-1409: LGTM!

The onAck handler correctly extracts message.res from the protocol message and passes it to the pending message queue's ack() method, enabling per-message result propagation.

lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (6)

34-34: LGTM!

The new imports for MessageAction and Listeners are required for the refactored update/delete/append operations.

Also applies to: 50-50


1128-1128: LGTM!

The change to use Listeners.fromCompletionListener(listener) aligns with the broader refactor to unify callback handling through the Callback<PublishResult> pattern in ConnectionManager.


1217-1257: LGTM!

The updateMessage overloads properly delegate to the centralized updateDeleteImpl method with correct parameter forwarding.


1266-1306: LGTM!

The deleteMessage overloads follow the same consistent delegation pattern to updateDeleteImpl.


1315-1355: LGTM!

The appendMessage overloads follow the same consistent delegation pattern to updateDeleteImpl.


1712-1715: LGTM!

The change to use Listeners.fromCompletionListener(listener) is consistent with the refactoring pattern applied to the publish method.

@ttypic ttypic force-pushed the AIT-99/message-appends branch from 9c5ac4a to d4cf2d1 Compare January 12, 2026 11:36
@ttypic ttypic force-pushed the AIT-98/message-edits-deletes branch from 9f0407f to 36cc288 Compare January 12, 2026 11:59
@github-actions github-actions bot temporarily deployed to staging/pull/1183/features January 12, 2026 11:59 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: 3

🤖 Fix all issues with AI agents
In @lib/src/main/java/io/ably/lib/realtime/ChannelBase.java:
- Around line 1374-1395: The code prepares updatedMessage with action and
version but then sends the original message and omits the serial; update
ChannelBase where ProtocolMessage msg is built so msg.messages contains the
prepared updatedMessage (not message) and copy the original message.serial into
updatedMessage.serial before encoding/sending (ensure updatedMessage.serial is
set from message.serial), then send msg via connectionManager.send as before.

In @lib/src/main/java/io/ably/lib/types/PublishResult.java:
- Around line 52-63: The SERIALS deserialization loop incorrectly uses the
outer-loop variable i instead of the inner-loop variable j, causing misreads; in
the block that unpacks the serials array (where count, serials, and the for loop
are defined), change the loop condition and increment to use j (for (int j = 0;
j < count; j++)) and ensure all array writes use serials[j], leaving
unpacker.getNextFormat(), unpacker.unpackNil(), unpacker.unpackString(), and the
new PublishResult(serials) unchanged.

In
@lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java:
- Around line 321-339: The test getMessageVersions_async is incomplete: after
publishing and retrieving publishedMessage, call the async API to fetch message
versions (use Channel.getMessageVersions or the async variant provided by the
SDK) passing publishedMessage.serial and a callback/future, wait for the async
result, then assert the response is non-null, contains at least one version, and
that the earliest/latest version data matches "Original data" (or the expected
payload); add appropriate timeout handling (similar to
waitForMessageAppearInHistory) and meaningful assert messages so the test
actually verifies async version retrieval.
🧹 Nitpick comments (3)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)

71-81: Consider propagating IOException instead of wrapping in RuntimeException.

writeMsgpackArray catches IOException and wraps it in a RuntimeException. This is inconsistent with readMsgpackArray which propagates IOException. For consistency and proper error handling, consider declaring throws IOException.

♻️ Suggested improvement
-    public static void writeMsgpackArray(PublishResult[] results, MessagePacker packer) {
-        try {
-            int count = results.length;
-            packer.packArrayHeader(count);
-            for (PublishResult result : results) {
-                result.writeMsgpack(packer);
-            }
-        } catch (IOException e) {
-            throw new RuntimeException(e.getMessage(), e);
-        }
+    public static void writeMsgpackArray(PublishResult[] results, MessagePacker packer) throws IOException {
+        int count = results.length;
+        packer.packArrayHeader(count);
+        for (PublishResult result : results) {
+            result.writeMsgpack(packer);
+        }
     }
lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (2)

42-43: Consider reducing the test timeout.

A 300-second (5-minute) timeout is quite long for unit/integration tests. While message operations may need time, consider whether a shorter timeout (e.g., 60 seconds) would be sufficient while still allowing adequate time for network operations.


111-111: Use diamond operator for type inference.

♻️ Minor cleanup
-        Helpers.AsyncWaiter<UpdateDeleteResult> waiter = new Helpers.AsyncWaiter();
+        Helpers.AsyncWaiter<UpdateDeleteResult> waiter = new Helpers.AsyncWaiter<>();
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9f0407f and 36cc288.

📒 Files selected for processing (8)
  • lib/src/main/java/io/ably/lib/realtime/ChannelBase.java
  • lib/src/main/java/io/ably/lib/realtime/Presence.java
  • lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
  • lib/src/main/java/io/ably/lib/types/ProtocolMessage.java
  • lib/src/main/java/io/ably/lib/types/PublishResult.java
  • lib/src/main/java/io/ably/lib/util/Listeners.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java
  • lib/src/main/java/io/ably/lib/types/ProtocolMessage.java
🧰 Additional context used
🧬 Code graph analysis (3)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (6)
lib/src/main/java/io/ably/lib/types/ErrorInfo.java (1)
  • ErrorInfo (17-197)
lib/src/main/java/io/ably/lib/types/Message.java (1)
  • Message (25-443)
lib/src/main/java/io/ably/lib/types/MessageOperation.java (1)
  • MessageOperation (14-100)
lib/src/main/java/io/ably/lib/types/Param.java (1)
  • Param (6-88)
lib/src/main/java/io/ably/lib/types/UpdateDeleteResult.java (1)
  • UpdateDeleteResult (14-81)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)
  • Listeners (9-72)
lib/src/main/java/io/ably/lib/realtime/Presence.java (2)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-129)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)
  • Listeners (9-72)
lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (3)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)
  • Listeners (9-72)
lib/src/main/java/io/ably/lib/util/Log.java (1)
  • Log (21-157)
lib/src/main/java/io/ably/lib/types/AblyException.java (1)
  • AblyException (12-67)
⏰ 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). (12)
  • GitHub Check: check (19)
  • GitHub Check: check (21)
  • GitHub Check: check (29)
  • GitHub Check: check (24)
  • GitHub Check: check
  • GitHub Check: build
  • GitHub Check: check-rest-httpurlconnection
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-realtime-httpurlconnection
  • GitHub Check: check-liveobjects
  • GitHub Check: check (29)
  • GitHub Check: check-rest-okhttp
🔇 Additional comments (18)
lib/src/main/java/io/ably/lib/util/Listeners.java (3)

15-17: LGTM!

The adapter factory method correctly creates a wrapper to translate PublishResult callbacks to UpdateDeleteResult callbacks, enabling integration between the new callback-based flow and existing update/delete result handling.


19-25: LGTM!

The unwrap method provides a clean way to extract the underlying CompletionListener from a wrapped callback, with appropriate null handling when the callback isn't a wrapper.


49-71: LGTM!

The adapter correctly translates PublishResult to UpdateDeleteResult by extracting the first serial from the array. The null-safety checks for result, result.serials, and array length are properly implemented.

lib/src/main/java/io/ably/lib/realtime/Presence.java (3)

125-130: LGTM!

The method signature change to Callback<PublishResult> aligns with the broader API shift. Using Listeners.unwrap(listener) correctly extracts the underlying CompletionListener for storage in QueuedPresence, maintaining compatibility with the existing queued presence handling.


768-768: LGTM!

Properly wraps the CompletionListener using Listeners.fromCompletionListener before passing to ConnectionManager.send, aligning with the new Callback<PublishResult> signature.


897-897: LGTM!

Correctly wraps the listener for the new callback-based ConnectionManager API when sending queued presence messages.

lib/src/main/java/io/ably/lib/types/PublishResult.java (1)

92-128: LGTM!

The body handler correctly handles both JSON and msgpack content types, with appropriate error handling and fallback to empty array.

lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (1)

487-524: LGTM on helper methods!

The polling helpers are well-structured with appropriate timeouts and sleep intervals. The predicate-based approach in waitForMessageAppearInVersionHistory provides flexibility for different wait conditions.

lib/src/main/java/io/ably/lib/transport/ConnectionManager.java (4)

1728-1734: LGTM!

The QueuedMessage class is correctly updated to use Callback<PublishResult> instead of CompletionListener, aligning with the new callback-based architecture for publish operations.


1737-1751: LGTM!

The send method signature change to Callback<PublishResult> is consistent with the broader refactoring to support per-message publish results.


1831-1885: LGTM!

The ack method correctly handles the new PublishResult[] parameter:

  • Null-safe access to results array with bounds checking
  • Properly maps each result to its corresponding message callback
  • Maintains backward compatibility when results array is null or shorter than expected

1408-1410: LGTM!

The onAck method correctly passes message.res (the PublishResult[] from the protocol message) to the pending message acknowledgment handler.

lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (6)

34-34: LGTM!

The new imports for MessageAction and Listeners are required for the refactored update/delete/append operations and the listener wrapping changes.

Also applies to: 50-50


1128-1128: LGTM!

The wrapping of CompletionListener with Listeners.fromCompletionListener() aligns with the updated ConnectionManager.send() signature that now expects Callback<PublishResult>.


1211-1257: LGTM!

The updateMessage overloads follow a consistent delegation pattern and properly funnel to updateDeleteImpl with MessageAction.MESSAGE_UPDATE. The API is symmetric with deleteMessage and appendMessage.


1260-1306: LGTM!

The deleteMessage overloads mirror the updateMessage pattern and correctly use MessageAction.MESSAGE_DELETE.


1309-1355: LGTM!

The appendMessage overloads complete the symmetric API with MessageAction.MESSAGE_APPEND.


1712-1715: LGTM!

The sendProtocolMessage method correctly wraps the CompletionListener to match the updated ConnectionManager.send() signature.

@ttypic ttypic force-pushed the AIT-98/message-edits-deletes branch from 36cc288 to a80788d Compare January 12, 2026 12:38
@github-actions github-actions bot temporarily deployed to staging/pull/1183/features January 12, 2026 12:39 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1183/javadoc January 12, 2026 12:41 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: 5

🤖 Fix all issues with AI agents
In @lib/src/main/java/io/ably/lib/realtime/ChannelBase.java:
- Around line 1357-1396: In updateDeleteImpl you build and encode updatedMessage
but then send the original message, losing action/version and potentially
serial; copy the original serial (e.g. updatedMessage.serial = message.serial)
and any other identifying fields you need (timestamp/id if applicable), and
change the ProtocolMessage construction to send updatedMessage (msg.messages =
new Message[] { updatedMessage }) instead of the original message; also consider
running ably.auth.checkClientId against the same updatedMessage you encode/send.

In @lib/src/main/java/io/ably/lib/transport/ConnectionManager.java:
- Around line 1408-1410: PendingMessageQueue.ack currently adjusts msgSerial and
count when msgSerial < startSerial but doesn't offset the PublishResult[]
(results), causing misalignment between acked messages and their callbacks;
update PendingMessageQueue.ack (and the same logic in the other affected block)
to compute a resultsOffset = startSerial - originalMsgSerial (or equivalent)
whenever you shift msgSerial, then apply that offset when selecting/iterating
results and when passing PublishResult items to pending message callbacks so
each callback receives the matching PublishResult for its message; ensure any
array slicing, indexing, or loop start uses resultsOffset consistently and
preserve original behaviour when no offset is needed.

In @lib/src/main/java/io/ably/lib/types/PublishResult.java:
- Around line 42-69: In readMsgpack, the inner array loop uses the wrong loop
variable and mutates the outer loop index (for (int j = 0; i < count; i++)),
breaking parsing; change that inner loop to iterate j (for (int j = 0; j <
count; j++)) and increment j, assigning serials[j] inside, so the outer variable
i is not modified and the serials array is correctly populated when handling the
SERIALS field using the unpacker.

In
@lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java:
- Around line 52-55: The tearDown method in RealtimeChannelMessageEditTest
unconditionally calls ably.close(), which can throw a NullPointerException if
setUp failed and ably is null; update tearDown to guard the call (e.g., if (ably
!= null) ably.close() or use Objects.requireNonNullElse/try-finally pattern) so
close() is only invoked when ably was successfully initialized and ensure the
test class variable name ably and method tearDown are used to locate the change.
🧹 Nitpick comments (1)
liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt (1)

4-32: Guard coroutine resumption against cancellation/double-callback.

Today continuation.resume(...) can throw if the coroutine was cancelled before the callback fires (or if an error+success bug happens upstream).

Proposed hardening
 internal suspend fun ObjectsAdapter.sendAsync(message: ProtocolMessage) = suspendCancellableCoroutine { continuation ->
   try {
     connectionManager.send(message, clientOptions.queueMessages, object : Callback<PublishResult> {
       override fun onSuccess(result: PublishResult) {
-        continuation.resume(Unit)
+        if (continuation.isActive) continuation.resume(Unit)
       }

       override fun onError(reason: ErrorInfo) {
-        continuation.resumeWithException(ablyException(reason))
+        if (continuation.isActive) continuation.resumeWithException(ablyException(reason))
       }
     })
   } catch (e: Exception) {
-    continuation.resumeWithException(e)
+    if (continuation.isActive) continuation.resumeWithException(e)
   }
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 36cc288 and a80788d.

📒 Files selected for processing (9)
  • lib/src/main/java/io/ably/lib/realtime/ChannelBase.java
  • lib/src/main/java/io/ably/lib/realtime/Presence.java
  • lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
  • lib/src/main/java/io/ably/lib/types/ProtocolMessage.java
  • lib/src/main/java/io/ably/lib/types/PublishResult.java
  • lib/src/main/java/io/ably/lib/util/Listeners.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java
  • liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/src/main/java/io/ably/lib/types/ProtocolMessage.java
  • lib/src/main/java/io/ably/lib/realtime/Presence.java
🧰 Additional context used
🧬 Code graph analysis (4)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java (2)
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (1)
  • ProtocolMessage (34-375)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-129)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (5)
lib/src/main/java/io/ably/lib/types/ErrorInfo.java (1)
  • ErrorInfo (17-197)
lib/src/main/java/io/ably/lib/types/MessageOperation.java (1)
  • MessageOperation (14-100)
lib/src/main/java/io/ably/lib/types/Param.java (1)
  • Param (6-88)
lib/src/main/java/io/ably/lib/types/UpdateDeleteResult.java (1)
  • UpdateDeleteResult (14-81)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)
  • Listeners (9-72)
lib/src/main/java/io/ably/lib/transport/ConnectionManager.java (2)
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (1)
  • ProtocolMessage (34-375)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-129)
lib/src/main/java/io/ably/lib/types/PublishResult.java (3)
lib/src/main/java/io/ably/lib/http/HttpCore.java (1)
  • HttpCore (39-583)
lib/src/main/java/io/ably/lib/util/Serialisation.java (1)
  • Serialisation (40-299)
lib/src/main/java/io/ably/lib/types/AblyException.java (1)
  • AblyException (12-67)
⏰ 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). (12)
  • GitHub Check: check (19)
  • GitHub Check: check (24)
  • GitHub Check: check-rest-okhttp
  • GitHub Check: check (21)
  • GitHub Check: check (29)
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-liveobjects
  • GitHub Check: build
  • GitHub Check: check-realtime-httpurlconnection
  • GitHub Check: check-rest-httpurlconnection
  • GitHub Check: check
  • GitHub Check: check (29)
🔇 Additional comments (3)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java (1)

22-27: Callback migration in test is correct.

Using Callback<PublishResult> here preserves the intent (assert onError path) and safely ignores the success payload.

Also applies to: 397-407

lib/src/main/java/io/ably/lib/util/Listeners.java (1)

15-25: Listener adapters look sound (null-safe + correct 1st-serial mapping).

Also applies to: 49-71

lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (1)

1101-1129: publish() now correctly bridges CompletionListener -> Callback via Listeners.

@ttypic ttypic force-pushed the AIT-99/message-appends branch 3 times, most recently from b852631 to 58541a7 Compare January 12, 2026 14:25
@ttypic ttypic force-pushed the AIT-98/message-edits-deletes branch 2 times, most recently from a80788d to d4684b0 Compare January 12, 2026 14:39
@github-actions github-actions bot temporarily deployed to staging/pull/1183/features January 12, 2026 14:40 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1183/javadoc January 12, 2026 14:41 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: 2

🤖 Fix all issues with AI agents
In @lib/src/main/java/io/ably/lib/realtime/ChannelBase.java:
- Around line 1384-1392: In ChannelBase, the catch block around
ably.auth.checkClientId(...) and updatedMessage.encode(...) should notify errors
using callCompletionListenerError(listener, e.errorInfo) (not only
listener.onError) so null-safe listener handling matches publish; additionally
if listener is null (i.e., synchronous overloads like updateMessage(Message)),
rethrow the AblyException so callers receive the failure instead of it being
silently swallowed. Update the catch to call callCompletionListenerError(...)
and then if listener == null throw e; otherwise return after notification.
🧹 Nitpick comments (6)
lib/src/main/java/io/ably/lib/types/PublishResult.java (3)

29-31: Specify charset for JSON parsing.

Using new String(packed) relies on the platform's default charset, which can vary and cause encoding issues. Explicitly use UTF-8.

♻️ Suggested fix
     public static PublishResult readFromJson(byte[] packed) throws MessageDecodeException {
-        return Serialisation.gson.fromJson(new String(packed), PublishResult.class);
+        return Serialisation.gson.fromJson(new String(packed, java.nio.charset.StandardCharsets.UTF_8), PublishResult.class);
     }

71-81: Consider consistent exception handling.

writeMsgpackArray wraps IOException in RuntimeException, while readMsgpack(byte[]) wraps it in AblyException. This inconsistency could make error handling unpredictable for callers.

♻️ Suggested fix for consistency
-    public static void writeMsgpackArray(PublishResult[] results, MessagePacker packer) {
+    public static void writeMsgpackArray(PublishResult[] results, MessagePacker packer) throws AblyException {
         try {
             int count = results.length;
             packer.packArrayHeader(count);
             for (PublishResult result : results) {
                 result.writeMsgpack(packer);
             }
         } catch (IOException e) {
-            throw new RuntimeException(e.getMessage(), e);
+            throw AblyException.fromThrowable(e);
         }
     }

Note: This would require updating the call site in ProtocolMessage.writeMsgpack to handle the exception, which already throws IOException, so the change may not be necessary if the calling context handles exceptions appropriately.


113-128: Unrecognized content types silently return empty array.

When contentType is neither application/json nor application/x-msgpack, publishResult remains null and an empty array is returned. Consider logging or throwing for unexpected content types.

lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (3)

112-112: Use parameterized type for AsyncWaiter.

Raw type Helpers.AsyncWaiter should use the generic type parameter for type safety.

Suggested fix
-        Helpers.AsyncWaiter<UpdateDeleteResult> waiter = new Helpers.AsyncWaiter();
+        Helpers.AsyncWaiter<UpdateDeleteResult> waiter = new Helpers.AsyncWaiter<>();

514-536: Empty catch blocks may hide real errors.

The empty catch (Exception e) {} blocks in waitForUpdatedMessageAppear and waitForDeletedMessageAppear swallow all exceptions silently. This can mask unexpected errors and cause tests to timeout rather than fail fast with meaningful error messages.

Consider logging the exception or at least checking for expected exception types (e.g., 404 for message not found yet).

Suggested improvement for waitForUpdatedMessageAppear
     private Message waitForUpdatedMessageAppear(Channel channel, String serial) throws Exception {
         long timeout = System.currentTimeMillis() + 5_000;
         while (true) {
             try {
                 Message message = channel.getMessage(serial);
                 if ((message != null && message.action == MessageAction.MESSAGE_UPDATE) || System.currentTimeMillis() > timeout)
                     return message;
                 Thread.sleep(200);
-            } catch (Exception e) {}
+            } catch (AblyException e) {
+                // Expected when message not yet available, continue polling
+                if (System.currentTimeMillis() > timeout) throw e;
+                Thread.sleep(200);
+            }
         }
     }

499-500: Consider adding parentheses for clarity.

The boolean expression relies on operator precedence (&& before ||). While the current behavior is correct, explicit parentheses would improve readability.

Suggested fix
-            if (history.items().length > 0 && predicate.test(history.items()) || System.currentTimeMillis() > timeout)
+            if ((history.items().length > 0 && predicate.test(history.items())) || System.currentTimeMillis() > timeout)
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a80788d and d4684b0.

📒 Files selected for processing (10)
  • lib/src/main/java/io/ably/lib/realtime/ChannelBase.java
  • lib/src/main/java/io/ably/lib/realtime/Presence.java
  • lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
  • lib/src/main/java/io/ably/lib/types/ProtocolMessage.java
  • lib/src/main/java/io/ably/lib/types/PublishResult.java
  • lib/src/main/java/io/ably/lib/util/Listeners.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeSuite.java
  • liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • lib/src/main/java/io/ably/lib/util/Listeners.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java
  • lib/src/main/java/io/ably/lib/realtime/Presence.java
🧰 Additional context used
🧬 Code graph analysis (4)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (8)
lib/src/main/java/io/ably/lib/types/AblyException.java (1)
  • AblyException (12-67)
lib/src/main/java/io/ably/lib/types/ChannelOptions.java (1)
  • ChannelOptions (13-131)
lib/src/main/java/io/ably/lib/types/ErrorInfo.java (1)
  • ErrorInfo (17-197)
lib/src/main/java/io/ably/lib/types/Message.java (1)
  • Message (25-443)
lib/src/main/java/io/ably/lib/types/MessageOperation.java (1)
  • MessageOperation (14-100)
lib/src/main/java/io/ably/lib/types/Param.java (1)
  • Param (6-88)
lib/src/main/java/io/ably/lib/types/UpdateDeleteResult.java (1)
  • UpdateDeleteResult (14-81)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)
  • Listeners (9-72)
lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (3)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)
  • Listeners (9-72)
lib/src/main/java/io/ably/lib/util/Log.java (1)
  • Log (21-157)
lib/src/main/java/io/ably/lib/types/AblyException.java (1)
  • AblyException (12-67)
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (1)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-129)
lib/src/main/java/io/ably/lib/transport/ConnectionManager.java (2)
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (1)
  • ProtocolMessage (34-375)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-129)
⏰ 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). (12)
  • GitHub Check: check-liveobjects
  • GitHub Check: build
  • GitHub Check: check
  • GitHub Check: check (19)
  • GitHub Check: check-rest-httpurlconnection
  • GitHub Check: check (29)
  • GitHub Check: check (24)
  • GitHub Check: check-rest-okhttp
  • GitHub Check: check-realtime-httpurlconnection
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check (29)
  • GitHub Check: check (21)
🔇 Additional comments (15)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeSuite.java (1)

33-34: LGTM!

The new RealtimeChannelMessageEditTest.class is correctly added to the test suite, following the existing pattern.

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

18-32: LGTM!

The callback type migration from CompletionListener to Callback<PublishResult> aligns with the broader PR changes. The PublishResult parameter in onSuccess is intentionally unused here since sendAsync only needs success/failure signaling for coroutine resumption.

lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (3)

140-141: LGTM!

The new res field follows the existing pattern for nullable array fields in ProtocolMessage.


215-218: LGTM!

MsgPack serialization of res follows the established pattern used for other array fields like messages, presence, and annotations.


290-292: LGTM!

MsgPack deserialization of res correctly uses PublishResult.readMsgpackArray and follows the existing switch-case pattern.

lib/src/main/java/io/ably/lib/transport/ConnectionManager.java (3)

1728-1734: LGTM!

The QueuedMessage class correctly updated to use Callback<PublishResult> instead of CompletionListener, aligning with the new callback model.


1408-1409: LGTM!

The onAck method correctly passes message.res to the ack method, enabling per-message publish results to flow through to listeners.


1831-1885: LGTM with a note on null handling.

The ack handling correctly maps PublishResult[] entries to corresponding acknowledged messages using index-based iteration. The bounds check results != null && results.length > i properly handles cases where the results array might be shorter than expected or null.

The known Callback<PublishResult> implementations in the codebase properly handle null results: UpdateResultToPublishAdapter explicitly checks result != null before accessing its properties, and CompletionListenerWrapper ignores the result parameter entirely.

lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (3)

38-56: LGTM!

Test class setup and teardown are well-structured. The 5-minute timeout is appropriate for integration tests that involve network operations and polling.


316-319: Verify version history ordering assumption.

The test assumes versions.items()[versions.items().length - 1] contains the latest version. This depends on the API's default ordering (likely forwards). Consider explicitly setting the direction parameter to ensure consistent behavior, or add a comment clarifying the expected ordering.


454-493: LGTM!

Good coverage for testing the append message functionality, verifying both the history state and realtime subscription behavior including the MESSAGE_APPEND action and delta data.

lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (4)

1121-1130: LGTM!

The change to use Listeners.fromCompletionListener(listener) properly wraps the CompletionListener for use with the updated ConnectionManager.send API that now expects a Callback<PublishResult>.


1217-1257: LGTM!

The updateMessage overloads follow a clean delegation pattern, routing all variants through the central updateDeleteImpl method. The API surface provides flexibility for callers with or without MessageOperation metadata and callbacks.


1308-1355: LGTM!

The appendMessage overloads follow the same consistent delegation pattern as updateMessage and deleteMessage, routing all variants through updateDeleteImpl with MessageAction.MESSAGE_APPEND.


1713-1716: LGTM!

The sendProtocolMessage method is correctly updated to use Listeners.fromCompletionListener(listener), maintaining consistency with the publish method and the updated ConnectionManager.send API.

@ttypic ttypic force-pushed the AIT-98/message-edits-deletes branch from d4684b0 to 8924312 Compare January 12, 2026 14:50
@github-actions github-actions bot temporarily deployed to staging/pull/1183/features January 12, 2026 14:51 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1183/javadoc January 12, 2026 14:53 Inactive
@ttypic ttypic force-pushed the AIT-99/message-appends branch from 58541a7 to 31969a8 Compare January 12, 2026 16:28
@ttypic ttypic force-pushed the AIT-98/message-edits-deletes branch from 8924312 to 6dac2cf Compare January 12, 2026 16:29
@github-actions github-actions bot temporarily deployed to staging/pull/1183/features January 12, 2026 16:30 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1183/javadoc January 12, 2026 16:32 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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/src/main/java/io/ably/lib/transport/ConnectionManager.java (1)

1831-1885: Critical: PendingMessageQueue.ack() misaligns results[] when adjusting msgSerial < startSerial.

When you reduce count and advance msgSerial to startSerial, you also need to skip the corresponding leading entries in results, otherwise callbacks receive the wrong PublishResult.

Proposed fix
-        public void ack(long msgSerial, int count, @Nullable PublishResult[] results, ErrorInfo reason) {
+        public void ack(long msgSerial, int count, @Nullable PublishResult[] results, ErrorInfo reason) {
             QueuedMessage[] ackMessages = null, nackMessages = null;
+            int resultsOffset = 0;
             synchronized(this) {
                 if (queue.isEmpty()) return;
                 long startSerial = queue.get(0).msg.msgSerial;
                 if(msgSerial < startSerial) {
                     /* this is an error condition and shouldn't happen but
                      * we can handle it gracefully by only processing the
                      * relevant portion of the response */
-                    count -= (int)(startSerial - msgSerial);
+                    resultsOffset = (int)(startSerial - msgSerial);
+                    count -= resultsOffset;
                     if(count < 0)
                         count = 0;
                     msgSerial = startSerial;
                 }
                 if(msgSerial > startSerial) {
                     /* this counts as a nack of the messages earlier than serial,
                      * as well as an ack */
                     int nCount = (int)(msgSerial - startSerial);
+                    if (nCount > queue.size()) nCount = queue.size();
                     List<QueuedMessage> nackList = queue.subList(0, nCount);
                     nackMessages = nackList.toArray(new QueuedMessage[nCount]);
                     nackList.clear();
                     startSerial = msgSerial;
                 }
                 if(msgSerial == startSerial) {
+                    if (count > queue.size()) count = queue.size();
                     List<QueuedMessage> ackList = queue.subList(0, count);
                     ackMessages = ackList.toArray(new QueuedMessage[count]);
                     ackList.clear();
                 }
             }
@@
             if(ackMessages != null) {
                 for (int i = 0; i < ackMessages.length; i++) {
                     QueuedMessage msg = ackMessages[i];
                     try {
                         if (msg.listener != null) {
-                            PublishResult messageResult = results != null && results.length > i ? results[i] : null;
+                            int idx = i + resultsOffset;
+                            PublishResult messageResult = results != null && results.length > idx ? results[idx] : null;
                             msg.listener.onSuccess(messageResult);
                         }
                     } catch (Throwable t) {
                         Log.e(TAG, "ack(): listener exception", t);
                     }
                 }
             }
         }
🤖 Fix all issues with AI agents
In
@lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java:
- Around line 357-393: The subscription's mismatch branch currently calls
appendWaiter.onError(null) which wrongly fails the test when unrelated messages
arrive; update the channel.subscribe callback (the lambda that checks
msg.serial) to only call appendWaiter.onSuccess(msg) when msg.serial equals
serial (and optionally check msg.action == MessageAction.MESSAGE_APPEND), and
otherwise do nothing (ignore unrelated messages); also ensure you unsubscribe or
remove the listener after success to avoid spurious future triggers.
- Around line 48-51: The tearDown method currently calls ably.close() unguarded
which can throw an NPE if ably is null; update the @After method tearDown() to
check that the ably field is non-null before calling close (e.g., if (ably !=
null) ably.close()) so the cleanup is safe when setup failed or tests didn't
initialize ably.
🧹 Nitpick comments (2)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (1)

395-436: Avoid swallowing exceptions + make timeouts fail loudly in polling helpers.

Catching Exception and ignoring it can mask regressions; also returning “whatever we last got” on timeout makes failures harder to diagnose than an explicit assert/fail (e.g., “timed out waiting for MESSAGE_UPDATE for serial …”).

lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (1)

1210-1397: Verify message-edit wire semantics: avoid sending an “empty” version, and align auth-check with the sent message.

Today updatedMessage.version is always non-null (Line 1377), which may change the payload compared to omitting it when there’s no operation metadata. Also checkClientId(...) runs on message but you encode/send updatedMessage (Line 1385-1396).

Proposed tightening
-        Message updatedMessage = new Message(message.name, message.data, message.extras);
+        Message updatedMessage = new Message(message.name, message.data, message.extras);
         updatedMessage.serial = message.serial;
         updatedMessage.action = action;
-        updatedMessage.version = new MessageVersion();
         if (operation != null) {
+            updatedMessage.version = new MessageVersion();
             updatedMessage.version.clientId = operation.clientId;
             updatedMessage.version.description = operation.description;
             updatedMessage.version.metadata = operation.metadata;
         }
@@
-            ably.auth.checkClientId(message, true, connected);
+            ably.auth.checkClientId(updatedMessage, true, connected);
             updatedMessage.encode(options);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d4684b0 and 6dac2cf.

📒 Files selected for processing (10)
  • lib/src/main/java/io/ably/lib/realtime/ChannelBase.java
  • lib/src/main/java/io/ably/lib/realtime/Presence.java
  • lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
  • lib/src/main/java/io/ably/lib/types/ProtocolMessage.java
  • lib/src/main/java/io/ably/lib/types/PublishResult.java
  • lib/src/main/java/io/ably/lib/util/Listeners.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeSuite.java
  • liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • lib/src/main/java/io/ably/lib/util/Listeners.java
  • lib/src/main/java/io/ably/lib/types/PublishResult.java
  • liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt
🧰 Additional context used
🧬 Code graph analysis (5)
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (1)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-129)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (6)
lib/src/main/java/io/ably/lib/types/AblyException.java (1)
  • AblyException (12-67)
lib/src/main/java/io/ably/lib/types/ChannelOptions.java (1)
  • ChannelOptions (13-131)
lib/src/main/java/io/ably/lib/types/Message.java (1)
  • Message (25-443)
lib/src/main/java/io/ably/lib/types/Param.java (1)
  • Param (6-88)
lib/src/main/java/io/ably/lib/types/UpdateDeleteResult.java (1)
  • UpdateDeleteResult (14-81)
lib/src/main/java/io/ably/lib/util/Crypto.java (1)
  • Crypto (25-410)
lib/src/main/java/io/ably/lib/realtime/Presence.java (2)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-129)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)
  • Listeners (9-72)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java (2)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-129)
liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt (2)
  • onSuccess (21-23)
  • onSuccess (37-39)
lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (2)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)
  • Listeners (9-72)
lib/src/main/java/io/ably/lib/types/AblyException.java (1)
  • AblyException (12-67)
⏰ 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). (12)
  • GitHub Check: check
  • GitHub Check: check (29)
  • GitHub Check: check-rest-httpurlconnection
  • GitHub Check: check (29)
  • GitHub Check: check (24)
  • GitHub Check: build
  • GitHub Check: check (19)
  • GitHub Check: check (21)
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-realtime-httpurlconnection
  • GitHub Check: check-liveobjects
  • GitHub Check: check-rest-okhttp
🔇 Additional comments (8)
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (1)

154-219: MsgPack encode/decode for res looks consistent; consider defensive handling for malformed/partial arrays.

Encode/decode symmetry is good, but if you ever receive an ack with msgSerial < startSerial (or other partial processing scenarios), downstream ack handling may need to tolerate res arrays that don’t align perfectly with count. (See ConnectionManager review comment on results alignment.)

Also applies to: 221-293

lib/src/test/java/io/ably/lib/test/realtime/RealtimeSuite.java (1)

33-35: Good: new test class is included in the realtime suite.

lib/src/main/java/io/ably/lib/realtime/Presence.java (1)

754-773: Good: presence sends now consistently adapt CompletionListener -> Callback.

Also applies to: 864-902

lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java (1)

21-26: Test update matches the new Callback<PublishResult> send API.

Also applies to: 396-406

lib/src/main/java/io/ably/lib/transport/ConnectionManager.java (2)

1408-1410: Good: ack now plumbs per-message results (ProtocolMessage.res) into pending queue processing.


1728-1766: Good: send API migration to Callback<PublishResult> is consistent with queuing + pending tracking.

lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (2)

1101-1129: Good: publish path is updated cleanly via Listeners.fromCompletionListener(...).


1713-1716: Good: sendProtocolMessage now uses the new internal callback type via Listeners.

Comment on lines +125 to 130
void addPendingPresence(PresenceMessage presenceMessage, Callback<PublishResult> listener) {
synchronized(channel) {
final QueuedPresence queuedPresence = new QueuedPresence(presenceMessage,listener);
final QueuedPresence queuedPresence = new QueuedPresence(presenceMessage, Listeners.unwrap(listener));
pendingPresence.add(queuedPresence);
}
}
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

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the Presence.java file
find . -type f -name "Presence.java" | grep -E "(lib|src)"

Repository: ably/ably-java

Length of output: 112


🏁 Script executed:

# Let's look at the addPendingPresence method around lines 125-130
head -150 lib/src/main/java/io/ably/lib/realtime/Presence.java | tail -50

Repository: ably/ably-java

Length of output: 2578


🏁 Script executed:

# Find the Listeners class to understand unwrap() behavior
find . -type f -name "Listeners.java" | head -5

Repository: ably/ably-java

Length of output: 109


🏁 Script executed:

# Examine the Listeners.java file to understand unwrap() and fromCompletionListener()
cat -n ./lib/src/main/java/io/ably/lib/util/Listeners.java

Repository: ably/ably-java

Length of output: 2810


🏁 Script executed:

# Find all callers of addPendingPresence to see how it's being called
rg -B 3 -A 3 "addPendingPresence" lib/src/main/java/io/ably/lib/realtime/Presence.java

Repository: ably/ably-java

Length of output: 462


🏁 Script executed:

# Search for other callers of addPendingPresence in the entire codebase
rg "addPendingPresence" --type java

Repository: ably/ably-java

Length of output: 362


🏁 Script executed:

# Find the call to addPendingPresence in ChannelBase.java
rg -B 10 -A 5 "this.presence.addPendingPresence" lib/src/main/java/io/ably/lib/realtime/ChannelBase.java

Repository: ably/ably-java

Length of output: 872


🏁 Script executed:

# Find where queuedMessage.listener is created/set
rg -B 5 "queuedMessage.listener" lib/src/main/java/io/ably/lib/realtime/ChannelBase.java | head -100

Repository: ably/ably-java

Length of output: 506


🏁 Script executed:

# Look at the QueuedMessage class to understand its listener field
rg -B 5 -A 15 "class QueuedMessage" lib/src/main/java/io/ably/lib/realtime/ChannelBase.java

Repository: ably/ably-java

Length of output: 40


🏁 Script executed:

# Search for QueuedMessage class definition in the codebase
rg -B 5 -A 20 "class QueuedMessage" --type java

Repository: ably/ably-java

Length of output: 2556


🏁 Script executed:

# Look at the ConnectionManager to see where QueuedMessage is created
find . -type f -name "ConnectionManager.java" | head -1 | xargs cat -n | head -200

Repository: ably/ably-java

Length of output: 9747


🏁 Script executed:

# Search for where QueuedMessage listener is set in ConnectionManager
rg "new QueuedMessage" --type java -B 2 -A 5

Repository: ably/ably-java

Length of output: 4022


🏁 Script executed:

# Find where presence messages are sent to ConnectionManager.send
rg "presence" lib/src/main/java/io/ably/lib/realtime/Presence.java -i -A 3 | grep -E "(send|Listeners\.from)" | head -20

Repository: ably/ably-java

Length of output: 489


🏁 Script executed:

# Look for public methods in Presence that accept listeners and handle presence operations
rg -B 5 -A 10 "public.*Callback<PublishResult>" lib/src/main/java/io/ably/lib/realtime/Presence.java

Repository: ably/ably-java

Length of output: 40


🏁 Script executed:

# Search for calls to send() in Presence with presence messages
rg "connectionManager\.send|\.send\(" lib/src/main/java/io/ably/lib/realtime/Presence.java -B 3 -A 3

Repository: ably/ably-java

Length of output: 941


🏁 Script executed:

# Let's check the full sendQueuedMessages method to understand the complete flow
rg -B 2 -A 30 "void sendQueuedMessages" lib/src/main/java/io/ably/lib/realtime/Presence.java

Repository: ably/ably-java

Length of output: 1478


🏁 Script executed:

# Check QueuedPresence class to see what it does with the listener
rg -B 5 -A 20 "class QueuedPresence" lib/src/main/java/io/ably/lib/realtime/Presence.java

Repository: ably/ably-java

Length of output: 1095


🏁 Script executed:

# Let's verify if there are any other code paths where addPendingPresence is called
rg "addPendingPresence\|QueuedPresence" --type java

Repository: ably/ably-java

Length of output: 40


🏁 Script executed:

# Double-check if addPendingPresence is called from anywhere else besides transferQueuedPresenceMessages
rg -r "addPendingPresence" --type java --entire-file lib/src/main/java/

Repository: ably/ably-java

Length of output: 502


🏁 Script executed:

# Verify all the public presence methods to see the full public API
rg -B 3 "public.*CompletionListener\|public.*Callback<PublishResult>" lib/src/main/java/io/ably/lib/realtime/Presence.java | head -60

Repository: ably/ably-java

Length of output: 40


🏁 Script executed:

# Check if there are any tests that demonstrate how addPendingPresence should be used
find . -type f -name "*PresenceTest*.java" -o -name "*Presence*Test.java" | xargs rg "addPendingPresence" 2>/dev/null | head -20

Repository: ably/ably-java

Length of output: 40


🏁 Script executed:

# Check the visibility of addPendingPresence method
rg "addPendingPresence" lib/src/main/java/io/ably/lib/realtime/Presence.java -B 1 -A 1

Repository: ably/ably-java

Length of output: 187


🏁 Script executed:

# Examine the complete flow - look at what listeners are stored in ConnectionManager.queuedMessages
rg -B 5 -A 10 "queuedMessages.add" lib/src/main/java/io/ably/lib/transport/ConnectionManager.java

Repository: ably/ably-java

Length of output: 1342


🏁 Script executed:

# Check if CompletionListenerWrapper is the only type of Callback that gets stored in QueuedMessage
rg "ConnectionManager.send\|new QueuedMessage" lib/src/main/java/io/ably/lib/realtime/Presence.java -B 3 -A 3

Repository: ably/ably-java

Length of output: 40


🏁 Script executed:

# Check the exact line where addPendingPresence is defined to confirm its visibility
sed -n '125,130p' lib/src/main/java/io/ably/lib/realtime/Presence.java

Repository: ably/ably-java

Length of output: 366


🏁 Script executed:

# Look at the complete Presence class to see all methods that take CompletionListener or callbacks
rg "public|protected|private|void " lib/src/main/java/io/ably/lib/realtime/Presence.java | grep -E "(enter|update|leave)" | head -20

Repository: ably/ably-java

Length of output: 1516


🏁 Script executed:

# Check if there's any way a non-wrapped callback could be passed through the public API
rg -B 10 "enter\(|update\(|leave\(" lib/src/main/java/io/ably/lib/realtime/Presence.java | grep -E "(public|Callback|CompletionListener)" | head -30

Repository: ably/ably-java

Length of output: 393


🏁 Script executed:

# Let's trace the complete flow from public methods to see if there's ANY way a non-wrapped callback could reach addPendingPresence
rg -B 15 "void updatePresence" lib/src/main/java/io/ably/lib/realtime/Presence.java | head -40

Repository: ably/ably-java

Length of output: 800


🏁 Script executed:

# Check if ConnectionManager.send is ever called from outside Presence with presence messages
rg "connectionManager.send" lib/src/main/java/io/ably/lib/realtime/ --type java -B 2 | grep -v "Listeners.from"

Repository: ably/ably-java

Length of output: 2039


🏁 Script executed:

# Verify all the paths that lead to transferQueuedPresenceMessages
rg "transferQueuedPresenceMessages\|transferToChannelQueue" lib/src/main/java/io/ably/lib/transport/ConnectionManager.java -B 5 -A 5

Repository: ably/ably-java

Length of output: 40


🏁 Script executed:

# Let's verify the specific lines 768 and 897 mentioned in the review
sed -n '760,780p' lib/src/main/java/io/ably/lib/realtime/Presence.java

Repository: ably/ably-java

Length of output: 1062


🏁 Script executed:

# Check line 897
sed -n '890,910p' lib/src/main/java/io/ably/lib/realtime/Presence.java

Repository: ably/ably-java

Length of output: 777


🏁 Script executed:

# Verify the complete data flow: public methods -> updatePresence -> send with Listeners.from
rg -A 20 "public void updatePresence" lib/src/main/java/io/ably/lib/realtime/Presence.java | head -30

Repository: ably/ably-java

Length of output: 862


🏁 Script executed:

# Let's check if there are any other code paths that might call addPendingPresence
rg "pendingPresence" lib/src/main/java/io/ably/lib/realtime/Presence.java -B 2 -A 2

Repository: ably/ably-java

Length of output: 1619


🏁 Script executed:

# Verify the complete updatePresence method to see all branches
sed -n '740,780p' lib/src/main/java/io/ably/lib/realtime/Presence.java

Repository: ably/ably-java

Length of output: 1780


🏁 Script executed:

# Confirm the visibility and full signature of addPendingPresence
sed -n '124,131p' lib/src/main/java/io/ably/lib/realtime/Presence.java

Repository: ably/ably-java

Length of output: 368


🏁 Script executed:

# Check if there are integration tests that might show the expected behavior
find . -type f -name "*.java" -path "*/test/*" | xargs rg "addPendingPresence\|QueuedMessage" 2>/dev/null | head -10

Repository: ably/ably-java

Length of output: 40


🏁 Script executed:

# Verify that ConnectionManager only ever creates QueuedMessage with wrapped callbacks
rg "new QueuedMessage" lib/src/main/java/io/ably/lib/transport/ConnectionManager.java -B 8 -A 2

Repository: ably/ably-java

Length of output: 2333


🏁 Script executed:

# Final verification: check if transferQueuedPresenceMessages is only called from one place
rg "transferQueuedPresenceMessages" lib/src/main/java/io/ably/lib/ --type java

Repository: ably/ably-java

Length of output: 360


Design fragility: addPendingPresence relies on unwrapping without defensive checks.

The method is package-private and currently safe—all call paths from transferQueuedPresenceMessages receive listeners wrapped via Listeners.fromCompletionListener(). However, the design is brittle: Listeners.unwrap() silently returns null for any non-wrapper Callback<PublishResult>, making it easy for future code paths to lose callbacks without notice.

Consider adding an assertion or log when unwrapping fails to catch accidental misuse:

final CompletionListener completionListener = Listeners.unwrap(listener);
if (listener != null && completionListener == null) {
    Log.w(TAG, "Warning: addPendingPresence received unwrapped callback");
    // or assert: assert completionListener != null;
}

@ttypic ttypic force-pushed the AIT-98/message-edits-deletes branch from 6dac2cf to c4ffcd4 Compare January 13, 2026 09:55
@github-actions github-actions bot temporarily deployed to staging/pull/1183/features January 13, 2026 09:55 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1183/javadoc January 13, 2026 09:58 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

🤖 Fix all issues with AI agents
In @lib/src/main/java/io/ably/lib/types/PublishResult.java:
- Around line 71-83: writeMsgpackArray currently packs an array header with
results.length but skips null entries, corrupting MsgPack structure; change
PublishResult.writeMsgpackArray to iterate all indices and call packer.packNil()
for any null element (instead of skipping), and update readMsgpackArray to
accept and preserve nils by mapping a packed nil to a null PublishResult in the
returned PublishResult[] so the array length and element positions remain
consistent.
🧹 Nitpick comments (3)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (2)

370-378: Passing null to onError may cause issues.

When the message serial doesn't match, appendWaiter.onError(null) is called. If AsyncWaiter.onError doesn't handle null ErrorInfo, this could cause NPE or unexpected behavior.

Consider creating a meaningful error:
        channel.subscribe(msg -> {
            if (msg.serial.equals(serial)) {
                appendWaiter.onSuccess(msg);
            } else {
-               appendWaiter.onError(null);
+               appendWaiter.onError(new ErrorInfo("Unexpected message serial: " + msg.serial, 400, 40000));
            }
        });

414-436: Silent exception swallowing in helper methods may hide test failures.

The catch (Exception e) {} blocks silently discard exceptions, which could mask real issues and lead to false positives if the loop times out.

Consider logging or tracking exceptions:
    private Message waitForUpdatedMessageAppear(Channel channel, String serial) throws Exception {
        long timeout = System.currentTimeMillis() + 5_000;
+       Exception lastException = null;
        while (true) {
            try {
                Message message = channel.getMessage(serial);
                if ((message != null && message.action == MessageAction.MESSAGE_UPDATE) || System.currentTimeMillis() > timeout)
                    return message;
                Thread.sleep(200);
-           } catch (Exception e) {}
+           } catch (Exception e) {
+               lastException = e;
+               if (System.currentTimeMillis() > timeout) {
+                   throw new AssertionError("Timeout waiting for updated message", lastException);
+               }
+           }
        }
    }
lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (1)

1357-1397: Consider null-checking listener before calling toPublishResultListener.

The Listeners.toPublishResultListener(listener) is called even when listener is null. While looking at the UpdateResultToPublishAdapter in Listeners.java, it does null-check the listener before invoking callbacks. However, passing null through creates an unnecessary adapter wrapper.

Also, the error handling is inconsistent: validation errors (lines 1363-1371) throw AblyException, while encoding errors (line 1389) call listener.onError(). This matches the pattern in publish(), but callers need to handle both paths.

♻️ Optional: Short-circuit when listener is null
-    connectionManager.send(msg, queueMessages, Listeners.toPublishResultListener(listener));
+    connectionManager.send(msg, queueMessages, listener != null ? Listeners.toPublishResultListener(listener) : null);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6dac2cf and c4ffcd4.

📒 Files selected for processing (11)
  • lib/src/main/java/io/ably/lib/realtime/ChannelBase.java
  • lib/src/main/java/io/ably/lib/realtime/Presence.java
  • lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
  • lib/src/main/java/io/ably/lib/types/ProtocolMessage.java
  • lib/src/main/java/io/ably/lib/types/PublishResult.java
  • lib/src/main/java/io/ably/lib/util/Listeners.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeSuite.java
  • liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt
  • liveobjects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeSuite.java
  • liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt
  • lib/src/main/java/io/ably/lib/util/Listeners.java
🧰 Additional context used
🧬 Code graph analysis (6)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (7)
lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java (1)
  • AblyRealtime (31-319)
lib/src/main/java/io/ably/lib/types/ChannelOptions.java (1)
  • ChannelOptions (13-131)
lib/src/main/java/io/ably/lib/types/Message.java (1)
  • Message (25-443)
lib/src/main/java/io/ably/lib/types/MessageOperation.java (1)
  • MessageOperation (14-100)
lib/src/main/java/io/ably/lib/types/Param.java (1)
  • Param (6-88)
lib/src/main/java/io/ably/lib/types/UpdateDeleteResult.java (1)
  • UpdateDeleteResult (14-81)
lib/src/main/java/io/ably/lib/util/Crypto.java (1)
  • Crypto (25-410)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java (2)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-131)
liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt (2)
  • onSuccess (21-23)
  • onSuccess (37-39)
liveobjects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt (1)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-131)
lib/src/main/java/io/ably/lib/realtime/Presence.java (2)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-131)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)
  • Listeners (9-72)
lib/src/main/java/io/ably/lib/transport/ConnectionManager.java (3)
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (1)
  • ProtocolMessage (34-375)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-131)
liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt (2)
  • onSuccess (21-23)
  • onSuccess (37-39)
lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (3)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)
  • Listeners (9-72)
lib/src/main/java/io/ably/lib/util/Log.java (1)
  • Log (21-157)
lib/src/main/java/io/ably/lib/types/AblyException.java (1)
  • AblyException (12-67)
⏰ 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). (12)
  • GitHub Check: build
  • GitHub Check: check
  • GitHub Check: check (19)
  • GitHub Check: check (24)
  • GitHub Check: check (29)
  • GitHub Check: check-rest-okhttp
  • GitHub Check: check-realtime-httpurlconnection
  • GitHub Check: check-liveobjects
  • GitHub Check: check (21)
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-rest-httpurlconnection
  • GitHub Check: check (29)
🔇 Additional comments (35)
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (3)

140-141: LGTM - New res field for publish results.

The field follows the existing nullable pattern and integrates cleanly with other ProtocolMessage fields.


166-166: LGTM - MsgPack serialization for res field.

Serialization correctly guards with null check and uses PublishResult.writeMsgpackArray consistent with other array fields.

Also applies to: 215-218


290-292: LGTM - MsgPack deserialization for res field.

Deserialization follows the established pattern and correctly uses PublishResult.readMsgpackArray. JSON deserialization for the res field is handled automatically by Gson via reflection—neither PublishResult nor ProtocolMessage register custom JSON deserializers in Serialisation.java, allowing Gson to deserialize the res array field using standard reflection-based mechanisms.

liveobjects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt (2)

31-34: LGTM - Test correctly updated for Callback<PublishResult> API.

The mock now properly reflects the updated ConnectionManager.send() signature that accepts Callback<PublishResult> instead of CompletionListener.


54-57: LGTM - Error propagation test updated consistently.

The error path test correctly uses the new callback type while maintaining the same error propagation behavior.

lib/src/main/java/io/ably/lib/realtime/Presence.java (4)

18-19: LGTM - Required imports for the new callback pattern.


768-768: LGTM - Adapter wrapping for ConnectionManager.send().

Correctly wraps CompletionListener to Callback<PublishResult> for the updated ConnectionManager.send() signature.


897-897: LGTM - Consistent adapter usage in sendQueuedMessages.

The queued message sending correctly uses the same adapter pattern for consistency with the direct send path.


125-130: The null handling in Listeners.unwrap is not a concern for this code path.

addPendingPresence is a private method with only one caller: ChannelBase.java:249, which passes a listener from queuedMessage.listener. All listeners stored in QueuedMessage originate from ConnectionManager.send() (line 1746), which receives pre-wrapped callbacks from public entry points via Listeners.fromCompletionListener(). Therefore, queuedMessage.listener is always a CompletionListenerWrapper, and Listeners.unwrap() will successfully extract the underlying listener rather than returning null.

lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java (2)

21-21: LGTM - Import updates for new callback types.

Also applies to: 25-25


396-406: LGTM - Test correctly updated for Callback<PublishResult> API.

The test properly reflects the updated ConnectionManager.send() signature. The unused result parameter in onSuccess is appropriate since this test expects failure, not success.

lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (9)

36-51: LGTM - Test class structure and lifecycle management.

The test properly extends ParameterizedTest, sets up AblyRealtime in @Before, and cleans up in @After.


53-80: LGTM - Basic message retrieval test.

Good coverage for the publish-then-retrieve-by-serial flow.


82-122: LGTM - Update message test with result verification.

Good test that verifies both the updated data and the version serial from the callback result.


124-160: LGTM - Encrypted message update test.

Good coverage for the cipher key path using ChannelOptions.withCipherKey.


162-194: LGTM - Soft delete test.

Correctly verifies the delete operation results in MESSAGE_DELETE action.


196-243: LGTM - Version history test with metadata.

Good coverage for version history including MessageOperation metadata verification.


245-300: LGTM - Error handling tests.

Good coverage for invalid serial, null serial, and empty serial error cases with appropriate assertions on error messages.


302-355: LGTM - Complete workflow test.

Comprehensive end-to-end test covering publish → update → verify → delete → verify → version history. Good assertions on version serials matching callback results.


395-412: LGTM - Helper polling methods.

The polling approach with 200ms sleep and 5s timeout is reasonable for integration tests.

lib/src/main/java/io/ably/lib/types/PublishResult.java (4)

1-27: LGTM!

The class structure, imports, and field declaration are well-organized. The nullable serials array with appropriate Javadoc is clear.


29-69: LGTM!

The JSON and MsgPack deserialization methods handle null fields correctly and return an empty array as a safe default when the serials field is absent.


85-96: LGTM!

The readMsgpackArray and getBodyHandler methods are implemented correctly, assuming the null handling fix is applied to align with writeMsgpackArray.


115-130: There is no type parameter mismatch. The HttpCore.BodyHandler<T> interface declares T[] handleResponseBody(...), so implementing HttpCore.BodyHandler<String> correctly requires returning String[]. The implementation is correct.

lib/src/main/java/io/ably/lib/transport/ConnectionManager.java (5)

31-42: LGTM!

The new imports for Callback, PublishResult, and @Nullable are correctly added to support the typed callback pattern.


1408-1410: LGTM!

Correctly passes the res field from the ProtocolMessage to propagate per-message publish results.


1728-1735: LGTM!

The QueuedMessage class correctly updated to use Callback<PublishResult> instead of CompletionListener, aligning with the new typed callback pattern.


1737-1751: LGTM!

The send method signature correctly updated to accept Callback<PublishResult>, maintaining consistent behavior for queueing and immediate send paths.


1831-1885: LGTM!

The ack method correctly handles per-message PublishResult propagation. The bounds check at line 1877 (results.length > i) properly guards against array index out of bounds when the results array is shorter than the acknowledged messages.

lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (6)

34-50: LGTM!

The new imports for MessageAction and Listeners are correctly added to support the message edit/delete functionality and listener adaptation.


1128-1128: LGTM!

Using Listeners.fromCompletionListener(listener) correctly adapts the CompletionListener to the new Callback<PublishResult> expected by ConnectionManager.send().


1217-1257: LGTM!

The updateMessage overloads properly delegate to the centralized updateDeleteImpl method with appropriate defaults for optional parameters.


1266-1306: LGTM!

The deleteMessage overloads follow the same consistent delegation pattern as updateMessage.


1315-1355: LGTM!

The appendMessage overloads complete the trio of message mutation operations with consistent API design.


1713-1716: LGTM!

The sendProtocolMessage method correctly uses the listener adapter for consistency with the new callback pattern.

Comment on lines 71 to 85
public static void writeMsgpackArray(PublishResult[] results, MessagePacker packer) {
try {
int count = results.length;
packer.packArrayHeader(count);
for (PublishResult result : results) {
if (result != null) {
result.writeMsgpack(packer);
}
}
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Null elements in array will corrupt MsgPack serialization.

When results contains null elements, the array header declares results.length elements, but the loop only writes non-null elements. This causes a mismatch during deserialization since readMsgpackArray expects exactly count elements.

🐛 Proposed fix
 public static void writeMsgpackArray(PublishResult[] results, MessagePacker packer) {
     try {
         int count = results.length;
         packer.packArrayHeader(count);
         for (PublishResult result : results) {
             if (result != null) {
                 result.writeMsgpack(packer);
+            } else {
+                packer.packNil();
             }
         }
     } catch (IOException e) {
         throw new RuntimeException(e.getMessage(), e);
     }
 }

Also update readMsgpackArray to handle null values:

 public static PublishResult[] readMsgpackArray(MessageUnpacker unpacker) throws IOException {
     int count = unpacker.unpackArrayHeader();
     PublishResult[] results = new PublishResult[count];
     for (int i = 0; i < count; i++) {
+        if (unpacker.getNextFormat().equals(MessageFormat.NIL)) {
+            unpacker.unpackNil();
+            results[i] = null;
+        } else {
             results[i] = readMsgpack(unpacker);
+        }
     }
     return results;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static void writeMsgpackArray(PublishResult[] results, MessagePacker packer) {
try {
int count = results.length;
packer.packArrayHeader(count);
for (PublishResult result : results) {
if (result != null) {
result.writeMsgpack(packer);
}
}
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
public static void writeMsgpackArray(PublishResult[] results, MessagePacker packer) {
try {
int count = results.length;
packer.packArrayHeader(count);
for (PublishResult result : results) {
if (result != null) {
result.writeMsgpack(packer);
} else {
packer.packNil();
}
}
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
🤖 Prompt for AI Agents
In @lib/src/main/java/io/ably/lib/types/PublishResult.java around lines 71 - 83,
writeMsgpackArray currently packs an array header with results.length but skips
null entries, corrupting MsgPack structure; change
PublishResult.writeMsgpackArray to iterate all indices and call packer.packNil()
for any null element (instead of skipping), and update readMsgpackArray to
accept and preserve nils by mapping a packed nil to a null PublishResult in the
returned PublishResult[] so the array length and element positions remain
consistent.

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

🤖 Fix all issues with AI agents
In
@lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java:
- Around line 426-436: The helper waitForDeletedMessageAppear currently swallows
all exceptions in its loop via an empty catch, which can hide real errors from
channel.getMessage; change the catch in waitForDeletedMessageAppear to either
rethrow the exception (or throw a wrapped RuntimeException) after any necessary
cleanup or log the exception and then rethrow so failures surface to the test
runner; keep the existing timeout and sleep behavior and still return when
message.action == MessageAction.MESSAGE_DELETE, but do not leave the catch block
empty—use the exception variable (e) and propagate it.
- Around line 414-424: The waitForUpdatedMessageAppear method currently swallows
all exceptions in an empty catch block; change it to either catch only expected
exceptions (e.g., InterruptedException) or rethrow/unwrap unexpected ones and
log details so test failures aren't hidden: update the catch in
waitForUpdatedMessageAppear to log the exception (including stack trace) using
the test logger or throw a RuntimeException for unexpected errors, and only
suppress transient, documented exceptions around channel.getMessage/Thread.sleep
while still honoring the timeout and MessageAction.MESSAGE_UPDATE check.
- Around line 370-378: The subscriber's error path calls
appendWaiter.onError(null), which can cause NPEs; modify the lambda in
channel.subscribe to construct and pass a non-null ErrorInfo (or an appropriate
error object) to appendWaiter.onError when msg.serial does not match serial,
e.g., create an ErrorInfo with a clear message mentioning Message.serial
mismatch and include relevant context (serial and msg.serial) so
Helpers.AsyncWaiter#onError receives a valid error object.
🧹 Nitpick comments (2)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (2)

38-39: Consider reducing the test timeout.

A 5-minute (300s) timeout is unusually long for integration tests. If tests are expected to complete within seconds, a timeout of 60-120 seconds would catch stuck tests faster while still providing adequate margin.


395-403: Operator precedence could cause unexpected behavior.

The condition history.items().length > 0 && predicate.test(history.items()) || System.currentTimeMillis() > timeout may not behave as intended due to operator precedence. && binds tighter than ||, so this reads as (length > 0 && predicate) || timeout. If you intend to return early on timeout regardless of predicate, the current logic is correct but parentheses would improve clarity.

Proposed fix for clarity
-            if (history.items().length > 0 && predicate.test(history.items()) || System.currentTimeMillis() > timeout)
+            if ((history.items().length > 0 && predicate.test(history.items())) || System.currentTimeMillis() > timeout)
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c4ffcd4 and 99a2c94.

📒 Files selected for processing (11)
  • lib/src/main/java/io/ably/lib/realtime/ChannelBase.java
  • lib/src/main/java/io/ably/lib/realtime/Presence.java
  • lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
  • lib/src/main/java/io/ably/lib/types/ProtocolMessage.java
  • lib/src/main/java/io/ably/lib/types/PublishResult.java
  • lib/src/main/java/io/ably/lib/util/Listeners.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeSuite.java
  • liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt
  • liveobjects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/src/main/java/io/ably/lib/util/Listeners.java
  • lib/src/main/java/io/ably/lib/types/PublishResult.java
🧰 Additional context used
🧬 Code graph analysis (3)
lib/src/main/java/io/ably/lib/realtime/Presence.java (2)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-133)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)
  • Listeners (9-72)
liveobjects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt (1)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-133)
lib/src/main/java/io/ably/lib/transport/ConnectionManager.java (2)
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (1)
  • ProtocolMessage (34-375)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)
  • PublishResult (15-133)
⏰ 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). (12)
  • GitHub Check: build
  • GitHub Check: check (21)
  • GitHub Check: check (24)
  • GitHub Check: check (29)
  • GitHub Check: check (19)
  • GitHub Check: check (29)
  • GitHub Check: check
  • GitHub Check: check-rest-httpurlconnection
  • GitHub Check: check-realtime-httpurlconnection
  • GitHub Check: check-realtime-okhttp
  • GitHub Check: check-rest-okhttp
  • GitHub Check: check-liveobjects
🔇 Additional comments (21)
liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt (1)

18-32: LGTM!

The migration from CompletionListener to Callback<PublishResult> is correctly implemented. The coroutine now properly resumes with the PublishResult on success, maintaining the same error handling pattern.

liveobjects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt (2)

31-34: LGTM!

The test mock correctly uses Callback<PublishResult> and invokes onSuccess with PublishResult(null), which is appropriate for this test scenario where the actual serials aren't being verified.


54-57: LGTM!

The error path test is correctly updated to use the new Callback<PublishResult> interface while maintaining the same error propagation verification.

lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (3)

140-141: LGTM!

The new res field for per-message publish results follows the established pattern for nullable array fields in this class.


215-218: LGTM!

The msgpack serialization correctly follows the pattern used for other array fields, with proper null-checking before serialization.


290-292: LGTM!

The msgpack deserialization correctly reads the res field using PublishResult.readMsgpackArray, consistent with other array field deserialization in this class.

lib/src/test/java/io/ably/lib/test/realtime/RealtimeSuite.java (1)

33-34: LGTM!

The new RealtimeChannelMessageEditTest is appropriately added to the test suite.

lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java (1)

396-406: LGTM!

The test callback is correctly migrated to Callback<PublishResult>. The unused result parameter in onSuccess is appropriate here since the test only verifies that success is NOT called (it would fail the test if invoked).

lib/src/main/java/io/ably/lib/realtime/Presence.java (3)

125-130: LGTM - Parameter type migration with unwrap.

The migration from CompletionListener to Callback<PublishResult> is handled correctly. Listeners.unwrap() extracts the underlying CompletionListener from the wrapper, and QueuedPresence continues to store CompletionListener which is appropriate since presence messages don't need PublishResult serials.


768-768: LGTM - Consistent callback wrapping.

The Listeners.fromCompletionListener(listener) correctly wraps the CompletionListener for the updated connectionManager.send() API.


897-897: LGTM - Consistent callback wrapping for queued messages.

Same pattern applied correctly for sendQueuedMessages().

lib/src/main/java/io/ably/lib/transport/ConnectionManager.java (4)

1728-1734: LGTM - QueuedMessage updated for new callback type.

The QueuedMessage class correctly updated to store Callback<PublishResult> instead of CompletionListener, aligning with the new per-message publish result handling.


1737-1737: LGTM - Send signature updated.

The public send() method signature correctly updated to accept Callback<PublishResult>.


1831-1831: LGTM - Ack method signature extended.

The ack() method now accepts an optional PublishResult[] array to deliver per-message results, with proper @Nullable annotation.


1872-1884: LGTM - Per-message PublishResult delivery.

The indexed loop correctly matches each acknowledged message with its corresponding PublishResult from the results array. The bounds check results.length > i safely handles cases where the results array might be shorter than expected.

lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (6)

1128-1128: LGTM - Publish callback wrapping.

The publish() method correctly uses Listeners.fromCompletionListener(listener) to adapt the callback for the updated connectionManager.send() API.


1217-1257: LGTM - updateMessage overloads restructured.

The method overloads correctly delegate to the most complete signature which calls updateDeleteImpl. The naming and delegation chain are clear.


1266-1306: LGTM - deleteMessage overloads restructured.

Same clean delegation pattern as updateMessage.


1315-1355: LGTM - appendMessage overloads restructured.

Same clean delegation pattern as updateMessage and deleteMessage.


1357-1397: LGTM - Centralized updateDeleteImpl handles all update/delete/append operations.

The implementation correctly:

  • Validates the message serial (lines 1363-1365)
  • Checks connection state and queue policy (lines 1366-1371)
  • Constructs the updated message with the appropriate action and version metadata (lines 1374-1382)
  • Encodes the message and handles errors appropriately (lines 1384-1392)
  • Uses Listeners.toPublishResultListener(listener) to adapt the Callback<UpdateDeleteResult> to Callback<PublishResult> (line 1396)

This centralization reduces code duplication across the three operations.


1715-1715: LGTM - sendProtocolMessage callback wrapping.

Consistent use of Listeners.fromCompletionListener(listener) for the internal protocol message sending.

Comment on lines +370 to +378
Helpers.AsyncWaiter<Message> appendWaiter = new Helpers.AsyncWaiter<>();

channel.subscribe(msg -> {
if (msg.serial.equals(serial)) {
appendWaiter.onSuccess(msg);
} else {
appendWaiter.onError(null);
}
});
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

Passing null to onError may cause issues.

When the serial doesn't match, appendWaiter.onError(null) is called with a null ErrorInfo. This could cause NPEs if the waiter or downstream code expects a non-null error info. Consider providing a descriptive ErrorInfo instead.

Proposed fix
         channel.subscribe(msg -> {
             if (msg.serial.equals(serial)) {
                 appendWaiter.onSuccess(msg);
             } else {
-                appendWaiter.onError(null);
+                appendWaiter.onError(new ErrorInfo("Unexpected message serial: " + msg.serial, 500));
             }
         });
🤖 Prompt for AI Agents
In
@lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java
around lines 370 - 378, The subscriber's error path calls
appendWaiter.onError(null), which can cause NPEs; modify the lambda in
channel.subscribe to construct and pass a non-null ErrorInfo (or an appropriate
error object) to appendWaiter.onError when msg.serial does not match serial,
e.g., create an ErrorInfo with a clear message mentioning Message.serial
mismatch and include relevant context (serial and msg.serial) so
Helpers.AsyncWaiter#onError receives a valid error object.

Comment on lines +414 to +424
private Message waitForUpdatedMessageAppear(Channel channel, String serial) throws Exception {
long timeout = System.currentTimeMillis() + 5_000;
while (true) {
try {
Message message = channel.getMessage(serial);
if ((message != null && message.action == MessageAction.MESSAGE_UPDATE) || System.currentTimeMillis() > timeout)
return message;
Thread.sleep(200);
} catch (Exception e) {}
}
}
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

Swallowing exceptions may hide real test failures.

The empty catch (Exception e) {} block silently ignores all exceptions, which could mask real issues like network errors or unexpected API behavior. Consider logging the exception or only catching expected exceptions.

Proposed fix
             try {
                 Message message = channel.getMessage(serial);
                 if ((message != null && message.action == MessageAction.MESSAGE_UPDATE) || System.currentTimeMillis() > timeout)
                     return message;
                 Thread.sleep(200);
-            } catch (Exception e) {}
+            } catch (AblyException e) {
+                // Expected when message not yet available, continue polling
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private Message waitForUpdatedMessageAppear(Channel channel, String serial) throws Exception {
long timeout = System.currentTimeMillis() + 5_000;
while (true) {
try {
Message message = channel.getMessage(serial);
if ((message != null && message.action == MessageAction.MESSAGE_UPDATE) || System.currentTimeMillis() > timeout)
return message;
Thread.sleep(200);
} catch (Exception e) {}
}
}
private Message waitForUpdatedMessageAppear(Channel channel, String serial) throws Exception {
long timeout = System.currentTimeMillis() + 5_000;
while (true) {
try {
Message message = channel.getMessage(serial);
if ((message != null && message.action == MessageAction.MESSAGE_UPDATE) || System.currentTimeMillis() > timeout)
return message;
Thread.sleep(200);
} catch (AblyException e) {
// Expected when message not yet available, continue polling
}
}
}
🤖 Prompt for AI Agents
In
@lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java
around lines 414 - 424, The waitForUpdatedMessageAppear method currently
swallows all exceptions in an empty catch block; change it to either catch only
expected exceptions (e.g., InterruptedException) or rethrow/unwrap unexpected
ones and log details so test failures aren't hidden: update the catch in
waitForUpdatedMessageAppear to log the exception (including stack trace) using
the test logger or throw a RuntimeException for unexpected errors, and only
suppress transient, documented exceptions around channel.getMessage/Thread.sleep
while still honoring the timeout and MessageAction.MESSAGE_UPDATE check.

Comment on lines +426 to +436
private Message waitForDeletedMessageAppear(Channel channel, String serial) throws Exception {
long timeout = System.currentTimeMillis() + 5_000;
while (true) {
try {
Message message = channel.getMessage(serial);
if ((message != null && message.action == MessageAction.MESSAGE_DELETE) || System.currentTimeMillis() > timeout)
return message;
Thread.sleep(200);
} catch (Exception e) {}
}
}
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

Same exception swallowing issue in waitForDeletedMessageAppear.

This helper has the same empty catch block pattern that could hide real errors.

🤖 Prompt for AI Agents
In
@lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java
around lines 426 - 436, The helper waitForDeletedMessageAppear currently
swallows all exceptions in its loop via an empty catch, which can hide real
errors from channel.getMessage; change the catch in waitForDeletedMessageAppear
to either rethrow the exception (or throw a wrapped RuntimeException) after any
necessary cleanup or log the exception and then rethrow so failures surface to
the test runner; keep the existing timeout and sleep behavior and still return
when message.action == MessageAction.MESSAGE_DELETE, but do not leave the catch
block empty—use the exception variable (e) and propagate it.

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

This pull request implements realtime message edit and delete functionality by replacing REST-based operations with realtime protocol messages for the realtime client. The changes enable publish operations to return per-message results including serials, and introduce a new flow for message update, delete, and append operations through the realtime connection.

Changes:

  • Updated message publish and presence operations to return PublishResult with per-message serials via callbacks
  • Replaced synchronous REST-based message edit/delete operations with asynchronous realtime protocol message operations
  • Added comprehensive test coverage for message edit, delete, append, and versioning workflows

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
lib/src/main/java/io/ably/lib/types/PublishResult.java New type to hold publish results with message serials array
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java Added res field for publish results in protocol messages
lib/src/main/java/io/ably/lib/transport/ConnectionManager.java Updated send methods and ack handling to work with PublishResult callbacks
lib/src/main/java/io/ably/lib/util/Listeners.java Added adapter to convert between PublishResult and UpdateDeleteResult listeners
lib/src/main/java/io/ably/lib/realtime/ChannelBase.java Refactored update/delete/append methods to use realtime protocol instead of REST
lib/src/main/java/io/ably/lib/realtime/Presence.java Updated to use new callback signatures
lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java Added comprehensive tests for message edit functionality
lib/src/test/java/io/ably/lib/test/realtime/RealtimeConnectFailTest.java Updated test mocks for new callback signature
lib/src/test/java/io/ably/lib/test/realtime/RealtimeSuite.java Added new test class to suite
liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt Updated to handle PublishResult returns
liveobjects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt Updated test mocks for new callback signature

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

Comment on lines +390 to +391
assertEquals("Expected MESSAGE_APPEND action through realtime",MessageAction.MESSAGE_APPEND, appendWaiter.result.action);
assertEquals("Expected delta through realtime","Append data", appendWaiter.result.data);
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

There's a missing space after the comma in this assertion message. The string should be "Expected MESSAGE_APPEND action through realtime" with a space between the comma and "MessageAction".

Suggested change
assertEquals("Expected MESSAGE_APPEND action through realtime",MessageAction.MESSAGE_APPEND, appendWaiter.result.action);
assertEquals("Expected delta through realtime","Append data", appendWaiter.result.data);
assertEquals("Expected MESSAGE_APPEND action through realtime", MessageAction.MESSAGE_APPEND, appendWaiter.result.action);
assertEquals("Expected delta through realtime", "Append data", appendWaiter.result.data);

Copilot uses AI. Check for mistakes.
assertEquals("Expected updated data in history", "Initial dataAppend data", retrieved.data);
assertEquals("Expected MESSAGE_UPDATE action in history", MessageAction.MESSAGE_UPDATE, retrieved.action);
assertEquals("Expected MESSAGE_APPEND action through realtime",MessageAction.MESSAGE_APPEND, appendWaiter.result.action);
assertEquals("Expected delta through realtime","Append data", appendWaiter.result.data);
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

There's a missing space after the comma in this assertion message. The string should be "Expected delta through realtime" with a space between the comma and the string literal.

Suggested change
assertEquals("Expected delta through realtime","Append data", appendWaiter.result.data);
assertEquals("Expected delta through realtime", "Append data", appendWaiter.result.data);

Copilot uses AI. Check for mistakes.

ProtocolMessage msg = new ProtocolMessage(Action.message, this.name);
msg.messages = new Message[] { updatedMessage };
connectionManager.send(msg, queueMessages, Listeners.toPublishResultListener(listener));
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

The error handling in this method is inconsistent. When an exception occurs during checkClientId or encode (lines 1385-1392), the listener's onError is called and the method returns early. However, when the send operation at line 1396 fails, any AblyException thrown by connectionManager.send will propagate up to the caller, which means the listener won't be notified of the error. This creates inconsistent behavior where some errors notify the listener while others throw exceptions to the caller. The send call should be wrapped in a try-catch to maintain consistent error handling.

Suggested change
connectionManager.send(msg, queueMessages, Listeners.toPublishResultListener(listener));
try {
connectionManager.send(msg, queueMessages, Listeners.toPublishResultListener(listener));
} catch (AblyException e) {
if (listener != null) {
listener.onError(e.errorInfo);
}
}

Copilot uses AI. Check for mistakes.
import static org.junit.Assert.assertTrue;

/**
* Tests for REST channel message edit and delete operations
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

The comment states "Tests for REST channel message edit and delete operations" but this test file is actually testing realtime channel operations, not REST operations. The comment should be updated to reflect that these are tests for realtime channel message edit and delete operations.

Suggested change
* Tests for REST channel message edit and delete operations
* Tests for realtime channel message edit and delete operations

Copilot uses AI. Check for mistakes.
Comment on lines +399 to +400
if (history.items().length > 0 && predicate.test(history.items()) || System.currentTimeMillis() > timeout)
return history;
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

The condition in this if statement has a logical operator precedence issue. The condition history.items().length > 0 && predicate.test(history.items()) || System.currentTimeMillis() > timeout evaluates as (history.items().length > 0 && predicate.test(history.items())) || System.currentTimeMillis() > timeout, which means the method could return early with empty items if the timeout has passed. This should likely be (history.items().length > 0 && predicate.test(history.items())) || System.currentTimeMillis() > timeout with the timeout check properly grouped, or the logic should be restructured for clarity. Consider adding parentheses: (history.items().length > 0 && predicate.test(history.items())) || System.currentTimeMillis() > timeout or restructuring to check timeout separately.

Copilot uses AI. Check for mistakes.
if ((message != null && message.action == MessageAction.MESSAGE_UPDATE) || System.currentTimeMillis() > timeout)
return message;
Thread.sleep(200);
} catch (Exception e) {}
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

Empty catch blocks that silently swallow exceptions make debugging difficult. When getMessage fails with an exception, this catch block hides the error, which could mask real issues during test execution. Consider logging the exception or at least adding a comment explaining why the exception is intentionally ignored.

Copilot uses AI. Check for mistakes.
if ((message != null && message.action == MessageAction.MESSAGE_DELETE) || System.currentTimeMillis() > timeout)
return message;
Thread.sleep(200);
} catch (Exception e) {}
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

Empty catch blocks that silently swallow exceptions make debugging difficult. When getMessage fails with an exception, this catch block hides the error, which could mask real issues during test execution. Consider logging the exception or at least adding a comment explaining why the exception is intentionally ignored.

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