Skip to content

Conversation

@sacOO7
Copy link
Collaborator

@sacOO7 sacOO7 commented Jun 16, 2025

  1. Added LiveObjectsHelper for initializing LiveObjects plugin and related serializer
  2. Updated ProtocolMessage to handle msgpack serialization/deserialization

Decided to have separate serialization over just single gson serialization because =>

  1. In case of msgpack, needs two layers of serialization, first is always json and second is to msgpack.
  2. Even then conversation from json to msgpack for binary type( byte array ) is not compatible ( need to add hack for the same by checking binary field )
  3. Doesn't conform to the spec as per Add spec for ObjectMessage encoding and decoding specification#335 (comment)

Summary by CodeRabbit

  • New Features

    • Added support for serializing and deserializing arrays of live objects in both JSON and MessagePack formats.
    • Introduced a new field to protocol messages to carry live object state.
    • Enhanced object messaging with custom serialization for complex data types, including binary and JSON objects.
  • Improvements

    • Streamlined and unified serialization logic for live object-related classes.
    • Improved enum serialization by encoding them as integer codes for efficiency.
  • Bug Fixes

    • Ensured null fields are properly omitted or handled during serialization and deserialization.
  • Tests

    • Added comprehensive tests for object message serialization, deserialization, and handling of nulls and enums.
    • Introduced fixtures for reusable dummy data in tests.
  • Chores

    • Updated dependencies and build configuration to support new serialization features.

@coderabbitai
Copy link

coderabbitai bot commented Jun 16, 2025

Walkthrough

This change introduces comprehensive serialization and deserialization support for LiveObjects within the Ably Java library. It adds new serializers for both JSON and MessagePack formats, integrates them into protocol and object message classes, updates build dependencies, and provides extensive unit tests and fixtures to ensure correct round-trip serialization of LiveObjects data structures.

Changes

File(s) Change Summary
gradle/libs.versions.toml, live-objects/build.gradle.kts Added Jackson MessagePack and parameter names dependencies for serialization support; updated Kotlin compiler options.
lib/src/main/java/io/ably/lib/objects/LiveObjectSerializer.java, live-objects/src/main/kotlin/io/ably/lib/objects/serialization/Serialization.kt Introduced the LiveObjectSerializer interface and its implementation for serializing arrays of LiveObjects in JSON/MsgPack.
lib/src/main/java/io/ably/lib/objects/LiveObjectsHelper.java Added helper class for dynamic plugin and serializer initialization via reflection.
lib/src/main/java/io/ably/lib/objects/LiveObjectsJsonSerializer.java Added custom JSON serializer/deserializer for arrays of LiveObjects using Gson.
lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java Delegated LiveObjects plugin initialization to new helper class.
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java Added state field (array of LiveObjects) with custom serialization logic for JSON/MsgPack.
lib/src/main/java/io/ably/lib/types/ProtocolSerializer.java Updated exception handling for MsgPack serialization method.
live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt Made Binary data non-nullable and simplified related logic.
live-objects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt Annotated data classes with custom serializers; updated field types and removed obsolete fields.
live-objects/src/main/kotlin/io/ably/lib/objects/Serialization.kt Deleted legacy Gson serializer singleton.
live-objects/src/main/kotlin/io/ably/lib/objects/serialization/JsonSerialization.kt Added custom Gson (de)serializers and type adapters for LiveObjects-related classes.
live-objects/src/main/kotlin/io/ably/lib/objects/serialization/MsgpackSerialization.kt Added Jackson-based MessagePack (de)serializers and extension functions for LiveObjects.
live-objects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSerializationTest.kt Added unit tests for JSON and MsgPack serialization/deserialization of LiveObjects.
live-objects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSizeTest.kt Updated tests to match new field types and serialization logic.
live-objects/src/test/kotlin/io/ably/lib/objects/unit/fixtures/ObjectMessageFixture.kt Added reusable dummy data and helper functions for serialization tests.

Sequence Diagram(s)

sequenceDiagram
    participant AblyRealtime
    participant LiveObjectsHelper
    participant LiveObjectSerializer
    participant ProtocolMessage
    participant MsgPack/JSON

    AblyRealtime->>LiveObjectsHelper: tryInitializeLiveObjectsPlugin(this)
    LiveObjectsHelper->>LiveObjectSerializer: getLiveObjectSerializer()
    ProtocolMessage->>LiveObjectSerializer: serialize/deserialize state (array)
    LiveObjectSerializer->>MsgPack/JSON: Perform (de)serialization
    MsgPack/JSON-->>LiveObjectSerializer: Serialized/Deserialized data
    LiveObjectSerializer-->>ProtocolMessage: Object[] state
Loading

Assessment against linked issues

Objective Addressed Explanation
Implement LiveObjects serialization infrastructure (ECO-5386)
Provide custom (de)serializers for JSON and MsgPack for LiveObjects types (ECO-5386)
Integrate LiveObjects serialization into ProtocolMessage and AblyRealtime (ECO-5386)
Add unit tests verifying round-trip serialization for LiveObjects (ECO-5386)

Assessment against linked issues: Out-of-scope changes

No out-of-scope changes found.

Possibly related PRs

Suggested reviewers

  • ttypic

Poem

In the fields of code, a rabbit hops with glee,
For LiveObjects now serialize with accuracy!
JSON and MsgPack, both handled with care,
Arrays of state, now easy to share.
With tests and helpers, the work is complete—
This carrot of progress is truly a treat! 🥕

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate Unit Tests
  • Create PR with Unit Tests
  • Post Copyable Unit Tests in Comment
  • Commit Unit Tests in branch chore/liveobjects-serialization

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

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

Support

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

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai auto-generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Documentation and Community

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

@github-actions github-actions bot temporarily deployed to staging/pull/1101/features June 16, 2025 14:31 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/javadoc June 16, 2025 14:33 Inactive
@sacOO7 sacOO7 force-pushed the chore/liveobjects-serialization branch from 0592f72 to ec2c2d3 Compare June 16, 2025 14:34
@github-actions github-actions bot temporarily deployed to staging/pull/1101/features June 16, 2025 14:34 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/javadoc June 16, 2025 14:37 Inactive
@sacOO7 sacOO7 force-pushed the chore/liveobjects-serialization branch from ec2c2d3 to 6a4e367 Compare June 17, 2025 10:01
@github-actions github-actions bot temporarily deployed to staging/pull/1101/features June 17, 2025 10:02 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/javadoc June 17, 2025 10:03 Inactive
@sacOO7 sacOO7 force-pushed the chore/liveobjects-serialization branch from 6a4e367 to 9c79bbd Compare June 17, 2025 10:32
@github-actions github-actions bot temporarily deployed to staging/pull/1101/features June 17, 2025 10:33 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/javadoc June 17, 2025 10:35 Inactive
@sacOO7 sacOO7 force-pushed the chore/liveobjects-serialization branch from 9c79bbd to d227718 Compare June 17, 2025 11:04
@github-actions github-actions bot temporarily deployed to staging/pull/1101/features June 17, 2025 11:04 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/javadoc June 17, 2025 11:06 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/features June 17, 2025 13:16 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/javadoc June 17, 2025 13:18 Inactive
@sacOO7 sacOO7 force-pushed the chore/liveobjects-serialization branch from 42ca7a6 to 564a31b Compare June 17, 2025 13:36
@github-actions github-actions bot temporarily deployed to staging/pull/1101/features June 17, 2025 13:37 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/javadoc June 17, 2025 13:39 Inactive
@sacOO7 sacOO7 force-pushed the chore/liveobjects-serialization branch from 564a31b to 83ff3c0 Compare June 17, 2025 14:01
@github-actions github-actions bot temporarily deployed to staging/pull/1101/features June 17, 2025 14:02 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/javadoc June 17, 2025 14:03 Inactive
@sacOO7 sacOO7 force-pushed the chore/liveobjects-serialization branch from 83ff3c0 to 9d19c2b Compare June 17, 2025 14:05
@github-actions github-actions bot temporarily deployed to staging/pull/1101/features June 17, 2025 14:06 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/javadoc June 17, 2025 14:07 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/features June 18, 2025 12:28 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/javadoc June 18, 2025 12:31 Inactive
@sacOO7 sacOO7 force-pushed the chore/liveobjects-serialization branch from 10a09e3 to e3d62d9 Compare June 18, 2025 13:02
@github-actions github-actions bot temporarily deployed to staging/pull/1101/features June 18, 2025 13:03 Inactive
@sacOO7 sacOO7 force-pushed the chore/liveobjects-serialization branch from c4f91d5 to b32e76f Compare June 20, 2025 11:48
@github-actions github-actions bot temporarily deployed to staging/pull/1101/features June 20, 2025 11:48 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/javadoc June 20, 2025 11:50 Inactive
@sacOO7 sacOO7 force-pushed the chore/liveobjects-serialization branch from b32e76f to 86c746e Compare June 20, 2025 13:41
@github-actions github-actions bot temporarily deployed to staging/pull/1101/features June 20, 2025 13:42 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/javadoc June 20, 2025 13:43 Inactive
…ialization

1. Created ObjectMessageFixtures to represent dummy data in various formats
2. Added jackson-param dependency to fix jackson deserialization issue
3. Marked javaParameters as true on compilerOptions
@sacOO7 sacOO7 force-pushed the chore/liveobjects-serialization branch from 86c746e to 7e4f5d6 Compare June 20, 2025 15:58
@github-actions github-actions bot temporarily deployed to staging/pull/1101/features June 20, 2025 15:59 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/javadoc June 20, 2025 16:01 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/features June 22, 2025 12:45 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/javadoc June 22, 2025 12:47 Inactive
@sacOO7 sacOO7 force-pushed the chore/liveobjects-serialization branch from ef65124 to ee6b90f Compare June 22, 2025 16:55
@github-actions github-actions bot temporarily deployed to staging/pull/1101/features June 22, 2025 16:56 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/javadoc June 22, 2025 16:58 Inactive
@sacOO7 sacOO7 force-pushed the chore/liveobjects-serialization branch from ee6b90f to b68d962 Compare June 22, 2025 17:24
@github-actions github-actions bot temporarily deployed to staging/pull/1101/features June 22, 2025 17:24 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1101/javadoc June 22, 2025 17:26 Inactive
1. Updated test for testOmitNullsInObjectMessageSerialization
2. Added test HandleNullsInObjectMessageDeserialization
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: 9

🧹 Nitpick comments (5)
lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (1)

203-211: Robust MessagePack serialization with proper error handling.

The MessagePack serialization and deserialization logic for the state field includes appropriate null checks and error handling when the LiveObjectSerializer is unavailable. The warning logs provide helpful debugging information.

Consider caching the LiveObjectSerializer instance to avoid repeated lookups:

+    private static LiveObjectSerializer cachedSerializer;
+    
+    private static LiveObjectSerializer getOrCreateSerializer() {
+        if (cachedSerializer == null) {
+            cachedSerializer = LiveObjectsHelper.getLiveObjectSerializer();
+        }
+        return cachedSerializer;
+    }

Then use getOrCreateSerializer() instead of repeated LiveObjectsHelper.getLiveObjectSerializer() calls.

Also applies to: 274-282

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

14-17: Consider lazy initialization of the serializer field.

Storing the LiveObjectSerializer in an instance field during construction could lead to issues if the serializer becomes unavailable later or if this class is instantiated before the LiveObjects plugin is properly initialized.

Consider lazy initialization:

-    private final LiveObjectSerializer serializer = LiveObjectsHelper.getLiveObjectSerializer();
+    private LiveObjectSerializer getSerializer() {
+        return LiveObjectsHelper.getLiveObjectSerializer();
+    }

Then replace serializer references with getSerializer() calls.

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

13-25: Consider caching the plugin instance or making the method more robust.

The tryInitializeLiveObjectsPlugin method creates a new plugin instance on every call, which could be inefficient. Also, consider whether the method should handle the case where ablyRealtime is null.

Consider adding a null check for the ablyRealtime parameter and potentially caching the plugin instance if it's meant to be reused.

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

55-56: Fix incorrect comment.

The comment says "MsgPack format" but the code is actually serializing to JSON format.

-    // Serialize the ProtocolMessage containing ObjectMessages to MsgPack format
+    // Serialize the ProtocolMessage containing ObjectMessages to JSON format
live-objects/src/main/kotlin/io/ably/lib/objects/serialization/MsgpackSerialization.kt (1)

33-36: Remove redundant inline comment.

The implementation is correct, but the inline comment is unnecessary as it merely restates what the code does.

 internal fun ObjectMessage.writeTo(packer: MessagePacker) {
-    val msgpackBytes = msgpackMapper.writeValueAsBytes(this) // returns correct msgpack map structure
+    val msgpackBytes = msgpackMapper.writeValueAsBytes(this)
     packer.writePayload(msgpackBytes)
 }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 80a655e and 792ffc9.

📒 Files selected for processing (17)
  • gradle/libs.versions.toml (2 hunks)
  • lib/src/main/java/io/ably/lib/objects/LiveObjectSerializer.java (1 hunks)
  • lib/src/main/java/io/ably/lib/objects/LiveObjectsHelper.java (1 hunks)
  • lib/src/main/java/io/ably/lib/objects/LiveObjectsJsonSerializer.java (1 hunks)
  • lib/src/main/java/io/ably/lib/realtime/AblyRealtime.java (2 hunks)
  • lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (5 hunks)
  • lib/src/main/java/io/ably/lib/types/ProtocolSerializer.java (2 hunks)
  • live-objects/build.gradle.kts (2 hunks)
  • live-objects/src/main/kotlin/io/ably/lib/objects/Helpers.kt (1 hunks)
  • live-objects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt (5 hunks)
  • live-objects/src/main/kotlin/io/ably/lib/objects/Serialization.kt (0 hunks)
  • live-objects/src/main/kotlin/io/ably/lib/objects/serialization/JsonSerialization.kt (1 hunks)
  • live-objects/src/main/kotlin/io/ably/lib/objects/serialization/MsgpackSerialization.kt (1 hunks)
  • live-objects/src/main/kotlin/io/ably/lib/objects/serialization/Serialization.kt (1 hunks)
  • live-objects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSerializationTest.kt (1 hunks)
  • live-objects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSizeTest.kt (4 hunks)
  • live-objects/src/test/kotlin/io/ably/lib/objects/unit/fixtures/ObjectMessageFixture.kt (1 hunks)
💤 Files with no reviewable changes (1)
  • live-objects/src/main/kotlin/io/ably/lib/objects/Serialization.kt
🔇 Additional comments (30)
live-objects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSizeTest.kt (3)

3-3: LGTM - Import addition supports type migration.

The JsonObject import is necessary for the extras field type change from Map to JsonObject.


84-84: Verify the test data change aligns with the new Binary constructor.

The initialValue has been changed from a binary representation of "initial" to "some-value" as a string converted to ByteArray. This change should align with the Binary class constructor changes that now require non-nullable ByteArray.

Ensure this change doesn't affect the test's intent or size calculations.


37-40: Verify the JSON serialization size calculation.

The extras field has been changed from Map to JsonObject with explicit property additions. The comment indicates the total size should be 26 bytes for the JSON {"meta":"data","count":42}, which seems reasonable.

However, ensure that the Gson serialization of this JsonObject produces the expected byte length when converted to UTF-8.

#!/bin/bash
# Verify JsonObject serialization behavior in tests
ast-grep --pattern $'JsonObject().apply {
  $$$
}'
lib/src/main/java/io/ably/lib/types/ProtocolSerializer.java (2)

31-31: Improved error handling with proper exception declaration.

The method signature change to declare AblyException improves the API contract by making potential exceptions explicit.


38-40: Enhanced error propagation replaces null return.

The change from returning null on IOException to throwing AblyException provides better error handling and consistency with the readMsgpack method. This ensures callers can properly handle serialization failures instead of dealing with null return values.

live-objects/build.gradle.kts (3)

14-14: LGTM - Common bundle dependency addition.

The common bundle dependency supports shared functionality across the live-objects module.


16-17: Essential dependencies for Jackson serialization support.

The Jackson MessagePack and parameter names modules are necessary for the new serialization framework. These dependencies enable:

  • MessagePack format serialization/deserialization
  • Automatic JSON property to constructor parameter mapping without explicit annotations

51-57: Well-documented compiler option for Jackson integration.

The javaParameters = true option enables Jackson to map JSON properties to constructor parameters without @JsonProperty annotations. The comment correctly explains that this adds parameter metadata to bytecode while maintaining binary compatibility.

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

42-42: Improved API design with non-nullable ByteArray.

Changing the constructor to require a non-nullable ByteArray simplifies the Binary class design and eliminates potential null pointer issues. This is a good API improvement that makes the data contract clearer.


46-46: Simplified equals method after nullable removal.

The removal of null checks in the equals method is consistent with the non-nullable ByteArray constructor change. The direct use of contentEquals is now safe and more concise.


50-50: Consistent hashCode implementation.

The hashCode method correctly uses contentHashCode() directly without null checks, consistent with the non-nullable data field.


55-55: Simplified size calculation.

The size() extension function now directly returns data.size without null checks, which is consistent with the non-nullable ByteArray design.

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

8-8: Good refactoring to centralize LiveObjects initialization.

The import change to LiveObjectsHelper supports the architectural improvement of centralizing live objects plugin management in a dedicated helper class.


75-75: Improved separation of concerns with helper delegation.

Delegating the live objects plugin initialization to LiveObjectsHelper.tryInitializeLiveObjectsPlugin(this) is a good architectural decision. This removes reflection logic from the main AblyRealtime class and centralizes it in a specialized helper, improving maintainability and code organization.

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

1-51: Well-designed interface with clear separation of concerns.

The LiveObjectSerializer interface provides a clean abstraction for live object serialization with proper method signatures, comprehensive documentation, and appropriate nullability annotations. The separation between MessagePack and JSON serialization methods is logical and maintainable.

gradle/libs.versions.toml (1)

28-29: ```shell
#!/bin/bash

Verify latest versions of the two Jackson libraries

echo "Latest jackson-dataformat-msgpack version:"
curl -s "https://repo1.maven.org/maven2/org/msgpack/jackson-dataformat-msgpack/maven-metadata.xml"
| grep -oP "(?<=)[^<]+" | sort -V | tail -n1

echo "Latest jackson-module-parameter-names version:"
curl -s "https://repo1.maven.org/maven2/com/fasterxml/jackson/module/jackson-module-parameter-names/maven-metadata.xml"
| grep -oP "(?<=)[^<]+" | sort -V | tail -n1


</details>
<details>
<summary>lib/src/main/java/io/ably/lib/types/ProtocolMessage.java (1)</summary>

`7-11`: **Well-integrated state field with proper annotations.**

The addition of the `state` field with custom JSON adapter is well-implemented. The nullable annotation and comprehensive documentation provide clear expectations for usage.




Also applies to: 131-138

</details>
<details>
<summary>lib/src/main/java/io/ably/lib/objects/LiveObjectsJsonSerializer.java (1)</summary>

`18-28`: **Excellent error handling and validation logic.**

The implementation provides robust error handling for both serialization and deserialization cases, with appropriate validation of JSON input and proper fallback behavior when the serializer is unavailable.




Also applies to: 30-37

</details>
<details>
<summary>live-objects/src/main/kotlin/io/ably/lib/objects/serialization/Serialization.kt (1)</summary>

`19-22`: **Well-implemented deserialization methods with proper error handling.**

The deserialization methods provide good error handling and use appropriate Kotlin idioms for array processing. The JSON deserialization includes proper type validation for individual elements.




Also applies to: 30-35

</details>
<details>
<summary>live-objects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSerializationTest.kt (2)</summary>

`30-47`: **Well-structured test with good round-trip validation.**

The test properly validates round-trip serialization fidelity using MsgPack format and checks each ObjectMessage instance against its expected value.

---

`68-101`: **Excellent test coverage for null field omission.**

This test comprehensively validates that null fields are properly omitted during serialization, which is important for maintaining clean serialized output and reducing payload size.

</details>
<details>
<summary>live-objects/src/test/kotlin/io/ably/lib/objects/unit/fixtures/ObjectMessageFixture.kt (1)</summary>

`1-174`: **Well-designed test fixtures with comprehensive data type coverage.**

The fixture file provides excellent coverage of different data types and scenarios for testing ObjectMessage serialization. The use of immutable data structures and consistent naming conventions makes it easy to understand and maintain.

</details>
<details>
<summary>live-objects/src/main/kotlin/io/ably/lib/objects/serialization/JsonSerialization.kt (1)</summary>

`71-81`: **Robust JSON parsing with proper error handling.**

The JSON string parsing logic correctly handles both JsonObject and JsonArray cases with appropriate error handling for invalid JSON strings.

</details>
<details>
<summary>live-objects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt (4)</summary>

`330-332`: **Good type safety improvement for extras field.**

Changing the `extras` field type from `Any?` to `JsonObject?` provides better type safety and aligns with the JSON-encodable object specification mentioned in the documentation.

---

`45-47`: **Appropriate use of multiple serialization frameworks.**

The use of both Gson and Jackson annotations allows the same data classes to work with both serialization frameworks, which is a good design for flexibility.

---

`239-240`: **Well-documented deprecation.**

The deprecation of `initialValueEncoding` is properly documented with a clear explanation of the future direction.

---

`348-350`: **Correct property mapping for objectState field.**

The use of both `@SerializedName("object")` and `@JsonProperty("object")` properly maps the `objectState` field to the JSON property name "object" for both Gson and Jackson serialization frameworks.

</details>
<details>
<summary>live-objects/src/main/kotlin/io/ably/lib/objects/serialization/MsgpackSerialization.kt (3)</summary>

`24-31`: **Well-configured MessagePack mapper with appropriate settings.**

The mapper configuration is well-thought-out with NON_NULL inclusion, enum ordinal serialization, and parameter names module for proper constructor-based deserialization. The singleton pattern ensures efficient reuse.

---

`38-44`: **Excellent resource management and conversion logic.**

The use of Kotlin's `use` function ensures proper resource cleanup of the buffer packer, and the two-step conversion process correctly handles MessagePack value to object mapping.

---

`93-103`: **Clean binary serialization implementation.**

The Binary serializer and deserializer correctly handle raw binary data with minimal overhead.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines +24 to +28
override fun writeMsgpackArray(objects: Array<out Any>?, packer: MessagePacker) {
val objectMessages: Array<ObjectMessage> = objects as Array<ObjectMessage>
packer.packArrayHeader(objectMessages.size)
objectMessages.forEach { it.writeTo(packer) }
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add type validation for unsafe casting operations.

The unsafe casting from Array<out Any>? to Array<ObjectMessage> could cause ClassCastException at runtime if incorrect types are passed.

Add proper type validation:

  override fun writeMsgpackArray(objects: Array<out Any>?, packer: MessagePacker) {
-    val objectMessages: Array<ObjectMessage> = objects as Array<ObjectMessage>
+    val objectMessages: Array<ObjectMessage> = objects?.let { array ->
+      if (array.all { it is ObjectMessage }) {
+        @Suppress("UNCHECKED_CAST")
+        array as Array<ObjectMessage>
+      } else {
+        throw IllegalArgumentException("All elements must be ObjectMessage instances")
+      }
+    } ?: emptyArray()
    packer.packArrayHeader(objectMessages.size)
    objectMessages.forEach { it.writeTo(packer) }
  }
📝 Committable suggestion

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

Suggested change
override fun writeMsgpackArray(objects: Array<out Any>?, packer: MessagePacker) {
val objectMessages: Array<ObjectMessage> = objects as Array<ObjectMessage>
packer.packArrayHeader(objectMessages.size)
objectMessages.forEach { it.writeTo(packer) }
}
override fun writeMsgpackArray(objects: Array<out Any>?, packer: MessagePacker) {
- val objectMessages: Array<ObjectMessage> = objects as Array<ObjectMessage>
+ val objectMessages: Array<ObjectMessage> = objects?.let { array ->
+ if (array.all { it is ObjectMessage }) {
+ @Suppress("UNCHECKED_CAST")
+ array as Array<ObjectMessage>
+ } else {
+ throw IllegalArgumentException("All elements must be ObjectMessage instances")
+ }
+ } ?: emptyArray()
packer.packArrayHeader(objectMessages.size)
objectMessages.forEach { it.writeTo(packer) }
}
🤖 Prompt for AI Agents
In
live-objects/src/main/kotlin/io/ably/lib/objects/serialization/Serialization.kt
around lines 24 to 28, the code unsafely casts an Array of Any to Array of
ObjectMessage, risking a ClassCastException. Add a type check to verify each
element is an ObjectMessage before casting or processing. If any element is not
an ObjectMessage, handle the error appropriately, such as throwing an
informative exception or skipping invalid elements, to ensure safe operation.

Comment on lines +37 to +44
override fun asJsonArray(objects: Array<out Any>?): JsonArray {
val objectMessages: Array<ObjectMessage> = objects as Array<ObjectMessage>
val jsonArray = JsonArray()
for (objectMessage in objectMessages) {
jsonArray.add(objectMessage.toJsonObject())
}
return jsonArray
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Apply similar type safety improvements to JSON serialization.

The same type safety concern applies to the asJsonArray method.

  override fun asJsonArray(objects: Array<out Any>?): JsonArray {
-    val objectMessages: Array<ObjectMessage> = objects as Array<ObjectMessage>
+    val objectMessages: Array<ObjectMessage> = objects?.let { array ->
+      if (array.all { it is ObjectMessage }) {
+        @Suppress("UNCHECKED_CAST")
+        array as Array<ObjectMessage>
+      } else {
+        throw IllegalArgumentException("All elements must be ObjectMessage instances")
+      }
+    } ?: emptyArray()
    val jsonArray = JsonArray()
    for (objectMessage in objectMessages) {
      jsonArray.add(objectMessage.toJsonObject())
    }
    return jsonArray
  }
📝 Committable suggestion

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

Suggested change
override fun asJsonArray(objects: Array<out Any>?): JsonArray {
val objectMessages: Array<ObjectMessage> = objects as Array<ObjectMessage>
val jsonArray = JsonArray()
for (objectMessage in objectMessages) {
jsonArray.add(objectMessage.toJsonObject())
}
return jsonArray
}
override fun asJsonArray(objects: Array<out Any>?): JsonArray {
val objectMessages: Array<ObjectMessage> = objects?.let { array ->
if (array.all { it is ObjectMessage }) {
@Suppress("UNCHECKED_CAST")
array as Array<ObjectMessage>
} else {
throw IllegalArgumentException("All elements must be ObjectMessage instances")
}
} ?: emptyArray()
val jsonArray = JsonArray()
for (objectMessage in objectMessages) {
jsonArray.add(objectMessage.toJsonObject())
}
return jsonArray
}
🤖 Prompt for AI Agents
In
live-objects/src/main/kotlin/io/ably/lib/objects/serialization/Serialization.kt
around lines 37 to 44, the asJsonArray method unsafely casts the input array to
Array<ObjectMessage> without checking its type, risking a ClassCastException.
Modify the method to safely handle the input by verifying the type of each
element before casting or by using a safer approach such as filtering or mapping
only valid ObjectMessage instances to JSON, ensuring type safety during JSON
serialization.

Comment on lines +27 to +39
public static LiveObjectSerializer getLiveObjectSerializer() {
if (liveObjectSerializer == null) {
try {
Class<?> serializerClass = Class.forName("io.ably.lib.objects.serialization.DefaultLiveObjectSerializer");
liveObjectSerializer = (LiveObjectSerializer) serializerClass.getDeclaredConstructor().newInstance();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException |
InvocationTargetException e) {
Log.e(TAG, "Failed to init LiveObjectSerializer, LiveObjects plugin not included in the classpath", e);
return null;
}
}
return liveObjectSerializer;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix thread safety issue in singleton pattern.

The singleton implementation for liveObjectSerializer is not thread-safe. Multiple threads could simultaneously check the null condition and create multiple instances, violating the singleton pattern.

 public static LiveObjectSerializer getLiveObjectSerializer() {
-    if (liveObjectSerializer == null) {
+    if (liveObjectSerializer == null) {
+        synchronized (LiveObjectsHelper.class) {
+            if (liveObjectSerializer == null) {
                 try {
                     Class<?> serializerClass = Class.forName("io.ably.lib.objects.serialization.DefaultLiveObjectSerializer");
                     liveObjectSerializer = (LiveObjectSerializer) serializerClass.getDeclaredConstructor().newInstance();
                 } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException |
                          InvocationTargetException e) {
                     Log.e(TAG, "Failed to init LiveObjectSerializer, LiveObjects plugin not included in the classpath", e);
                     return null;
                 }
+            }
+        }
     }
     return liveObjectSerializer;
 }
📝 Committable suggestion

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

Suggested change
public static LiveObjectSerializer getLiveObjectSerializer() {
if (liveObjectSerializer == null) {
try {
Class<?> serializerClass = Class.forName("io.ably.lib.objects.serialization.DefaultLiveObjectSerializer");
liveObjectSerializer = (LiveObjectSerializer) serializerClass.getDeclaredConstructor().newInstance();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException |
InvocationTargetException e) {
Log.e(TAG, "Failed to init LiveObjectSerializer, LiveObjects plugin not included in the classpath", e);
return null;
}
}
return liveObjectSerializer;
}
public static LiveObjectSerializer getLiveObjectSerializer() {
if (liveObjectSerializer == null) {
synchronized (LiveObjectsHelper.class) {
if (liveObjectSerializer == null) {
try {
Class<?> serializerClass = Class.forName("io.ably.lib.objects.serialization.DefaultLiveObjectSerializer");
liveObjectSerializer = (LiveObjectSerializer) serializerClass.getDeclaredConstructor().newInstance();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException |
InvocationTargetException e) {
Log.e(TAG, "Failed to init LiveObjectSerializer, LiveObjects plugin not included in the classpath", e);
return null;
}
}
}
}
return liveObjectSerializer;
}
🤖 Prompt for AI Agents
In lib/src/main/java/io/ably/lib/objects/LiveObjectsHelper.java around lines 27
to 39, the singleton pattern for liveObjectSerializer is not thread-safe,
allowing multiple threads to create separate instances. To fix this, synchronize
the block that checks and initializes liveObjectSerializer or use a thread-safe
lazy initialization approach such as double-checked locking to ensure only one
instance is created even under concurrent access.

}
obj.has("string") -> ObjectValue(obj.get("string").asString)
obj.has("number") -> ObjectValue(obj.get("number").asDouble)
obj.has("bytes") -> ObjectValue(Binary(Base64.getDecoder().decode(obj.get("bytes").asString)))
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for Base64 decoding.

Base64 decoding operations should handle potential IllegalArgumentException that can be thrown for invalid Base64 input.

 obj.has("bytes") -> ObjectValue(Binary(Base64.getDecoder().decode(obj.get("bytes").asString)))

Consider wrapping in try-catch:

 obj.has("bytes") -> {
+    try {
         ObjectValue(Binary(Base64.getDecoder().decode(obj.get("bytes").asString)))
+    } catch (e: IllegalArgumentException) {
+        throw JsonParseException("Invalid base64 encoding in bytes field", e)
+    }
 }

Apply similar handling to the InitialValueJsonSerializer.deserialize method at line 97.

Also applies to: 97-97

🤖 Prompt for AI Agents
In
live-objects/src/main/kotlin/io/ably/lib/objects/serialization/JsonSerialization.kt
at lines 84 and 97, the Base64 decoding calls do not handle potential
IllegalArgumentException from invalid Base64 input. Wrap the
Base64.getDecoder().decode calls in try-catch blocks to catch
IllegalArgumentException and handle it appropriately, such as logging the error
or throwing a custom exception, to prevent crashes and improve robustness.

Comment on lines +37 to +40
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): T {
val code = json.asInt
return enumValues.first { getCode(it) == code }
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for enum deserialization.

The enum deserialization could throw NoSuchElementException if no matching enum value is found for the given code, which would result in an unhelpful error message.

 override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): T {
     val code = json.asInt
-    return enumValues.first { getCode(it) == code }
+    return enumValues.firstOrNull { getCode(it) == code }
+        ?: throw JsonParseException("Unknown enum code: $code for type $typeOfT")
 }
📝 Committable suggestion

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

Suggested change
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): T {
val code = json.asInt
return enumValues.first { getCode(it) == code }
}
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): T {
val code = json.asInt
return enumValues.firstOrNull { getCode(it) == code }
?: throw JsonParseException("Unknown enum code: $code for type $typeOfT")
}
🤖 Prompt for AI Agents
In
live-objects/src/main/kotlin/io/ably/lib/objects/serialization/JsonSerialization.kt
around lines 37 to 40, the deserialize method can throw a NoSuchElementException
if no enum matches the given code. To fix this, add error handling by catching
this exception and throwing a more descriptive exception or returning a default
value. This will provide clearer error messages when deserialization fails due
to an unknown code.

node.has("string") -> ObjectValue(node.get("string").asText())
node.has("number") -> ObjectValue(node.get("number").doubleValue())
node.has("bytes") -> ObjectValue(Binary(node.get("bytes").binaryValue()))
else -> throw IllegalArgumentException("ObjectData must have one of the fields: boolean, string, number, or bytes")
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Incomplete error message for deserialization failure.

The error message doesn't mention the JSON encoding scenario, which could confuse users when debugging.

-      else -> throw IllegalArgumentException("ObjectData must have one of the fields: boolean, string, number, or bytes")
+      else -> throw IllegalArgumentException("ObjectData must have one of the fields: boolean, string (with optional encoding), number, or bytes")
📝 Committable suggestion

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

Suggested change
else -> throw IllegalArgumentException("ObjectData must have one of the fields: boolean, string, number, or bytes")
else -> throw IllegalArgumentException("ObjectData must have one of the fields: boolean, string (with optional encoding), number, or bytes")
🤖 Prompt for AI Agents
In
live-objects/src/main/kotlin/io/ably/lib/objects/serialization/MsgpackSerialization.kt
at line 87, the IllegalArgumentException message for deserialization failure is
incomplete and does not mention the JSON encoding scenario. Update the error
message to explicitly include that the ObjectData must have one of the fields:
boolean, string, number, bytes, or JSON encoding, so users can better understand
the cause during debugging.

Comment on lines +75 to +82
val parsed = JsonParser.parseString(jsonStr)
ObjectValue(
when {
parsed.isJsonObject -> parsed.asJsonObject
parsed.isJsonArray -> parsed.asJsonArray
else -> throw IllegalArgumentException("Invalid JSON string for encoding=json")
}
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle all valid JSON types when encoding="json".

The current implementation only accepts JSON objects and arrays, but valid JSON can also be primitives (string, number, boolean, null). Consider supporting all JSON value types or document why only objects/arrays are supported.

If only objects/arrays are intended:

-            else -> throw IllegalArgumentException("Invalid JSON string for encoding=json")
+            else -> throw IllegalArgumentException("JSON encoding only supports objects and arrays, not primitive values")

If all JSON types should be supported, additional handling would be needed for primitive JSON values.

🤖 Prompt for AI Agents
In
live-objects/src/main/kotlin/io/ably/lib/objects/serialization/MsgpackSerialization.kt
around lines 75 to 82, the code currently only handles JSON objects and arrays
but ignores valid JSON primitives like strings, numbers, booleans, and null. To
fix this, extend the when clause to detect and properly handle these primitive
JSON types by converting them into appropriate ObjectValue representations or
document clearly why only objects and arrays are supported if that is the
intended behavior.

Comment on lines +111 to +115
internal class JsonObjectMsgpackDeserializer : com.fasterxml.jackson.databind.JsonDeserializer<JsonObject>() {
override fun deserialize(p: com.fasterxml.jackson.core.JsonParser, ctxt: DeserializationContext): JsonObject {
return Serialisation.msgpackToGson(p.binaryValue) as JsonObject
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Unsafe cast to JsonObject without type validation.

The deserializer assumes msgpackToGson returns a JsonObject, but it could return other JSON types (JsonArray, JsonPrimitive, etc.), leading to a ClassCastException.

Add type validation before casting:

 internal class JsonObjectMsgpackDeserializer : com.fasterxml.jackson.databind.JsonDeserializer<JsonObject>() {
   override fun deserialize(p: com.fasterxml.jackson.core.JsonParser, ctxt: DeserializationContext): JsonObject {
-    return Serialisation.msgpackToGson(p.binaryValue) as JsonObject
+    val element = Serialisation.msgpackToGson(p.binaryValue)
+    if (element is JsonObject) {
+      return element
+    } else {
+      throw IllegalArgumentException("Expected JsonObject but got ${element?.javaClass?.simpleName}")
+    }
   }
 }
📝 Committable suggestion

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

Suggested change
internal class JsonObjectMsgpackDeserializer : com.fasterxml.jackson.databind.JsonDeserializer<JsonObject>() {
override fun deserialize(p: com.fasterxml.jackson.core.JsonParser, ctxt: DeserializationContext): JsonObject {
return Serialisation.msgpackToGson(p.binaryValue) as JsonObject
}
}
internal class JsonObjectMsgpackDeserializer : com.fasterxml.jackson.databind.JsonDeserializer<JsonObject>() {
override fun deserialize(p: com.fasterxml.jackson.core.JsonParser, ctxt: DeserializationContext): JsonObject {
val element = Serialisation.msgpackToGson(p.binaryValue)
if (element is JsonObject) {
return element
} else {
throw IllegalArgumentException("Expected JsonObject but got ${element.javaClass.simpleName}")
}
}
}
🤖 Prompt for AI Agents
In
live-objects/src/main/kotlin/io/ably/lib/objects/serialization/MsgpackSerialization.kt
around lines 111 to 115, the code unsafely casts the result of msgpackToGson to
JsonObject without verifying its actual type, risking a ClassCastException.
Modify the deserialize method to first check if the returned value is an
instance of JsonObject before casting. If it is not, handle the case
appropriately, such as throwing a descriptive exception or returning a default
value, to ensure type safety.

Comment on lines +46 to +64
internal class ObjectDataMsgpackSerializer : com.fasterxml.jackson.databind.JsonSerializer<ObjectData>() {
override fun serialize(value: ObjectData, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeStartObject()
value.objectId?.let { gen.writeStringField("objectId", it) }
value.value?.let { v ->
when (val data = v.value) {
is Boolean -> gen.writeBooleanField("boolean", data)
is String -> gen.writeStringField("string", data)
is Number -> gen.writeNumberField("number", data.toDouble())
is Binary -> gen.writeBinaryField("bytes", data.data)
is JsonObject, is JsonArray -> {
gen.writeStringField("string", data.toString())
gen.writeStringField("encoding", "json")
}
}
}
gen.writeEndObject()
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential precision loss when converting Number to Double.

The serializer converts all Number types to Double, which could lose precision for Long values that exceed Double's precision range (values beyond ±2^53).

Consider preserving the numeric type or using a more appropriate representation:

-        is Number -> gen.writeNumberField("number", data.toDouble())
+        is Number -> gen.writeNumberField("number", data)

Note: If the protocol specifically requires doubles, document this limitation.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
live-objects/src/main/kotlin/io/ably/lib/objects/serialization/MsgpackSerialization.kt
between lines 46 and 64, the serialization converts all Number types to Double,
risking precision loss for Long values exceeding Double's precision range. To
fix this, modify the serialization logic to detect the specific Number subtype
and serialize it accordingly, preserving the original numeric type (e.g.,
writeLongField for Long, writeIntField for Int) instead of always converting to
Double. If the protocol mandates Double, add a comment documenting this
limitation clearly.

@sacOO7 sacOO7 requested a review from ttypic June 23, 2025 12:49
@sacOO7
Copy link
Collaborator Author

sacOO7 commented Jun 24, 2025

@sacOO7 sacOO7 closed this Jun 24, 2025
@sacOO7 sacOO7 changed the title [ECO-5386] Liveobjects serialization [ECO-5386] Liveobjects serialization using msgpack-jackson Jun 24, 2025
@sacOO7
Copy link
Collaborator Author

sacOO7 commented Jun 24, 2025

Related issue was #1098

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants