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

    • Callbacks now return per-message publish results (including serials).
    • New message edit APIs: update, delete, and append with multiple overloads supporting async callbacks.
  • Bug Fixes

    • Edit flows unified into a validated async path with consistent queuing, validation, and per-item acknowledgements.
    • Presence and send flows now surface per-message results and clearer error details.
  • Tests

    • Added comprehensive realtime tests for publish, update, append, delete, and message version/history.

✏️ 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

Note

Other AI code review bot(s) detected

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

Walkthrough

Centralizes update/delete/append handling in ChannelBase.updateDeleteImpl; adds PublishResult and ProtocolMessage.res; switches send/ack paths from CompletionListener to Callback via Listeners adapters; updates Presence, ConnectionManager, Kotlin helpers, tests, and adds realtime message-edit tests.

Changes

Cohort / File(s) Summary
Channel API
lib/src/main/java/io/ably/lib/realtime/ChannelBase.java
Adds multiple overloads for updateMessage/deleteMessage/appendMessage that route to a new private updateDeleteImpl(...) which validates message.serial, populates action/version, checks clientId, encodes and sends a ProtocolMessage via ConnectionManager using Listeners adapters; adds logging and Blocking/NonBlocking annotations.
Transport / Ack flow
lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
Changes send/sendImpl and queued message listener types from CompletionListenerCallback<PublishResult>; ack now accepts @Nullable PublishResult[] results and maps per-item PublishResult to corresponding callbacks using index-based iteration.
Protocol model
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java
Adds public @nullable PublishResult[] res and integrates it into MsgPack (de)serialization and field count handling.
PublishResult type
lib/src/main/java/io/ably/lib/types/PublishResult.java
New PublishResult class with @Nullable String[] serials, JSON/MsgPack read/write helpers, MsgPack array helpers, and an HTTP body handler that produces serial arrays.
Listener utilities
lib/src/main/java/io/ably/lib/util/Listeners.java
New adapters bridging CompletionListener and generic Callback<T>: fromCompletionListener(CompletionListener), toPublishResultListener(Callback<UpdateDeleteResult>), and unwrap(Callback<T>); includes wrapper implementations.
Presence path
lib/src/main/java/io/ably/lib/realtime/Presence.java
Switches pending/queued presence and send logic to Callback<PublishResult>; addPendingPresence signature updated; queued presence stores unwrapped listener and sends via 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
Adds extensive realtime message edit/version/delete tests; updates tests to use Callback<PublishResult> at send sites; includes new test in suite.
Kotlin helper & tests
liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt, liveobjects/src/test/.../HelpersTest.kt
ObjectsAdapter.sendAsync now returns PublishResult and uses Callback<PublishResult> to resume coroutines; unit tests updated to provide PublishResult in callbacks.
Misc (imports/logging)
multiple lib/src/...
Added imports for PublishResult, MessageAction, Listeners; updated send sites to wrap/unwrap listeners with Listeners helpers; added logging around publish/update/delete flows.

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, 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 res[] 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 39.73% 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 directly reflects the main feature introduced: realtime message edits and deletes functionality, which is the core change across multiple files.
Linked Issues check ✅ Passed The PR implements realtime message edits and deletes with comprehensive API updates, new serialization support, and integration across multiple components as indicated by the objectives.
Out of Scope Changes check ✅ Passed All changes are directly related to realtime message edits/deletes: new PublishResult type, Listeners adapter, ChannelBase API overloads, ConnectionManager updates, Presence changes, and comprehensive 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.

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

@ttypic ttypic force-pushed the AIT-98/message-edits-deletes branch from c4ffcd4 to 99a2c94 Compare January 13, 2026 10:48
@github-actions github-actions bot temporarily deployed to staging/pull/1183/features January 13, 2026 10:49 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1183/javadoc January 13, 2026 10:51 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/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.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

