Skip to content

[AIT-208] feat: update LiveObjects messages fields to support protocol v6 #1195

Open
ttypic wants to merge 1 commit intoAIT-276/apply-on-ackfrom
AIT-208/protocol-v6
Open

[AIT-208] feat: update LiveObjects messages fields to support protocol v6 #1195
ttypic wants to merge 1 commit intoAIT-276/apply-on-ackfrom
AIT-208/protocol-v6

Conversation

@ttypic
Copy link
Contributor

@ttypic ttypic commented Mar 4, 2026

  • Update ObjectOperation fields to protocol v6 names (mapCreate, mapSet, mapRemove, counterCreate, counterInc). Internally and externally
  • Expose typed fields on public ObjectData (boolean, bytes, number, string, json)
  • Remove previous field names on ObjectOperation and ObjectData
  • Handle *CreateWithObjectId operations for apply-on-ACK and message size calculation

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced Live Objects with refined operation types for more granular map and counter operations (creation, updates, removal, and increments now have explicit operation definitions).
  • Chores

    • Updated protocol version to 6.

@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

Walkthrough

This pull request updates the protocol version from "5" to "6" and refactors the LiveObjects operation payload system, replacing generic operation wrappers with explicit typed payloads for map and counter operations across creation, modification, and removal scenarios.

Changes

Cohort / File(s) Summary
Protocol Version Update
lib/src/main/java/io/ably/lib/transport/Defaults.java, lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java, lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java, lib/src/test/java/io/ably/lib/transport/DefaultsTest.java
Updated ABLY_PROTOCOL_VERSION constant from "5" to "6" and corresponding test assertions.
ObjectMessage Payload Types
liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt, liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt
Introduced new explicit payload data classes (MapCreate, MapSet, MapRemove, CounterCreate, CounterInc, MapCreateWithObjectId, CounterCreateWithObjectId, ObjectDelete); replaced ObjectOperation fields mapOp/counterOp/map/counter with explicit operation fields.
Serialization Logic
liveobjects/src/main/kotlin/io/ably/lib/objects/serialization/MsgpackSerialization.kt
Updated msgpack read/write handlers for ObjectOperation and all new payload types; replaced legacy serialization logic for old operation wrappers with explicit encode/decode paths for MapCreate, MapSet, MapRemove, CounterCreate, CounterInc variants.
Creator Operations
liveobjects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt, liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt, liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/DefaultLiveMap.kt
Refactored operation construction to use new payload types; MapCreate now holds semantics and entries; CounterCreate and MapSet directly represent creation and updates.
Manager Application Logic
liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/LiveCounterManager.kt, liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/LiveMapManager.kt
Updated operation dispatch and application logic to match new payload field names (mapSet, mapRemove, counterInc, counterCreate); refactored payload field access and validation accordingly.
Test Fixtures & Serialization Tests
liveobjects/src/test/kotlin/io/ably/lib/objects/unit/fixtures/ObjectMessageFixtures.kt, liveobjects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSerializationTest.kt, liveobjects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSizeTest.kt
Updated fixture construction and serialization tests to use new payload types; adjusted size calculations to reflect new operation structures.
Manager & Counter/Map Tests
liveobjects/src/test/kotlin/io/ably/lib/objects/unit/objects/..., liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/...
Updated all test cases to construct ObjectOperation with new fields (mapCreate, mapSet, mapRemove, counterCreate, counterInc); adjusted imports and payload expectations across DefaultRealtimeObjectsTest, ObjectsManagerTest, DefaultLiveCounterTest, LiveCounterManagerTest, DefaultLiveMapTest, and LiveMapManagerTest.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

This change involves substantial refactoring across multiple interconnected components: new public data types, revised ObjectOperation field signatures, updated serialization/deserialization logic, and coordinated changes across creator, manager, and test layers. While individual changes follow a consistent pattern, the breadth of affected files and the density of interdependent logic modifications require careful cross-file verification.

Poem

🐰 Hops of joy! Protocol dances up from five to six,
Operations shed their old disguises—MapSet, CounterInc mix!
From fuzzy types to crystal-clear, each action finds its place,
Semantics bloom in MapCreate's fields, a nimbler, brighter space! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: updating LiveObjects message fields to support protocol v6, with explicit mention of the affected component and the protocol version bump.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch AIT-208/protocol-v6

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (11)
liveobjects/src/test/kotlin/io/ably/lib/objects/unit/fixtures/ObjectMessageFixtures.kt (1)

131-139: Keep ObjectState.createOp as a create operation in fixtures.

In these fixtures, createOp is switched to MapSet, which conflicts with create-op semantics and LiveMap validation expectations. Consider keeping createOp as MapCreate and using operation for update scenarios.

Also applies to: 149-157

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

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/fixtures/ObjectMessageFixtures.kt`
around lines 131 - 139, The fixture wrongly assigns a MapSet to
ObjectState.createOp (see jsonObjectOperation and jsonObjectState based on
dummyObjectOperation), breaking create-op semantics and LiveMap validation;
revert createOp to a MapCreate instance (or keep the original
dummyObjectOperation with action = ObjectOperationAction.MapCreate) and move the
MapSet usage into the update/operation field (e.g., operation or a separate
update-op fixture) so createOp remains a proper create operation; apply the same
change to the similar fixtures around the other block (lines ~149-157).
liveobjects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSizeTest.kt (1)

42-87: Prefer protocol-valid operation shapes in size tests.

This test currently combines multiple mutually exclusive payloads into one ObjectOperation. Splitting this into per-action cases would better protect against invalid payload-shape regressions.

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

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSizeTest.kt`
around lines 42 - 87, The test builds an invalid ObjectOperation containing
multiple mutually exclusive payloads; instead create separate, protocol-valid
test cases each constructing a single ObjectOperation per action (e.g., one
ObjectOperation with only mapSet set to a MapSet instance to test MAP_SET size,
another with only counterInc set to a CounterInc for COUNTER_INC, one with only
mapCreate set to a MapCreate with its entries for MAP_CREATE, and one with only
counterCreate set to a CounterCreate for COUNTER_CREATE), and update the size
assertions to target each operation’s expected size; locate and modify the
ObjectOperation construction in ObjectMessageSizeTest (and the related
assertions) to split into these per-action instances using the same MapSet,
CounterInc, MapCreate, CounterCreate, and ObjectsMapEntry/ObjectData/ObjectValue
symbols.
liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/DefaultLiveCounterTest.kt (4)

208-208: Same redundant FQN as above.

Can be simplified to CounterInc(number = 5.0).

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

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/DefaultLiveCounterTest.kt`
at line 208, The test uses a redundant fully-qualified name when constructing
the CounterInc instance; replace the FQN io.ably.lib.objects.CounterInc(number =
5.0) with a simple CounterInc(number = 5.0) (update the variable counterInc
assignment in DefaultLiveCounterTest.kt to use the unqualified constructor).

163-163: Same redundant FQN as above.

Can be simplified to CounterInc(number = 5.0).

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

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/DefaultLiveCounterTest.kt`
at line 163, The test uses a redundant fully-qualified name when creating
counterInc; replace the FQN io.ably.lib.objects.CounterInc with the simple class
name CounterInc in DefaultLiveCounterTest (the assignment to variable
counterInc) so it reads CounterInc(number = 5.0), ensuring imports remain
present or add an import for CounterInc if needed.

138-138: Consider removing the fully-qualified name.

The io.ably.lib.objects.CounterInc is already imported at line 4, so you can use just CounterInc(number = 5.0) for consistency with other usages.

♻️ Suggested simplification
-        counterInc = io.ably.lib.objects.CounterInc(number = 5.0)
+        counterInc = CounterInc(number = 5.0)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/DefaultLiveCounterTest.kt`
at line 138, Replace the fully-qualified constructor call
io.ably.lib.objects.CounterInc(number = 5.0) with the imported simple name
CounterInc(number = 5.0) in DefaultLiveCounterTest.kt; update the assignment to
counterInc to use CounterInc(...) (the import for CounterInc at the top already
covers this).

187-187: Same redundant FQN as above.

Can be simplified to CounterInc(number = 5.0).

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

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/DefaultLiveCounterTest.kt`
at line 187, Replace the redundant fully qualified name in the test assignment
by using the simple class name: change the initialization currently using
io.ably.lib.objects.CounterInc(number = 5.0) to just CounterInc(number = 5.0) in
DefaultLiveCounterTest (the counterInc variable assignment) so it matches the
other usages and improves readability.
liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt (1)

4-5: Redundant imports with wildcard.

Lines 4-5 explicitly import CounterCreate and CounterInc, but these are already included via the wildcard import io.ably.lib.objects.* at line 3.

♻️ Remove redundant imports
 import io.ably.lib.objects.*
-import io.ably.lib.objects.CounterCreate
-import io.ably.lib.objects.CounterInc
 import io.ably.lib.objects.ObjectOperation
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt`
around lines 4 - 5, The file DefaultLiveCounter.kt has redundant explicit
imports for CounterCreate and CounterInc that are already covered by the
existing wildcard import io.ably.lib.objects.*; remove the specific import lines
for CounterCreate and CounterInc (the import statements at the top) so only the
wildcard import remains, leaving class DefaultLiveCounter and any references to
CounterCreate/CounterInc unchanged.
liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/LiveCounterManagerTest.kt (2)

4-6: Redundant imports with wildcard.

Lines 4-6 explicitly import CounterCreate, CounterInc, and MapCreate, but these are already included via the wildcard import io.ably.lib.objects.* at line 3.

♻️ Remove redundant imports
 import io.ably.lib.objects.*
-import io.ably.lib.objects.CounterCreate
-import io.ably.lib.objects.CounterInc
-import io.ably.lib.objects.MapCreate
 import io.ably.lib.objects.unit.LiveCounterManager
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/LiveCounterManagerTest.kt`
around lines 4 - 6, Remove the redundant explicit imports CounterCreate,
CounterInc, and MapCreate because they are already brought in by the existing
wildcard import io.ably.lib.objects.*; update the imports block by deleting the
specific lines importing CounterCreate, CounterInc, and MapCreate so only the
wildcard (or the explicit needed imports) remains to avoid duplication.

283-301: Duplicate test case.

This test (LiveCounterManager should throw error when counterInc payload missing) tests the same scenario as the test at lines 241-260 (LiveCounterManager should throw error for missing payload for counter increment operation). Both verify that an AblyException with error code 92000 is thrown when counterInc = null.

Consider removing one of these duplicate tests to reduce maintenance burden.

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

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/LiveCounterManagerTest.kt`
around lines 283 - 301, Remove the duplicate test in LiveCounterManagerTest:
either delete the test named "(RTLC7, RTLC7d2) LiveCounterManager should throw
error when counterInc payload missing" or consolidate it with the existing test
"LiveCounterManager should throw error for missing payload for counter increment
operation" so only one assertion verifies that
liveCounterManager.applyOperation(ObjectOperation(action =
ObjectOperationAction.CounterInc, objectId = "testCounterId", counterInc =
null), null) throws an io.ably.lib.types.AblyException with errorInfo.code ==
92000; update/remove the duplicate test method accordingly to avoid redundant
coverage.
liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/DefaultLiveMapTest.kt (1)

153-154: Consider using imported types instead of fully qualified names.

MapSet is already imported at line 6, but here and in several other test methods (lines 178, 201, 222, 244), fully qualified names like io.ably.lib.objects.MapSet are used. This inconsistency reduces readability.

♻️ Example fix for this line
-        mapSet = io.ably.lib.objects.MapSet(key = "key1", value = io.ably.lib.objects.ObjectData(value = io.ably.lib.objects.ObjectValue.String("value1")))
+        mapSet = MapSet(key = "key1", value = ObjectData(value = ObjectValue.String("value1")))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/DefaultLiveMapTest.kt`
around lines 153 - 154, The tests use fully qualified names like
io.ably.lib.objects.MapSet, io.ably.lib.objects.ObjectData and
io.ably.lib.objects.ObjectValue.String even though MapSet (and likely
ObjectData/ObjectValue) are already imported; replace those fully qualified
usages with the imported type names (MapSet, ObjectData, ObjectValue.String) in
DefaultLiveMapTest (and the other occurrences around the file at the referenced
test methods) to make the code consistent and more readable.
liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/LiveMapManagerTest.kt (1)

4-6: Redundant imports with wildcard.

Lines 4-6 explicitly import MapCreate, MapRemove, and MapSet, but these are already included via the wildcard import io.ably.lib.objects.* at line 3.

♻️ Remove redundant imports
 import io.ably.lib.objects.*
-import io.ably.lib.objects.MapCreate
-import io.ably.lib.objects.MapRemove
-import io.ably.lib.objects.MapSet
 import io.ably.lib.objects.type.livemap.LiveMapEntry
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/LiveMapManagerTest.kt`
around lines 4 - 6, The imports MapCreate, MapRemove, and MapSet are redundant
because io.ably.lib.objects.* already brings them in; remove the explicit import
lines for MapCreate, MapRemove, and MapSet from LiveMapManagerTest.kt so only
the wildcard import io.ably.lib.objects.* remains, keeping the imports clean and
avoiding duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt`:
- Around line 450-452: The size calculations currently use UTF-16 code unit
counts (String.length) causing undercount for multi-byte UTF-8 chars; update the
listed size methods to use the existing byteSize extension instead: in
MapCreate.size() replace it.key.length with it.key.byteSize; in MapSet.size()
replace key.length with key.byteSize; in MapRemove.size() replace key.length
with key.byteSize; in MapCreateWithObjectId.size() and
CounterCreateWithObjectId.size() replace initialValue.length and nonce.length
with initialValue.byteSize and nonce.byteSize; and in ObjectMessage.size()
replace clientId?.length with clientId?.byteSize so all payload string sizes
reflect UTF-8 byte length.

In
`@liveobjects/src/main/kotlin/io/ably/lib/objects/serialization/MsgpackSerialization.kt`:
- Around line 169-176: The ObjectOperation writer currently counts and emits
multiple optional payload fields (mapCreate, mapSet, mapRemove, counterCreate,
counterInc, objectDelete, mapCreateWithObjectId, counterCreateWithObjectId)
without enforcing that exactly one payload is present and that the payload
matches the declared action; update the writer (where those fields are checked)
to validate there is exactly one non-null payload and that it is allowed for the
chosen action enum, throwing/raising an error if not, and update the reader
(after readObjectOperation(...)) to perform the same validation on the parsed
fields so mismatched or multiple payloads are rejected; add explicit checks
referencing the fields above and the function readObjectOperation to locate the
reader validation point.

In
`@liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/LiveMapManager.kt`:
- Around line 109-112: The apply paths currently only read
mapCreate/counterCreate and ignore
mapCreateWithObjectId/counterCreateWithObjectId; update
LiveMapManager.applyOperation and LiveCounterManager.applyOperation to detect
and handle the "*WithObjectId" variants (or canonicalize them to the base
structures) before calling applyMapCreate/applyCounterCreate and
mergeInitialDataFromCreateOperation, and update any helpers that read
operation.mapCreate?.entries or operation.counterCreate?.value to fallback to
operation.mapCreateWithObjectId?.entries and
operation.counterCreateWithObjectId?.value respectively (or normalize the
operation object once at the start of applyOperation so downstream methods like
mergeInitialDataFromCreateOperation always see a populated
mapCreate/counterCreate).

---

Nitpick comments:
In
`@liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt`:
- Around line 4-5: The file DefaultLiveCounter.kt has redundant explicit imports
for CounterCreate and CounterInc that are already covered by the existing
wildcard import io.ably.lib.objects.*; remove the specific import lines for
CounterCreate and CounterInc (the import statements at the top) so only the
wildcard import remains, leaving class DefaultLiveCounter and any references to
CounterCreate/CounterInc unchanged.

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/fixtures/ObjectMessageFixtures.kt`:
- Around line 131-139: The fixture wrongly assigns a MapSet to
ObjectState.createOp (see jsonObjectOperation and jsonObjectState based on
dummyObjectOperation), breaking create-op semantics and LiveMap validation;
revert createOp to a MapCreate instance (or keep the original
dummyObjectOperation with action = ObjectOperationAction.MapCreate) and move the
MapSet usage into the update/operation field (e.g., operation or a separate
update-op fixture) so createOp remains a proper create operation; apply the same
change to the similar fixtures around the other block (lines ~149-157).

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSizeTest.kt`:
- Around line 42-87: The test builds an invalid ObjectOperation containing
multiple mutually exclusive payloads; instead create separate, protocol-valid
test cases each constructing a single ObjectOperation per action (e.g., one
ObjectOperation with only mapSet set to a MapSet instance to test MAP_SET size,
another with only counterInc set to a CounterInc for COUNTER_INC, one with only
mapCreate set to a MapCreate with its entries for MAP_CREATE, and one with only
counterCreate set to a CounterCreate for COUNTER_CREATE), and update the size
assertions to target each operation’s expected size; locate and modify the
ObjectOperation construction in ObjectMessageSizeTest (and the related
assertions) to split into these per-action instances using the same MapSet,
CounterInc, MapCreate, CounterCreate, and ObjectsMapEntry/ObjectData/ObjectValue
symbols.

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/DefaultLiveCounterTest.kt`:
- Line 208: The test uses a redundant fully-qualified name when constructing the
CounterInc instance; replace the FQN io.ably.lib.objects.CounterInc(number =
5.0) with a simple CounterInc(number = 5.0) (update the variable counterInc
assignment in DefaultLiveCounterTest.kt to use the unqualified constructor).
- Line 163: The test uses a redundant fully-qualified name when creating
counterInc; replace the FQN io.ably.lib.objects.CounterInc with the simple class
name CounterInc in DefaultLiveCounterTest (the assignment to variable
counterInc) so it reads CounterInc(number = 5.0), ensuring imports remain
present or add an import for CounterInc if needed.
- Line 138: Replace the fully-qualified constructor call
io.ably.lib.objects.CounterInc(number = 5.0) with the imported simple name
CounterInc(number = 5.0) in DefaultLiveCounterTest.kt; update the assignment to
counterInc to use CounterInc(...) (the import for CounterInc at the top already
covers this).
- Line 187: Replace the redundant fully qualified name in the test assignment by
using the simple class name: change the initialization currently using
io.ably.lib.objects.CounterInc(number = 5.0) to just CounterInc(number = 5.0) in
DefaultLiveCounterTest (the counterInc variable assignment) so it matches the
other usages and improves readability.

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/LiveCounterManagerTest.kt`:
- Around line 4-6: Remove the redundant explicit imports CounterCreate,
CounterInc, and MapCreate because they are already brought in by the existing
wildcard import io.ably.lib.objects.*; update the imports block by deleting the
specific lines importing CounterCreate, CounterInc, and MapCreate so only the
wildcard (or the explicit needed imports) remains to avoid duplication.
- Around line 283-301: Remove the duplicate test in LiveCounterManagerTest:
either delete the test named "(RTLC7, RTLC7d2) LiveCounterManager should throw
error when counterInc payload missing" or consolidate it with the existing test
"LiveCounterManager should throw error for missing payload for counter increment
operation" so only one assertion verifies that
liveCounterManager.applyOperation(ObjectOperation(action =
ObjectOperationAction.CounterInc, objectId = "testCounterId", counterInc =
null), null) throws an io.ably.lib.types.AblyException with errorInfo.code ==
92000; update/remove the duplicate test method accordingly to avoid redundant
coverage.

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/DefaultLiveMapTest.kt`:
- Around line 153-154: The tests use fully qualified names like
io.ably.lib.objects.MapSet, io.ably.lib.objects.ObjectData and
io.ably.lib.objects.ObjectValue.String even though MapSet (and likely
ObjectData/ObjectValue) are already imported; replace those fully qualified
usages with the imported type names (MapSet, ObjectData, ObjectValue.String) in
DefaultLiveMapTest (and the other occurrences around the file at the referenced
test methods) to make the code consistent and more readable.

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/LiveMapManagerTest.kt`:
- Around line 4-6: The imports MapCreate, MapRemove, and MapSet are redundant
because io.ably.lib.objects.* already brings them in; remove the explicit import
lines for MapCreate, MapRemove, and MapSet from LiveMapManagerTest.kt so only
the wildcard import io.ably.lib.objects.* remains, keeping the imports clean and
avoiding duplication.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 17065938-46ca-4898-add7-69593a37aa89

📥 Commits

Reviewing files that changed from the base of the PR and between f57632f and 031a4db.

📒 Files selected for processing (21)
  • lib/src/main/java/io/ably/lib/transport/Defaults.java
  • lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java
  • lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java
  • lib/src/test/java/io/ably/lib/transport/DefaultsTest.java
  • liveobjects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt
  • liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt
  • liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt
  • liveobjects/src/main/kotlin/io/ably/lib/objects/serialization/MsgpackSerialization.kt
  • liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt
  • liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/LiveCounterManager.kt
  • liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/DefaultLiveMap.kt
  • liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/LiveMapManager.kt
  • liveobjects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSerializationTest.kt
  • liveobjects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSizeTest.kt
  • liveobjects/src/test/kotlin/io/ably/lib/objects/unit/fixtures/ObjectMessageFixtures.kt
  • liveobjects/src/test/kotlin/io/ably/lib/objects/unit/objects/DefaultRealtimeObjectsTest.kt
  • liveobjects/src/test/kotlin/io/ably/lib/objects/unit/objects/ObjectsManagerTest.kt
  • liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/DefaultLiveCounterTest.kt
  • liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/LiveCounterManagerTest.kt
  • liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/DefaultLiveMapTest.kt
  • liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/LiveMapManagerTest.kt
💤 Files with no reviewable changes (1)
  • liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt

Comment on lines +450 to +452
private fun MapCreate.size(): Int {
return entries.entries.sumOf { it.key.length + it.value.size() }
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# First, check the file structure and line counts
echo "=== File info ==="
wc -l liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt

# Examine the specific lines mentioned
echo "=== Lines 450-452 (MapCreate.size) ==="
sed -n '450,452p' liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt

echo "=== Lines 457-459 (MapSet.size) ==="
sed -n '457,459p' liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt

echo "=== Lines 464-466 (MapRemove.size) ==="
sed -n '464,466p' liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt

echo "=== Lines 485-487 (MapCreateWithObjectId.size) ==="
sed -n '485,487p' liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt

echo "=== Lines 492-494 (CounterCreateWithObjectId.size) ==="
sed -n '492,494p' liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt

# Check if byteSize is defined in the file
echo "=== Searching for byteSize definition ==="
rg -n 'fun.*byteSize|extension.*byteSize' liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt

# Check context around ObjectValue.String to see how byteSize is used
echo "=== ObjectValue.String references ==="
rg -n 'ObjectValue\.String' liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt -B2 -A2

Repository: ably/ably-java

Length of output: 831


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search entire codebase for byteSize
echo "=== Searching for byteSize definition in codebase ==="
rg -n 'byteSize|fun.*byteSize|extension.*byteSize' --max-count=20

# Check imports in the ObjectMessage.kt file
echo "=== Imports in ObjectMessage.kt ==="
head -30 liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt

# Search for how size() is calculated for ObjectValue
echo "=== ObjectValue.size() implementation ==="
rg -n 'sealed class ObjectValue|is ObjectValue\.String' liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt -B2 -A5 | head -40

# Look for all .size() method implementations in the file
echo "=== All size() methods in ObjectMessage.kt ==="
rg -n 'fun.*size\(\)' liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt -A2

Repository: ably/ably-java

Length of output: 4683


Use UTF-8 byte length in payload size calculations.

String.length counts UTF-16 code units, not UTF-8 bytes. The byteSize extension already exists and is correctly used for ObjectValue.String. The following methods must be updated:

  • MapCreate.size() (line 450): it.key.lengthit.key.byteSize
  • MapSet.size() (line 457): key.lengthkey.byteSize
  • MapRemove.size() (line 464): key.lengthkey.byteSize
  • MapCreateWithObjectId.size() (line 485): initialValue.length, nonce.lengthbyteSize
  • CounterCreateWithObjectId.size() (line 492): initialValue.length, nonce.lengthbyteSize
  • ObjectMessage.size() (line 409): clientId?.lengthclientId?.byteSize (also uses char count instead of bytes)

Multi-byte UTF-8 characters (e.g., "你" = 3 bytes, "😊" = 4 bytes) will be undercounted, risking client-side size checks passing while the server rejects the message.

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

In `@liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt` around
lines 450 - 452, The size calculations currently use UTF-16 code unit counts
(String.length) causing undercount for multi-byte UTF-8 chars; update the listed
size methods to use the existing byteSize extension instead: in MapCreate.size()
replace it.key.length with it.key.byteSize; in MapSet.size() replace key.length
with key.byteSize; in MapRemove.size() replace key.length with key.byteSize; in
MapCreateWithObjectId.size() and CounterCreateWithObjectId.size() replace
initialValue.length and nonce.length with initialValue.byteSize and
nonce.byteSize; and in ObjectMessage.size() replace clientId?.length with
clientId?.byteSize so all payload string sizes reflect UTF-8 byte length.

Comment on lines +169 to +176
if (mapCreate != null) fieldCount++
if (mapSet != null) fieldCount++
if (mapRemove != null) fieldCount++
if (counterCreate != null) fieldCount++
if (counterInc != null) fieldCount++
if (objectDelete != null) fieldCount++
if (mapCreateWithObjectId != null) fieldCount++
if (counterCreateWithObjectId != null) fieldCount++
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Enforce action ↔ payload consistency for ObjectOperation.

Current read/write paths allow multiple payloads and mismatched payload/action combinations. This can produce invalid wire objects and non-deterministic application behavior.

🔒 Suggested guard (writer + reader)
+private fun ObjectOperation.validatePayloadShape() {
+  val payloadCount = listOfNotNull(
+    mapCreate, mapSet, mapRemove, counterCreate, counterInc,
+    objectDelete, mapCreateWithObjectId, counterCreateWithObjectId
+  ).size
+  require(payloadCount <= 1) { "ObjectOperation must contain at most one payload" }
+
+  when (action) {
+    ObjectOperationAction.MapCreate ->
+      require(mapCreate != null || mapCreateWithObjectId != null) { "MAP_CREATE payload missing" }
+    ObjectOperationAction.MapSet ->
+      require(mapSet != null) { "MAP_SET payload missing" }
+    ObjectOperationAction.MapRemove ->
+      require(mapRemove != null) { "MAP_REMOVE payload missing" }
+    ObjectOperationAction.CounterCreate ->
+      require(counterCreate != null || counterCreateWithObjectId != null) { "COUNTER_CREATE payload missing" }
+    ObjectOperationAction.CounterInc ->
+      require(counterInc != null) { "COUNTER_INC payload missing" }
+    ObjectOperationAction.ObjectDelete -> Unit
+    else -> Unit
+  }
+}
+
 private fun ObjectOperation.writeMsgpack(packer: MessagePacker) {
+  validatePayloadShape()
   var fieldCount = 1
   ...
 }

And apply equivalent validation after readObjectOperation(...) parses fields.

Also applies to: 189-227, 243-310

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

In
`@liveobjects/src/main/kotlin/io/ably/lib/objects/serialization/MsgpackSerialization.kt`
around lines 169 - 176, The ObjectOperation writer currently counts and emits
multiple optional payload fields (mapCreate, mapSet, mapRemove, counterCreate,
counterInc, objectDelete, mapCreateWithObjectId, counterCreateWithObjectId)
without enforcing that exactly one payload is present and that the payload
matches the declared action; update the writer (where those fields are checked)
to validate there is exactly one non-null payload and that it is allowed for the
chosen action enum, throwing/raising an error if not, and update the reader
(after readObjectOperation(...)) to perform the same validation on the parsed
fields so mismatched or multiple payloads are rejected; add explicit checks
referencing the fields above and the function readObjectOperation to locate the
reader validation point.

Comment on lines +109 to 112
validateMapSemantics(operation.mapCreate?.semantics) // RTLM16c

return mergeInitialDataFromCreateOperation(operation) // RTLM16d
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== mapCreateWithObjectId producers/consumers ==="
rg -n --type=kt '\bmapCreateWithObjectId\b|\bMapCreateWithObjectId\b' liveobjects/src/main/kotlin

echo
echo "=== call paths to LiveMapManager.applyOperation ==="
rg -n --type=kt '\bapplyOperation\s*\(' liveobjects/src/main/kotlin/io/ably/lib/objects -C3

echo
echo "=== any explicit conversion from mapCreateWithObjectId to mapCreate ==="
rg -n --type=kt 'mapCreateWithObjectId.*mapCreate|initialValue.*MapCreate|readMapCreateWithObjectId' \
  liveobjects/src/main/kotlin -C3

Repository: ably/ably-java

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== mapCreateWithObjectId producers/consumers ==="
rg -n 'mapCreateWithObjectId|MapCreateWithObjectId' liveobjects/src/main/kotlin --glob='*.kt'

echo
echo "=== call paths to LiveMapManager.applyOperation ==="
rg -n 'applyOperation\s*\(' liveobjects/src/main/kotlin/io/ably/lib/objects --glob='*.kt' -C3

echo
echo "=== any explicit conversion from mapCreateWithObjectId to mapCreate ==="
rg -n 'mapCreateWithObjectId.*mapCreate|initialValue.*MapCreate|readMapCreateWithObjectId' \
  liveobjects/src/main/kotlin --glob='*.kt' -C3

Repository: ably/ably-java

Length of output: 12220


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== LiveMapManager.applyMapCreate and surrounding context (lines 100-130) ==="
sed -n '100,130p' liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/LiveMapManager.kt

echo
echo "=== LiveMapManager operation dispatch and action handling (lines 56-80) ==="
sed -n '56,80p' liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/LiveMapManager.kt

echo
echo "=== Lines 236-260 for similar patterns ==="
sed -n '236,260p' liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/LiveMapManager.kt

echo
echo "=== ObjectOperationAction enum to understand action types ==="
rg -n 'enum class ObjectOperationAction|^\s*(MapCreate|MapCreateWithObjectId)' liveobjects/src/main/kotlin --glob='*.kt' -A5

Repository: ably/ably-java

Length of output: 4439


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check if mapCreateWithObjectId is ever converted to mapCreate ==="
rg -n 'mapCreateWithObjectId.*mapCreate|operation\.mapCreateWithObjectId' liveobjects/src/main/kotlin --glob='*.kt' -B2 -A2

echo
echo "=== Check action assignment for operations ==="
rg -n 'action\s*=' liveobjects/src/main/kotlin --glob='*.kt' | head -20

echo
echo "=== ObjectOperation definition to see how action is determined ==="
rg -n 'data class ObjectOperation|val action' liveobjects/src/main/kotlin --glob='*.kt' -B2 -A5

Repository: ably/ably-java

Length of output: 5763


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check ObjectsManager.kt for operation processing ==="
sed -n '210,240p' liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectsManager.kt

echo
echo "=== Search for any initialization or conversion of mapCreate from mapCreateWithObjectId ==="
rg -n 'mapCreate\s*=' liveobjects/src/main/kotlin --glob='*.kt' -B3 -A3

echo
echo "=== Check if applyObjectOperation gets modified before call ==="
rg -n 'applyObjectOperation|applyOperation' liveobjects/src/main/kotlin/io/ably/lib/objects -B5 -A2 --glob='*.kt' | head -60

Repository: ably/ably-java

Length of output: 10758


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Complete ObjectOperation definition ==="
sed -n '199,270p' liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt

echo
echo "=== Check if there's any conditional logic for mapCreateWithObjectId ==="
rg -n 'mapCreateWithObjectId' liveobjects/src/main/kotlin/io/ably/lib/objects/type --glob='*.kt'

Repository: ably/ably-java

Length of output: 1764


Handle MAP_CREATE_WITH_OBJECT_ID and COUNTER_CREATE_WITH_OBJECT_ID operations in the apply paths.

When these payloads arrive with action = MapCreate or action = CounterCreate, the corresponding mapCreateWithObjectId and counterCreateWithObjectId fields are populated while the base mapCreate and counterCreate fields are null. The applyMapCreate and applyCounterCreate methods (and their downstream helpers like mergeInitialDataFromCreateOperation) read only from the base payload fields, silently skipping the create-with-objectId variants.

Add handlers in LiveMapManager.applyOperation and LiveCounterManager.applyOperation to check for and apply the *WithObjectId payloads, or convert them to their base counterparts before the apply methods.

This also applies to lines 236–247 and 251–255 where operation.mapCreate?.entries and operation.counterCreate?.value are read without fallback to the *WithObjectId variants.

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

In
`@liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/LiveMapManager.kt`
around lines 109 - 112, The apply paths currently only read
mapCreate/counterCreate and ignore
mapCreateWithObjectId/counterCreateWithObjectId; update
LiveMapManager.applyOperation and LiveCounterManager.applyOperation to detect
and handle the "*WithObjectId" variants (or canonicalize them to the base
structures) before calling applyMapCreate/applyCounterCreate and
mergeInitialDataFromCreateOperation, and update any helpers that read
operation.mapCreate?.entries or operation.counterCreate?.value to fallback to
operation.mapCreateWithObjectId?.entries and
operation.counterCreateWithObjectId?.value respectively (or normalize the
operation object once at the start of applyOperation so downstream methods like
mergeInitialDataFromCreateOperation always see a populated
mapCreate/counterCreate).

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.

1 participant