@ttypic ttypic force-pushed the AIT-99/message-appends branch 2 times, most recently from da7d431 to 6563568 Compare January 20, 2026 09:07
@ttypic ttypic force-pushed the AIT-98/message-edits-deletes branch from 47b958f to e61aff4 Compare January 20, 2026 09:11
@github-actions github-actions bot temporarily deployed to staging/pull/1183/features January 20, 2026 09:11 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1183/javadoc January 20, 2026 09:13 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: 0

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/types/ProtocolMessage.java (1)

140-218: Add nil handling to PublishResult.readMsgpackArray() to match write symmetry.

ProtocolMessage.res (line 290-292) decodes via PublishResult.readMsgpackArray, which currently assumes every element is a map. However, writeMsgpackArray (lines 71-85) can write null elements as nil. If the server sends nil entries in the res array, decoding will throw an exception. Add nil checking before calling readMsgpack to handle this case, consistent with the serials array nil handling at lines 56-61.

Proposed fix in PublishResult.readMsgpackArray
 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;
 }
♻️ Duplicate comments (5)
lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (2)

48-50: Guard ably before closing in @After.

If setup fails, tearDown() can NPE.

💡 Suggested fix
 `@After`
 public void tearDown() {
-    ably.close();
+    if (ably != null) {
+        ably.close();
+    }
 }

357-392: Don’t fail the append test on unrelated messages or call onError(null).

The current subscription turns unrelated traffic into a failure and passes a null error, which makes the test flaky. Only complete the waiter on the expected serial; otherwise ignore.

🔧 Suggested fix
-        channel.subscribe(msg -> {
-            if (msg.serial.equals(serial)) {
-                appendWaiter.onSuccess(msg);
-            } else {
-                appendWaiter.onError(null);
-            }
-        });
+        channel.subscribe(msg -> {
+            if (serial.equals(msg.serial)) {
+                appendWaiter.onSuccess(msg);
+            }
+        });
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)

87-93: Handle MsgPack NIL entries in arrays.
writeMsgpackArray packs nil for null elements, but readMsgpackArray doesn’t consume nil, which will break decoding if any nulls are present.

🐛 Proposed fix
 public static PublishResult[] readMsgpackArray(MessageUnpacker unpacker) throws IOException {
     int count = unpacker.unpackArrayHeader();
     PublishResult[] results = new PublishResult[count];
     for (int i = 0; i < count; i++) {
-        results[i] = readMsgpack(unpacker);
+        if (unpacker.getNextFormat().equals(MessageFormat.NIL)) {
+            unpacker.unpackNil();
+            results[i] = null;
+        } else {
+            results[i] = readMsgpack(unpacker);
+        }
     }
     return results;
 }
lib/src/main/java/io/ably/lib/transport/ConnectionManager.java (1)

1831-1878: Align PublishResult[] with adjusted msgSerial.
When msgSerial < startSerial, count is reduced but results isn’t offset, so callbacks can receive the wrong PublishResult.

🐛 Proposed fix (offset results)
 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);
+            int delta = (int)(startSerial - msgSerial);
+            resultsOffset = delta;
+            count -= delta;
             if(count < 0)
                 count = 0;
             msgSerial = startSerial;
         }
         ...
     }
     ...
     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 ri = i + resultsOffset;
+                    PublishResult messageResult = results != null && results.length > ri ? results[ri] : null;
                     msg.listener.onSuccess(messageResult);
                 }
             } catch (Throwable t) {
                 Log.e(TAG, "ack(): listener exception", t);
             }
         }
     }
 }
lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (1)

1400-1417: Don’t swallow errors when no callback is supplied.
For update/delete/append overloads without a listener, AblyException is silently consumed in both encode and send paths. This makes failures invisible to synchronous callers.

🐛 Proposed fix (rethrow when listener is null)
         try {
             ably.auth.checkClientId(message, true, connected);
             updatedMessage.encode(options);
         } catch (AblyException e) {
             if (listener != null) {
                 listener.onError(e.errorInfo);
-            }
-            return;
+                return;
+            }
+            throw e;
         }

         ProtocolMessage msg = new ProtocolMessage(Action.message, this.name);
         msg.messages = new Message[] { updatedMessage };
         try {
             connectionManager.send(msg, queueMessages, Listeners.toPublishResultListener(listener));
         } catch (AblyException e) {
             if (listener != null) {
                 listener.onError(e.errorInfo);
+            } else {
+                throw e;
             }
         }
🧹 Nitpick comments (1)
lib/src/main/java/io/ably/lib/util/Listeners.java (1)

9-13: Make this utility class non-instantiable.
All members are static; consider final + private constructor to prevent accidental instantiation/extension.

💡 Suggested refactor
-public class Listeners {
+public final class Listeners {
+
+    private Listeners() {}

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 96-127: The BodyHandler generic is wrong: PublishResultBodyHandler
implements HttpCore.BodyHandler<String> but its handleResponseBody returns
String[]; change the generic to HttpCore.BodyHandler<String[]> and update
getBodyHandler() to return HttpCore.BodyHandler<String[]> so the types match
(adjust PublishResultBodyHandler class declaration and the getBodyHandler()
method signature accordingly); ensure handleResponseBody remains returning
String[] in the PublishResultBodyHandler implementation.
♻️ Duplicate comments (4)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)

87-92: Handle nil elements when reading MsgPack arrays.
writeMsgpackArray writes nil for null entries, but readMsgpackArray unconditionally calls readMsgpack, which will fail on nil.

🛠️ Proposed fix
     for (int i = 0; i < count; i++) {
-        results[i] = readMsgpack(unpacker);
+        if (unpacker.getNextFormat().equals(MessageFormat.NIL)) {
+            unpacker.unpackNil();
+            results[i] = null;
+        } else {
+            results[i] = readMsgpack(unpacker);
+        }
     }
lib/src/test/java/io/ably/lib/test/realtime/RealtimeChannelMessageEditTest.java (2)

48-51: Guard ably before closing in tearDown.
If setup fails, this can NPE.

🛠️ Proposed fix
     `@After`
     public void tearDown() {
-        ably.close();
+        if (ably != null) {
+            ably.close();
+        }
     }

372-377: Avoid failing the test on unrelated messages (and avoid onError(null)).
Unrelated messages can arrive on the channel; treating them as errors is flaky and passing null can cascade into NPEs.

🛠️ Proposed fix
         channel.subscribe(msg -> {
             if (msg.serial.equals(serial)) {
                 appendWaiter.onSuccess(msg);
-            } else {
-                appendWaiter.onError(null);
             }
         });
lib/src/main/java/io/ably/lib/realtime/ChannelBase.java (1)

1400-1418: Don’t swallow exceptions when listener is null (sync overloads).
updateMessage(Message) / deleteMessage(Message) can fail silently. Propagate the exception when no listener is provided.

🛠️ Proposed fix
         } catch (AblyException e) {
             if (listener != null) {
                 listener.onError(e.errorInfo);
-            }
-            return;
+                return;
+            }
+            throw e;
         }
 ...
         } catch (AblyException e) {
             if (listener != null) {
                 listener.onError(e.errorInfo);
+            } else {
+                throw e;
             }
         }
🧹 Nitpick comments (1)
lib/src/main/java/io/ably/lib/types/PublishResult.java (1)

29-31: Avoid platform-default charset for JSON decoding.
Using the default charset can yield inconsistent behavior across environments.

♻️ Suggested tweak
+import java.nio.charset.StandardCharsets;
...
-        return Serialisation.gson.fromJson(new String(packed), PublishResult.class);
+        return Serialisation.gson.fromJson(new String(packed, StandardCharsets.UTF_8), PublishResult.class);

Copy link
Collaborator

@sacOO7 sacOO7 left a comment

Choose a reason for hiding this comment

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

lgtm

Base automatically changed from AIT-99/message-appends to main January 20, 2026 11:24
@ttypic ttypic merged commit 85af5c4 into main Jan 20, 2026
17 of 19 checks passed
@ttypic ttypic deleted the AIT-98/message-edits-deletes branch January 20, 2026 11:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants