From 34c3bbeeffbc01d2a2090af89ea34a5de8eda54c Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Thu, 18 Jun 2026 23:33:37 +0530 Subject: [PATCH 1/6] Implemented Json and MsgPack serializers for path based liveobjects --- .../serialization/DefaultSerialization.kt | 44 + .../object/serialization/JsonSerialization.kt | 68 ++ .../serialization/MsgpackSerialization.kt | 908 ++++++++++++++++++ 3 files changed, 1020 insertions(+) create mode 100644 liveobjects/src/main/kotlin/io/ably/lib/object/serialization/DefaultSerialization.kt create mode 100644 liveobjects/src/main/kotlin/io/ably/lib/object/serialization/JsonSerialization.kt create mode 100644 liveobjects/src/main/kotlin/io/ably/lib/object/serialization/MsgpackSerialization.kt diff --git a/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/DefaultSerialization.kt b/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/DefaultSerialization.kt new file mode 100644 index 000000000..e8db5c956 --- /dev/null +++ b/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/DefaultSerialization.kt @@ -0,0 +1,44 @@ +package io.ably.lib.`object`.serialization + +import com.google.gson.* +import io.ably.lib.objects.* + +import io.ably.lib.objects.ObjectMessage +import org.msgpack.core.MessagePacker +import org.msgpack.core.MessageUnpacker + +/** + * Default implementation of {@link ObjectsSerializer} that handles serialization/deserialization + * of ObjectMessage arrays for both JSON and MessagePack formats using Jackson and Gson. + * Dynamically loaded by ObjectsHelper#getSerializer() to avoid hard dependencies. + */ +@Suppress("unused") // Used via reflection in ObjectsHelper +internal class DefaultObjectsSerializer : ObjectsSerializer { + + override fun readMsgpackArray(unpacker: MessageUnpacker): Array { + val objectMessagesCount = unpacker.unpackArrayHeader() + return Array(objectMessagesCount) { readObjectMessage(unpacker) } + } + + override fun writeMsgpackArray(objects: Array, packer: MessagePacker) { + val objectMessages = objects.map { it as ObjectMessage } + packer.packArrayHeader(objectMessages.size) + objectMessages.forEach { it.writeMsgpack(packer) } + } + + override fun readFromJsonArray(json: JsonArray): Array { + return json.map { element -> + if (element.isJsonObject) element.asJsonObject.toObjectMessage() + else throw JsonParseException("Expected JsonObject, but found: $element") + }.toTypedArray() + } + + override fun asJsonArray(objects: Array): JsonArray { + val objectMessages = objects.map { it as ObjectMessage } + val jsonArray = JsonArray() + for (objectMessage in objectMessages) { + jsonArray.add(objectMessage.toJsonObject()) + } + return jsonArray + } +} diff --git a/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/JsonSerialization.kt b/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/JsonSerialization.kt new file mode 100644 index 000000000..07ef60cb7 --- /dev/null +++ b/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/JsonSerialization.kt @@ -0,0 +1,68 @@ +package io.ably.lib.`object`.serialization + +import com.google.gson.* +import io.ably.lib.objects.ObjectsMapSemantics +import io.ably.lib.objects.ObjectData +import io.ably.lib.objects.ObjectMessage +import io.ably.lib.objects.ObjectOperationAction +import io.ably.lib.objects.serialization.EnumCodeTypeAdapter +import java.lang.reflect.Type +import kotlin.enums.EnumEntries + +// Gson instance for JSON serialization/deserialization +internal val gson = GsonBuilder() + .registerTypeAdapter(ObjectOperationAction::class.java, EnumCodeTypeAdapter({ it.code }, ObjectOperationAction.entries)) + .registerTypeAdapter(ObjectsMapSemantics::class.java, EnumCodeTypeAdapter({ it.code }, ObjectsMapSemantics.entries)) + .create() + +internal fun ObjectMessage.toJsonObject(): JsonObject { + return gson.toJsonTree(this).asJsonObject +} + +internal fun JsonObject.toObjectMessage(): ObjectMessage { + return gson.fromJson(this, ObjectMessage::class.java) +} + +internal class EnumCodeTypeAdapter>( + private val getCode: (T) -> Int, + private val enumValues: EnumEntries +) : JsonSerializer, JsonDeserializer { + + override fun serialize(src: T, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { + return JsonPrimitive(getCode(src)) + } + + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): T { + val code = json.asInt + return enumValues.firstOrNull { getCode(it) == code } ?: enumValues.firstOrNull { getCode(it) == -1 } + ?: throw JsonParseException("Unknown enum code: $code and no Unknown fallback found") + } +} + +internal class ObjectDataJsonSerializer : JsonSerializer, JsonDeserializer { + override fun serialize(src: ObjectData, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { + val obj = JsonObject() + src.objectId?.let { obj.addProperty("objectId", it) } + src.string?.let { obj.addProperty("string", it) } + src.number?.let { obj.addProperty("number", it) } + src.boolean?.let { obj.addProperty("boolean", it) } + src.bytes?.let { obj.addProperty("bytes", it) } + src.json?.let { obj.addProperty("json", it.toString()) } // Spec: OD4c5 + return obj + } + + override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): ObjectData { + val obj = if (json.isJsonObject) json.asJsonObject else throw JsonParseException("Expected JsonObject") + val objectId = if (obj.has("objectId")) obj.get("objectId").asString else null + val string = if (obj.has("string")) obj.get("string").asString else null + val number = if (obj.has("number")) obj.get("number").asDouble else null + val boolean = if (obj.has("boolean")) obj.get("boolean").asBoolean else null + val bytes = if (obj.has("bytes")) obj.get("bytes").asString else null + val json = if (obj.has("json")) JsonParser.parseString(obj.get("json").asString) else null + + if (objectId == null && string == null && number == null && boolean == null && bytes == null && json == null) { + throw JsonParseException("Since objectId is not present, at least one of the value fields must be present") + } + return ObjectData(objectId = objectId, string = string, number = number, boolean = boolean, bytes = bytes, json = json) + } +} diff --git a/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/MsgpackSerialization.kt b/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/MsgpackSerialization.kt new file mode 100644 index 000000000..52e3ef533 --- /dev/null +++ b/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/MsgpackSerialization.kt @@ -0,0 +1,908 @@ +package io.ably.lib.`object`.serialization + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import io.ably.lib.objects.* +import io.ably.lib.objects.CounterCreate +import io.ably.lib.objects.CounterCreateWithObjectId +import io.ably.lib.objects.CounterInc +import io.ably.lib.objects.MapCreate +import io.ably.lib.objects.MapCreateWithObjectId +import io.ably.lib.objects.MapRemove +import io.ably.lib.objects.MapSet +import io.ably.lib.objects.MapClear +import io.ably.lib.objects.ObjectDelete +import io.ably.lib.objects.ObjectsMapSemantics +import io.ably.lib.objects.ObjectsCounter +import io.ably.lib.objects.ObjectData +import io.ably.lib.objects.ObjectsMap +import io.ably.lib.objects.ObjectsMapEntry +import io.ably.lib.objects.ObjectMessage +import io.ably.lib.objects.ObjectOperation +import io.ably.lib.objects.ObjectOperationAction +import io.ably.lib.objects.ObjectState +import java.util.Base64 +import io.ably.lib.util.Serialisation +import org.msgpack.core.MessageFormat +import org.msgpack.core.MessagePacker +import org.msgpack.core.MessageUnpacker + +/** + * Write ObjectMessage to MessagePacker + */ +internal fun ObjectMessage.writeMsgpack(packer: MessagePacker) { + var fieldCount = 0 + + if (id != null) fieldCount++ + if (timestamp != null) fieldCount++ + if (clientId != null) fieldCount++ + if (connectionId != null) fieldCount++ + if (extras != null) fieldCount++ + if (operation != null) fieldCount++ + if (objectState != null) fieldCount++ + if (serial != null) fieldCount++ + if (serialTimestamp != null) fieldCount++ + if (siteCode != null) fieldCount++ + + packer.packMapHeader(fieldCount) + + if (id != null) { + packer.packString("id") + packer.packString(id) + } + + if (timestamp != null) { + packer.packString("timestamp") + packer.packLong(timestamp) + } + + if (clientId != null) { + packer.packString("clientId") + packer.packString(clientId) + } + + if (connectionId != null) { + packer.packString("connectionId") + packer.packString(connectionId) + } + + if (extras != null) { + packer.packString("extras") + packer.writePayload(Serialisation.gsonToMsgpack(extras)) + } + + if (operation != null) { + packer.packString("operation") + operation.writeMsgpack(packer) + } + + if (objectState != null) { + packer.packString("object") + objectState.writeMsgpack(packer) + } + + if (serial != null) { + packer.packString("serial") + packer.packString(serial) + } + + if (serialTimestamp != null) { + packer.packString("serialTimestamp") + packer.packLong(serialTimestamp) + } + + if (siteCode != null) { + packer.packString("siteCode") + packer.packString(siteCode) + } +} + +/** + * Read an ObjectMessage from MessageUnpacker + */ +internal fun readObjectMessage(unpacker: MessageUnpacker): ObjectMessage { + if (unpacker.nextFormat == MessageFormat.NIL) { + unpacker.unpackNil() + return ObjectMessage() // default/empty message + } + + val fieldCount = unpacker.unpackMapHeader() + + var id: String? = null + var timestamp: Long? = null + var clientId: String? = null + var connectionId: String? = null + var extras: JsonObject? = null + var operation: ObjectOperation? = null + var objectState: ObjectState? = null + var serial: String? = null + var serialTimestamp: Long? = null + var siteCode: String? = null + + for (i in 0 until fieldCount) { + val fieldName = unpacker.unpackString().intern() + val fieldFormat = unpacker.nextFormat + + if (fieldFormat == MessageFormat.NIL) { + unpacker.unpackNil() + continue + } + + when (fieldName) { + "id" -> id = unpacker.unpackString() + "timestamp" -> timestamp = unpacker.unpackLong() + "clientId" -> clientId = unpacker.unpackString() + "connectionId" -> connectionId = unpacker.unpackString() + "extras" -> extras = Serialisation.msgpackToGson(unpacker.unpackValue()) as? JsonObject + "operation" -> operation = readObjectOperation(unpacker) + "object" -> objectState = readObjectState(unpacker) + "serial" -> serial = unpacker.unpackString() + "serialTimestamp" -> serialTimestamp = unpacker.unpackLong() + "siteCode" -> siteCode = unpacker.unpackString() + else -> unpacker.skipValue() + } + } + + return ObjectMessage( + id = id, + timestamp = timestamp, + clientId = clientId, + connectionId = connectionId, + extras = extras, + operation = operation, + objectState = objectState, + serial = serial, + serialTimestamp = serialTimestamp, + siteCode = siteCode + ) +} + +/** + * Write ObjectOperation to MessagePacker + */ +private fun ObjectOperation.writeMsgpack(packer: MessagePacker) { + var fieldCount = 1 // action is always required + require(objectId.isNotEmpty()) { "objectId must be non-empty per Objects protocol" } + fieldCount++ + + 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++ + if (mapClear != null) fieldCount++ + + packer.packMapHeader(fieldCount) + + packer.packString("action") + packer.packInt(action.code) + + // Always include objectId as per Objects protocol + packer.packString("objectId") + packer.packString(objectId) + + if (mapCreate != null) { + packer.packString("mapCreate") + mapCreate.writeMsgpack(packer) + } + + if (mapSet != null) { + packer.packString("mapSet") + mapSet.writeMsgpack(packer) + } + + if (mapRemove != null) { + packer.packString("mapRemove") + mapRemove.writeMsgpack(packer) + } + + if (counterCreate != null) { + packer.packString("counterCreate") + counterCreate.writeMsgpack(packer) + } + + if (counterInc != null) { + packer.packString("counterInc") + counterInc.writeMsgpack(packer) + } + + if (objectDelete != null) { + packer.packString("objectDelete") + packer.packMapHeader(0) // empty map + } + + if (mapCreateWithObjectId != null) { + packer.packString("mapCreateWithObjectId") + mapCreateWithObjectId.writeMsgpack(packer) + } + + if (counterCreateWithObjectId != null) { + packer.packString("counterCreateWithObjectId") + counterCreateWithObjectId.writeMsgpack(packer) + } + + if (mapClear != null) { + packer.packString("mapClear") + packer.packMapHeader(0) // empty map, no fields + } + +} + +/** + * Read ObjectOperation from MessageUnpacker + */ +private fun readObjectOperation(unpacker: MessageUnpacker): ObjectOperation { + val fieldCount = unpacker.unpackMapHeader() + + var action: ObjectOperationAction? = null + var objectId: String = "" + var mapCreate: MapCreate? = null + var mapSet: MapSet? = null + var mapRemove: MapRemove? = null + var counterCreate: CounterCreate? = null + var counterInc: CounterInc? = null + var objectDelete: ObjectDelete? = null + var mapCreateWithObjectId: MapCreateWithObjectId? = null + var counterCreateWithObjectId: CounterCreateWithObjectId? = null + var mapClear: MapClear? = null + + for (i in 0 until fieldCount) { + val fieldName = unpacker.unpackString().intern() + val fieldFormat = unpacker.nextFormat + + if (fieldFormat == MessageFormat.NIL) { + unpacker.unpackNil() + continue + } + + when (fieldName) { + "action" -> { + val actionCode = unpacker.unpackInt() + action = ObjectOperationAction.entries.firstOrNull { it.code == actionCode } + ?: ObjectOperationAction.entries.firstOrNull { it.code == -1 } + ?: throw objectError("Unknown ObjectOperationAction code: $actionCode and no Unknown fallback found") + } + "objectId" -> objectId = unpacker.unpackString() + "mapCreate" -> mapCreate = readMapCreate(unpacker) + "mapSet" -> mapSet = readMapSet(unpacker) + "mapRemove" -> mapRemove = readMapRemove(unpacker) + "counterCreate" -> counterCreate = readCounterCreate(unpacker) + "counterInc" -> counterInc = readCounterInc(unpacker) + "objectDelete" -> { + unpacker.skipValue() // empty map, just consume it + objectDelete = ObjectDelete + } + "mapCreateWithObjectId" -> mapCreateWithObjectId = readMapCreateWithObjectId(unpacker) + "counterCreateWithObjectId" -> counterCreateWithObjectId = readCounterCreateWithObjectId(unpacker) + "mapClear" -> { + unpacker.skipValue() // empty map, consume it + mapClear = MapClear + } + else -> unpacker.skipValue() + } + } + + if (action == null) { + throw objectError("Missing required 'action' field in ObjectOperation") + } + + return ObjectOperation( + action = action, + objectId = objectId, + mapCreate = mapCreate, + mapSet = mapSet, + mapRemove = mapRemove, + counterCreate = counterCreate, + counterInc = counterInc, + objectDelete = objectDelete, + mapCreateWithObjectId = mapCreateWithObjectId, + counterCreateWithObjectId = counterCreateWithObjectId, + mapClear = mapClear, + ) +} + +/** + * Write ObjectState to MessagePacker + */ +private fun ObjectState.writeMsgpack(packer: MessagePacker) { + var fieldCount = 3 // objectId, siteTimeserials, and tombstone are required + + if (createOp != null) fieldCount++ + if (map != null) fieldCount++ + if (counter != null) fieldCount++ + + packer.packMapHeader(fieldCount) + + packer.packString("objectId") + packer.packString(objectId) + + packer.packString("siteTimeserials") + packer.packMapHeader(siteTimeserials.size) + for ((key, value) in siteTimeserials) { + packer.packString(key) + packer.packString(value) + } + + packer.packString("tombstone") + packer.packBoolean(tombstone) + + if (createOp != null) { + packer.packString("createOp") + createOp.writeMsgpack(packer) + } + + if (map != null) { + packer.packString("map") + map.writeMsgpack(packer) + } + + if (counter != null) { + packer.packString("counter") + counter.writeMsgpack(packer) + } +} + +/** + * Read ObjectState from MessageUnpacker + */ +private fun readObjectState(unpacker: MessageUnpacker): ObjectState { + val fieldCount = unpacker.unpackMapHeader() + + var objectId = "" + var siteTimeserials = mapOf() + var tombstone = false + var createOp: ObjectOperation? = null + var map: ObjectsMap? = null + var counter: ObjectsCounter? = null + + for (i in 0 until fieldCount) { + val fieldName = unpacker.unpackString().intern() + val fieldFormat = unpacker.nextFormat + + if (fieldFormat == MessageFormat.NIL) { + unpacker.unpackNil() + continue + } + + when (fieldName) { + "objectId" -> objectId = unpacker.unpackString() + "siteTimeserials" -> { + val mapSize = unpacker.unpackMapHeader() + val tempMap = mutableMapOf() + for (j in 0 until mapSize) { + val key = unpacker.unpackString() + val value = unpacker.unpackString() + tempMap[key] = value + } + siteTimeserials = tempMap + } + "tombstone" -> tombstone = unpacker.unpackBoolean() + "createOp" -> createOp = readObjectOperation(unpacker) + "map" -> map = readObjectMap(unpacker) + "counter" -> counter = readObjectCounter(unpacker) + else -> unpacker.skipValue() + } + } + + return ObjectState( + objectId = objectId, + siteTimeserials = siteTimeserials, + tombstone = tombstone, + createOp = createOp, + map = map, + counter = counter + ) +} + +/** + * Write MapCreate to MessagePacker + */ +private fun MapCreate.writeMsgpack(packer: MessagePacker) { + packer.packMapHeader(2) + packer.packString("semantics") + packer.packInt(semantics.code) + packer.packString("entries") + packer.packMapHeader(entries.size) + for ((key, value) in entries) { + packer.packString(key) + value.writeMsgpack(packer) + } +} + +/** + * Read MapCreate from MessageUnpacker + */ +private fun readMapCreate(unpacker: MessageUnpacker): MapCreate { + val fieldCount = unpacker.unpackMapHeader() + var semantics: ObjectsMapSemantics = ObjectsMapSemantics.LWW + var entries: Map = emptyMap() + + for (i in 0 until fieldCount) { + val fieldName = unpacker.unpackString().intern() + val fieldFormat = unpacker.nextFormat + if (fieldFormat == MessageFormat.NIL) { unpacker.unpackNil(); continue } + when (fieldName) { + "semantics" -> { + val code = unpacker.unpackInt() + semantics = ObjectsMapSemantics.entries.firstOrNull { it.code == code } + ?: ObjectsMapSemantics.entries.firstOrNull { it.code == -1 } + ?: throw objectError("Unknown MapSemantics code: $code and no UNKNOWN fallback found") + } + "entries" -> { + val mapSize = unpacker.unpackMapHeader() + val tempMap = mutableMapOf() + for (j in 0 until mapSize) { + tempMap[unpacker.unpackString()] = readObjectMapEntry(unpacker) + } + entries = tempMap + } + else -> unpacker.skipValue() + } + } + return MapCreate(semantics = semantics, entries = entries) +} + +/** + * Write MapSet to MessagePacker + */ +private fun MapSet.writeMsgpack(packer: MessagePacker) { + packer.packMapHeader(2) + packer.packString("key") + packer.packString(key) + packer.packString("value") + value.writeMsgpack(packer) +} + +/** + * Read MapSet from MessageUnpacker + */ +private fun readMapSet(unpacker: MessageUnpacker): MapSet { + val fieldCount = unpacker.unpackMapHeader() + var key: String? = null + var value: ObjectData? = null + + for (i in 0 until fieldCount) { + val fieldName = unpacker.unpackString().intern() + val fieldFormat = unpacker.nextFormat + if (fieldFormat == MessageFormat.NIL) { unpacker.unpackNil(); continue } + when (fieldName) { + "key" -> key = unpacker.unpackString() + "value" -> value = readObjectData(unpacker) + else -> unpacker.skipValue() + } + } + return MapSet( + key = key ?: throw objectError("Missing 'key' in MapSet payload"), + value = value ?: throw objectError("Missing 'value' in MapSet payload") + ) +} + +/** + * Write MapRemove to MessagePacker + */ +private fun MapRemove.writeMsgpack(packer: MessagePacker) { + packer.packMapHeader(1) + packer.packString("key") + packer.packString(key) +} + +/** + * Read MapRemove from MessageUnpacker + */ +private fun readMapRemove(unpacker: MessageUnpacker): MapRemove { + val fieldCount = unpacker.unpackMapHeader() + var key: String? = null + + for (i in 0 until fieldCount) { + val fieldName = unpacker.unpackString().intern() + val fieldFormat = unpacker.nextFormat + if (fieldFormat == MessageFormat.NIL) { unpacker.unpackNil(); continue } + when (fieldName) { + "key" -> key = unpacker.unpackString() + else -> unpacker.skipValue() + } + } + return MapRemove(key = key ?: throw objectError("Missing 'key' in MapRemove payload")) +} + +/** + * Write CounterCreate to MessagePacker + */ +private fun CounterCreate.writeMsgpack(packer: MessagePacker) { + packer.packMapHeader(1) + packer.packString("count") + packer.packDouble(count) +} + +/** + * Read CounterCreate from MessageUnpacker + */ +private fun readCounterCreate(unpacker: MessageUnpacker): CounterCreate { + val fieldCount = unpacker.unpackMapHeader() + var count: Double? = null + + for (i in 0 until fieldCount) { + val fieldName = unpacker.unpackString().intern() + val fieldFormat = unpacker.nextFormat + if (fieldFormat == MessageFormat.NIL) { unpacker.unpackNil(); continue } + when (fieldName) { + "count" -> count = unpacker.unpackDouble() + else -> unpacker.skipValue() + } + } + return CounterCreate(count = count ?: throw objectError("Missing 'count' in CounterCreate payload")) +} + +/** + * Write CounterInc to MessagePacker + */ +private fun CounterInc.writeMsgpack(packer: MessagePacker) { + packer.packMapHeader(1) + packer.packString("number") + packer.packDouble(number) +} + +/** + * Read CounterInc from MessageUnpacker + */ +private fun readCounterInc(unpacker: MessageUnpacker): CounterInc { + val fieldCount = unpacker.unpackMapHeader() + var number: Double? = null + + for (i in 0 until fieldCount) { + val fieldName = unpacker.unpackString().intern() + val fieldFormat = unpacker.nextFormat + if (fieldFormat == MessageFormat.NIL) { unpacker.unpackNil(); continue } + when (fieldName) { + "number" -> number = unpacker.unpackDouble() + else -> unpacker.skipValue() + } + } + return CounterInc(number = number ?: throw objectError("Missing 'number' in CounterInc payload")) +} + +/** + * Write MapCreateWithObjectId to MessagePacker + */ +private fun MapCreateWithObjectId.writeMsgpack(packer: MessagePacker) { + packer.packMapHeader(2) + packer.packString("initialValue") + packer.packString(initialValue) + packer.packString("nonce") + packer.packString(nonce) +} + +/** + * Read MapCreateWithObjectId from MessageUnpacker + */ +private fun readMapCreateWithObjectId(unpacker: MessageUnpacker): MapCreateWithObjectId { + val fieldCount = unpacker.unpackMapHeader() + var initialValue: String? = null + var nonce: String? = null + + for (i in 0 until fieldCount) { + val fieldName = unpacker.unpackString().intern() + val fieldFormat = unpacker.nextFormat + if (fieldFormat == MessageFormat.NIL) { unpacker.unpackNil(); continue } + when (fieldName) { + "initialValue" -> initialValue = unpacker.unpackString() + "nonce" -> nonce = unpacker.unpackString() + else -> unpacker.skipValue() + } + } + return MapCreateWithObjectId( + initialValue = initialValue ?: throw objectError("Missing 'initialValue' in MapCreateWithObjectId payload"), + nonce = nonce ?: throw objectError("Missing 'nonce' in MapCreateWithObjectId payload") + ) +} + +/** + * Write CounterCreateWithObjectId to MessagePacker + */ +private fun CounterCreateWithObjectId.writeMsgpack(packer: MessagePacker) { + packer.packMapHeader(2) + packer.packString("initialValue") + packer.packString(initialValue) + packer.packString("nonce") + packer.packString(nonce) +} + +/** + * Read CounterCreateWithObjectId from MessageUnpacker + */ +private fun readCounterCreateWithObjectId(unpacker: MessageUnpacker): CounterCreateWithObjectId { + val fieldCount = unpacker.unpackMapHeader() + var initialValue: String? = null + var nonce: String? = null + + for (i in 0 until fieldCount) { + val fieldName = unpacker.unpackString().intern() + val fieldFormat = unpacker.nextFormat + if (fieldFormat == MessageFormat.NIL) { unpacker.unpackNil(); continue } + when (fieldName) { + "initialValue" -> initialValue = unpacker.unpackString() + "nonce" -> nonce = unpacker.unpackString() + else -> unpacker.skipValue() + } + } + return CounterCreateWithObjectId( + initialValue = initialValue ?: throw objectError("Missing 'initialValue' in CounterCreateWithObjectId payload"), + nonce = nonce ?: throw objectError("Missing 'nonce' in CounterCreateWithObjectId payload") + ) +} + +/** + * Write ObjectMap to MessagePacker + */ +private fun ObjectsMap.writeMsgpack(packer: MessagePacker) { + var fieldCount = 0 + + if (semantics != null) fieldCount++ + if (entries != null) fieldCount++ + if (clearTimeserial != null) fieldCount++ + + packer.packMapHeader(fieldCount) + + if (semantics != null) { + packer.packString("semantics") + packer.packInt(semantics.code) + } + + if (entries != null) { + packer.packString("entries") + packer.packMapHeader(entries.size) + for ((key, value) in entries) { + packer.packString(key) + value.writeMsgpack(packer) + } + } + + if (clearTimeserial != null) { + packer.packString("clearTimeserial") + packer.packString(clearTimeserial) + } +} + +/** + * Read ObjectMap from MessageUnpacker + */ +private fun readObjectMap(unpacker: MessageUnpacker): ObjectsMap { + val fieldCount = unpacker.unpackMapHeader() + + var semantics: ObjectsMapSemantics? = null + var entries: Map? = null + var clearTimeserial: String? = null + + for (i in 0 until fieldCount) { + val fieldName = unpacker.unpackString().intern() + val fieldFormat = unpacker.nextFormat + + if (fieldFormat == MessageFormat.NIL) { + unpacker.unpackNil() + continue + } + + when (fieldName) { + "semantics" -> { + val semanticsCode = unpacker.unpackInt() + semantics = ObjectsMapSemantics.entries.firstOrNull { it.code == semanticsCode } + ?: ObjectsMapSemantics.entries.firstOrNull { it.code == -1 } + ?: throw objectError("Unknown MapSemantics code: $semanticsCode and no UNKNOWN fallback found") + } + "entries" -> { + val mapSize = unpacker.unpackMapHeader() + val tempMap = mutableMapOf() + for (j in 0 until mapSize) { + val key = unpacker.unpackString() + val value = readObjectMapEntry(unpacker) + tempMap[key] = value + } + entries = tempMap + } + "clearTimeserial" -> clearTimeserial = unpacker.unpackString() + else -> unpacker.skipValue() + } + } + + return ObjectsMap(semantics = semantics, entries = entries, clearTimeserial = clearTimeserial) +} + +/** + * Write ObjectCounter to MessagePacker + */ +private fun ObjectsCounter.writeMsgpack(packer: MessagePacker) { + var fieldCount = 0 + + if (count != null) fieldCount++ + + packer.packMapHeader(fieldCount) + + if (count != null) { + packer.packString("count") + packer.packDouble(count) + } +} + +/** + * Read ObjectCounter from MessageUnpacker + */ +private fun readObjectCounter(unpacker: MessageUnpacker): ObjectsCounter { + val fieldCount = unpacker.unpackMapHeader() + + var count: Double? = null + + for (i in 0 until fieldCount) { + val fieldName = unpacker.unpackString().intern() + val fieldFormat = unpacker.nextFormat + + if (fieldFormat == MessageFormat.NIL) { + unpacker.unpackNil() + continue + } + + when (fieldName) { + "count" -> count = unpacker.unpackDouble() + else -> unpacker.skipValue() + } + } + + return ObjectsCounter(count = count) +} + +/** + * Write ObjectMapEntry to MessagePacker + */ +private fun ObjectsMapEntry.writeMsgpack(packer: MessagePacker) { + var fieldCount = 0 + + if (tombstone != null) fieldCount++ + if (timeserial != null) fieldCount++ + if (serialTimestamp != null) fieldCount++ + if (data != null) fieldCount++ + + packer.packMapHeader(fieldCount) + + if (tombstone != null) { + packer.packString("tombstone") + packer.packBoolean(tombstone) + } + + if (timeserial != null) { + packer.packString("timeserial") + packer.packString(timeserial) + } + + if (serialTimestamp != null) { + packer.packString("serialTimestamp") + packer.packLong(serialTimestamp) + } + + if (data != null) { + packer.packString("data") + data.writeMsgpack(packer) + } +} + +/** + * Read ObjectMapEntry from MessageUnpacker + */ +private fun readObjectMapEntry(unpacker: MessageUnpacker): ObjectsMapEntry { + val fieldCount = unpacker.unpackMapHeader() + + var tombstone: Boolean? = null + var timeserial: String? = null + var serialTimestamp: Long? = null + var data: ObjectData? = null + + for (i in 0 until fieldCount) { + val fieldName = unpacker.unpackString().intern() + val fieldFormat = unpacker.nextFormat + + if (fieldFormat == MessageFormat.NIL) { + unpacker.unpackNil() + continue + } + + when (fieldName) { + "tombstone" -> tombstone = unpacker.unpackBoolean() + "timeserial" -> timeserial = unpacker.unpackString() + "serialTimestamp" -> serialTimestamp = unpacker.unpackLong() + "data" -> data = readObjectData(unpacker) + else -> unpacker.skipValue() + } + } + + return ObjectsMapEntry(tombstone = tombstone, timeserial = timeserial, serialTimestamp = serialTimestamp, data = data) +} + +/** + * Write ObjectData to MessagePacker + */ +private fun ObjectData.writeMsgpack(packer: MessagePacker) { + var fieldCount = 0 + + if (objectId != null) fieldCount++ + if (string != null) fieldCount++ + if (number != null) fieldCount++ + if (boolean != null) fieldCount++ + if (bytes != null) fieldCount++ + if (json != null) fieldCount++ + + packer.packMapHeader(fieldCount) + + if (objectId != null) { + packer.packString("objectId") + packer.packString(objectId) + } + + if (string != null) { + packer.packString("string") + packer.packString(string) + } + + if (number != null) { + packer.packString("number") + packer.packDouble(number) + } + + if (boolean != null) { + packer.packString("boolean") + packer.packBoolean(boolean) + } + + if (bytes != null) { + val rawBytes = Base64.getDecoder().decode(bytes) + packer.packString("bytes") + packer.packBinaryHeader(rawBytes.size) + packer.writePayload(rawBytes) + } + + if (json != null) { + packer.packString("json") + packer.packString(json.toString()) + } +} + +/** + * Read ObjectData from MessageUnpacker + */ +private fun readObjectData(unpacker: MessageUnpacker): ObjectData { + val fieldCount = unpacker.unpackMapHeader() + var objectId: String? = null + var string: String? = null + var number: Double? = null + var boolean: Boolean? = null + var bytes: String? = null + var json: JsonElement? = null + + for (i in 0 until fieldCount) { + val fieldName = unpacker.unpackString().intern() + val fieldFormat = unpacker.nextFormat + + if (fieldFormat == MessageFormat.NIL) { + unpacker.unpackNil() + continue + } + + when (fieldName) { + "objectId" -> objectId = unpacker.unpackString() + "string" -> string = unpacker.unpackString() + "number" -> number = unpacker.unpackDouble() + "boolean" -> boolean = unpacker.unpackBoolean() + "bytes" -> { + val size = unpacker.unpackBinaryHeader() + val rawBytes = ByteArray(size) + unpacker.readPayload(rawBytes) + bytes = Base64.getEncoder().encodeToString(rawBytes) + } + "json" -> json = JsonParser.parseString(unpacker.unpackString()) + else -> unpacker.skipValue() + } + } + + return ObjectData(objectId = objectId, string = string, number = number, boolean = boolean, bytes = bytes, json = json) +} From 5e452c29f664699338de01fc68475474d570fcec Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 19 Jun 2026 17:42:25 +0530 Subject: [PATCH 2/6] - Declared ObjectSerializer interface for json/msgpack encoding/decoding - Implemented JsonSerializer annotation for better json handling --- .../serialization/ObjectJsonSerializer.java | 33 +++++++ .../serialization/ObjectSerializer.java | 98 +++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 lib/src/main/java/io/ably/lib/object/serialization/ObjectJsonSerializer.java create mode 100644 lib/src/main/java/io/ably/lib/object/serialization/ObjectSerializer.java diff --git a/lib/src/main/java/io/ably/lib/object/serialization/ObjectJsonSerializer.java b/lib/src/main/java/io/ably/lib/object/serialization/ObjectJsonSerializer.java new file mode 100644 index 000000000..08e5d6b71 --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/serialization/ObjectJsonSerializer.java @@ -0,0 +1,33 @@ +package io.ably.lib.object.serialization; + +import com.google.gson.*; +import io.ably.lib.util.Log; + +import java.lang.reflect.Type; + +public class ObjectJsonSerializer implements JsonSerializer, JsonDeserializer { + private static final String TAG = ObjectJsonSerializer.class.getName(); + + @Override + public Object[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + ObjectSerializer serializer = ObjectSerializer.tryGet(); + if (serializer == null) { + Log.w(TAG, "Skipping 'state' field json deserialization because ObjectsSerializer not found."); + return null; + } + if (!json.isJsonArray()) { + throw new JsonParseException("Expected a JSON array for 'state' field, but got: " + json); + } + return serializer.readFromJsonArray(json.getAsJsonArray()); + } + + @Override + public JsonElement serialize(Object[] src, Type typeOfSrc, JsonSerializationContext context) { + ObjectSerializer serializer = ObjectSerializer.tryGet(); + if (serializer == null) { + Log.w(TAG, "Skipping 'state' field json serialization because ObjectsSerializer not found."); + return JsonNull.INSTANCE; + } + return serializer.asJsonArray(src); + } +} diff --git a/lib/src/main/java/io/ably/lib/object/serialization/ObjectSerializer.java b/lib/src/main/java/io/ably/lib/object/serialization/ObjectSerializer.java new file mode 100644 index 000000000..67b21b77c --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/serialization/ObjectSerializer.java @@ -0,0 +1,98 @@ +package io.ably.lib.object.serialization; + +import com.google.gson.JsonArray; +import io.ably.lib.util.Log; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.msgpack.core.MessagePacker; +import org.msgpack.core.MessageUnpacker; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; + +/** + * Serializer interface for converting between objects and their MessagePack or JSON representations. + */ +public interface ObjectSerializer { + + /** + * Reads a MessagePack array from the given unpacker and deserializes it into an Object array. + * + * @param unpacker the MessageUnpacker to read from + * @return the deserialized Object array + * @throws IOException if an I/O error occurs during unpacking + */ + @NotNull + Object[] readMsgpackArray(@NotNull MessageUnpacker unpacker) throws IOException; + + /** + * Serializes the given Object array as a MessagePack array using the provided packer. + * + * @param objects the Object array to serialize + * @param packer the MessagePacker to write to + * @throws IOException if an I/O error occurs during packing + */ + void writeMsgpackArray(@NotNull Object[] objects, @NotNull MessagePacker packer) throws IOException; + + /** + * Reads a JSON array from the given {@link JsonArray} and deserializes it into an Object array. + * + * @param json the {@link JsonArray} representing the array to deserialize + * @return the deserialized Object array + */ + @NotNull + Object[] readFromJsonArray(@NotNull JsonArray json); + + /** + * Serializes the given Object array as a JSON array. + * + * @param objects the Object array to serialize + * @return the resulting JsonArray + */ + @NotNull + JsonArray asJsonArray(@NotNull Object[] objects); + + /** + * Returns the lazily-initialized, process-wide {@link ObjectSerializer} singleton, reflectively + * loaded from the LiveObjects plugin on the classpath. Returns {@code null} if the plugin is not + * present; the lookup is retried on subsequent calls until it succeeds. + * + * @return the shared {@link ObjectSerializer} instance, or {@code null} if the plugin is unavailable. + */ + @Nullable + static ObjectSerializer tryGet() { + return Holder.getSerializer(); + } + + /** + * Holds the lazily-initialized {@link ObjectSerializer} singleton. Interfaces cannot declare + * mutable static fields, so the cache lives here while {@link #tryGet()} delegates to it. + */ + final class Holder { + private static final String TAG = ObjectSerializer.Holder.class.getName(); + private static final String IMPLEMENTATION_CLASS = "io.ably.lib.object.serialization.DefaultObjectsSerializer"; + private static volatile ObjectSerializer objectsSerializer; + + private Holder() {} + + @Nullable + static ObjectSerializer getSerializer() { + if (objectsSerializer == null) { + synchronized (Holder.class) { + if (objectsSerializer == null) { // Double-Checked Locking (DCL) + try { + Class serializerClass = Class.forName(IMPLEMENTATION_CLASS); + objectsSerializer = (ObjectSerializer) serializerClass.getDeclaredConstructor().newInstance(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | + NoSuchMethodException | + InvocationTargetException e) { + Log.w(TAG, "Failed to init ObjectsSerializer, LiveObjects plugin not included in the classpath", e); + return null; + } + } + } + } + return objectsSerializer; + } + } +} From ad10253bc5a5c177aedad03ca2914277b7e76084 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 19 Jun 2026 17:51:19 +0530 Subject: [PATCH 3/6] Retarget path-based serializers to WireObjectMessage model Point the JSON and MsgPack serializers in io.ably.lib.object.serialization at the new WireObjectMessage wire model instead of the legacy io.ably.lib.objects.ObjectMessage, so the new `object` package has no dependency on the legacy `objects` package. - DefaultSerialization: implement the new ObjectSerializer interface and (de)serialize WireObjectMessage arrays (reflectively loaded via ObjectSerializer.Holder). - Json/MsgpackSerialization: bind the Wire* types; replace legacy objectError with the object package's objectStateError (same 500/92000). - WireObjectMessage: restore the gson annotations required for wire-format fidelity - @SerializedName("object") on objectState and @JsonAdapter(WireObjectDataJsonSerializer) on WireObjectData. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../lib/object/message/WireObjectMessage.kt | 5 + .../serialization/DefaultSerialization.kt | 18 +- .../object/serialization/JsonSerialization.kt | 27 +- .../serialization/MsgpackSerialization.kt | 252 +++++++++--------- 4 files changed, 152 insertions(+), 150 deletions(-) diff --git a/liveobjects/src/main/kotlin/io/ably/lib/object/message/WireObjectMessage.kt b/liveobjects/src/main/kotlin/io/ably/lib/object/message/WireObjectMessage.kt index 6d8ccd785..b6f2f63f4 100644 --- a/liveobjects/src/main/kotlin/io/ably/lib/object/message/WireObjectMessage.kt +++ b/liveobjects/src/main/kotlin/io/ably/lib/object/message/WireObjectMessage.kt @@ -3,6 +3,9 @@ package io.ably.lib.`object`.message import com.google.gson.Gson import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.annotations.JsonAdapter +import com.google.gson.annotations.SerializedName +import io.ably.lib.`object`.serialization.WireObjectDataJsonSerializer import java.nio.charset.StandardCharsets import java.util.Base64 @@ -36,6 +39,7 @@ internal enum class WireObjectsMapSemantics(val code: Int) { } /** Spec: OD1, OD2 - binary carried as base64 string on the wire */ +@JsonAdapter(WireObjectDataJsonSerializer::class) internal data class WireObjectData( val objectId: String? = null, // OD2a val string: String? = null, // OD2f @@ -145,6 +149,7 @@ internal data class WireObjectMessage( val connectionId: String? = null, // OM2c val extras: JsonObject? = null, // OM2d val operation: WireObjectOperation? = null, // OM2f + @SerializedName("object") val objectState: WireObjectState? = null, // OM2g - wire key "object" val serial: String? = null, // OM2h val serialTimestamp: Long? = null, // OM2j diff --git a/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/DefaultSerialization.kt b/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/DefaultSerialization.kt index e8db5c956..f410999fd 100644 --- a/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/DefaultSerialization.kt +++ b/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/DefaultSerialization.kt @@ -1,19 +1,17 @@ package io.ably.lib.`object`.serialization import com.google.gson.* -import io.ably.lib.objects.* - -import io.ably.lib.objects.ObjectMessage +import io.ably.lib.`object`.message.WireObjectMessage import org.msgpack.core.MessagePacker import org.msgpack.core.MessageUnpacker /** - * Default implementation of {@link ObjectsSerializer} that handles serialization/deserialization - * of ObjectMessage arrays for both JSON and MessagePack formats using Jackson and Gson. - * Dynamically loaded by ObjectsHelper#getSerializer() to avoid hard dependencies. + * Default implementation of {@link ObjectSerializer} that handles serialization/deserialization + * of WireObjectMessage arrays for both JSON and MessagePack formats using Gson and MessagePack. + * Dynamically loaded by ObjectSerializer#tryGet() to avoid hard dependencies. */ -@Suppress("unused") // Used via reflection in ObjectsHelper -internal class DefaultObjectsSerializer : ObjectsSerializer { +@Suppress("unused") // Used via reflection in ObjectSerializer.Holder +internal class DefaultObjectsSerializer : ObjectSerializer { override fun readMsgpackArray(unpacker: MessageUnpacker): Array { val objectMessagesCount = unpacker.unpackArrayHeader() @@ -21,7 +19,7 @@ internal class DefaultObjectsSerializer : ObjectsSerializer { } override fun writeMsgpackArray(objects: Array, packer: MessagePacker) { - val objectMessages = objects.map { it as ObjectMessage } + val objectMessages = objects.map { it as WireObjectMessage } packer.packArrayHeader(objectMessages.size) objectMessages.forEach { it.writeMsgpack(packer) } } @@ -34,7 +32,7 @@ internal class DefaultObjectsSerializer : ObjectsSerializer { } override fun asJsonArray(objects: Array): JsonArray { - val objectMessages = objects.map { it as ObjectMessage } + val objectMessages = objects.map { it as WireObjectMessage } val jsonArray = JsonArray() for (objectMessage in objectMessages) { jsonArray.add(objectMessage.toJsonObject()) diff --git a/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/JsonSerialization.kt b/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/JsonSerialization.kt index 07ef60cb7..cc5098cc5 100644 --- a/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/JsonSerialization.kt +++ b/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/JsonSerialization.kt @@ -1,26 +1,25 @@ package io.ably.lib.`object`.serialization import com.google.gson.* -import io.ably.lib.objects.ObjectsMapSemantics -import io.ably.lib.objects.ObjectData -import io.ably.lib.objects.ObjectMessage -import io.ably.lib.objects.ObjectOperationAction -import io.ably.lib.objects.serialization.EnumCodeTypeAdapter +import io.ably.lib.`object`.message.WireObjectData +import io.ably.lib.`object`.message.WireObjectMessage +import io.ably.lib.`object`.message.WireObjectOperationAction +import io.ably.lib.`object`.message.WireObjectsMapSemantics import java.lang.reflect.Type import kotlin.enums.EnumEntries // Gson instance for JSON serialization/deserialization internal val gson = GsonBuilder() - .registerTypeAdapter(ObjectOperationAction::class.java, EnumCodeTypeAdapter({ it.code }, ObjectOperationAction.entries)) - .registerTypeAdapter(ObjectsMapSemantics::class.java, EnumCodeTypeAdapter({ it.code }, ObjectsMapSemantics.entries)) + .registerTypeAdapter(WireObjectOperationAction::class.java, EnumCodeTypeAdapter({ it.code }, WireObjectOperationAction.entries)) + .registerTypeAdapter(WireObjectsMapSemantics::class.java, EnumCodeTypeAdapter({ it.code }, WireObjectsMapSemantics.entries)) .create() -internal fun ObjectMessage.toJsonObject(): JsonObject { +internal fun WireObjectMessage.toJsonObject(): JsonObject { return gson.toJsonTree(this).asJsonObject } -internal fun JsonObject.toObjectMessage(): ObjectMessage { - return gson.fromJson(this, ObjectMessage::class.java) +internal fun JsonObject.toObjectMessage(): WireObjectMessage { + return gson.fromJson(this, WireObjectMessage::class.java) } internal class EnumCodeTypeAdapter>( @@ -39,8 +38,8 @@ internal class EnumCodeTypeAdapter>( } } -internal class ObjectDataJsonSerializer : JsonSerializer, JsonDeserializer { - override fun serialize(src: ObjectData, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { +internal class WireObjectDataJsonSerializer : JsonSerializer, JsonDeserializer { + override fun serialize(src: WireObjectData, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { val obj = JsonObject() src.objectId?.let { obj.addProperty("objectId", it) } src.string?.let { obj.addProperty("string", it) } @@ -51,7 +50,7 @@ internal class ObjectDataJsonSerializer : JsonSerializer, JsonDeseri return obj } - override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): ObjectData { + override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): WireObjectData { val obj = if (json.isJsonObject) json.asJsonObject else throw JsonParseException("Expected JsonObject") val objectId = if (obj.has("objectId")) obj.get("objectId").asString else null val string = if (obj.has("string")) obj.get("string").asString else null @@ -63,6 +62,6 @@ internal class ObjectDataJsonSerializer : JsonSerializer, JsonDeseri if (objectId == null && string == null && number == null && boolean == null && bytes == null && json == null) { throw JsonParseException("Since objectId is not present, at least one of the value fields must be present") } - return ObjectData(objectId = objectId, string = string, number = number, boolean = boolean, bytes = bytes, json = json) + return WireObjectData(objectId = objectId, string = string, number = number, boolean = boolean, bytes = bytes, json = json) } } diff --git a/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/MsgpackSerialization.kt b/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/MsgpackSerialization.kt index 52e3ef533..0e5648002 100644 --- a/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/MsgpackSerialization.kt +++ b/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/MsgpackSerialization.kt @@ -3,35 +3,35 @@ package io.ably.lib.`object`.serialization import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonParser -import io.ably.lib.objects.* -import io.ably.lib.objects.CounterCreate -import io.ably.lib.objects.CounterCreateWithObjectId -import io.ably.lib.objects.CounterInc -import io.ably.lib.objects.MapCreate -import io.ably.lib.objects.MapCreateWithObjectId -import io.ably.lib.objects.MapRemove -import io.ably.lib.objects.MapSet -import io.ably.lib.objects.MapClear -import io.ably.lib.objects.ObjectDelete -import io.ably.lib.objects.ObjectsMapSemantics -import io.ably.lib.objects.ObjectsCounter -import io.ably.lib.objects.ObjectData -import io.ably.lib.objects.ObjectsMap -import io.ably.lib.objects.ObjectsMapEntry -import io.ably.lib.objects.ObjectMessage -import io.ably.lib.objects.ObjectOperation -import io.ably.lib.objects.ObjectOperationAction -import io.ably.lib.objects.ObjectState -import java.util.Base64 +import io.ably.lib.`object`.message.WireCounterCreate +import io.ably.lib.`object`.message.WireCounterCreateWithObjectId +import io.ably.lib.`object`.message.WireCounterInc +import io.ably.lib.`object`.message.WireMapClear +import io.ably.lib.`object`.message.WireMapCreate +import io.ably.lib.`object`.message.WireMapCreateWithObjectId +import io.ably.lib.`object`.message.WireMapRemove +import io.ably.lib.`object`.message.WireMapSet +import io.ably.lib.`object`.message.WireObjectData +import io.ably.lib.`object`.message.WireObjectDelete +import io.ably.lib.`object`.message.WireObjectMessage +import io.ably.lib.`object`.message.WireObjectOperation +import io.ably.lib.`object`.message.WireObjectOperationAction +import io.ably.lib.`object`.message.WireObjectState +import io.ably.lib.`object`.message.WireObjectsCounter +import io.ably.lib.`object`.message.WireObjectsMap +import io.ably.lib.`object`.message.WireObjectsMapEntry +import io.ably.lib.`object`.message.WireObjectsMapSemantics +import io.ably.lib.`object`.objectStateError import io.ably.lib.util.Serialisation +import java.util.Base64 import org.msgpack.core.MessageFormat import org.msgpack.core.MessagePacker import org.msgpack.core.MessageUnpacker /** - * Write ObjectMessage to MessagePacker + * Write WireObjectMessage to MessagePacker */ -internal fun ObjectMessage.writeMsgpack(packer: MessagePacker) { +internal fun WireObjectMessage.writeMsgpack(packer: MessagePacker) { var fieldCount = 0 if (id != null) fieldCount++ @@ -99,12 +99,12 @@ internal fun ObjectMessage.writeMsgpack(packer: MessagePacker) { } /** - * Read an ObjectMessage from MessageUnpacker + * Read a WireObjectMessage from MessageUnpacker */ -internal fun readObjectMessage(unpacker: MessageUnpacker): ObjectMessage { +internal fun readObjectMessage(unpacker: MessageUnpacker): WireObjectMessage { if (unpacker.nextFormat == MessageFormat.NIL) { unpacker.unpackNil() - return ObjectMessage() // default/empty message + return WireObjectMessage() // default/empty message } val fieldCount = unpacker.unpackMapHeader() @@ -114,8 +114,8 @@ internal fun readObjectMessage(unpacker: MessageUnpacker): ObjectMessage { var clientId: String? = null var connectionId: String? = null var extras: JsonObject? = null - var operation: ObjectOperation? = null - var objectState: ObjectState? = null + var operation: WireObjectOperation? = null + var objectState: WireObjectState? = null var serial: String? = null var serialTimestamp: Long? = null var siteCode: String? = null @@ -144,7 +144,7 @@ internal fun readObjectMessage(unpacker: MessageUnpacker): ObjectMessage { } } - return ObjectMessage( + return WireObjectMessage( id = id, timestamp = timestamp, clientId = clientId, @@ -159,9 +159,9 @@ internal fun readObjectMessage(unpacker: MessageUnpacker): ObjectMessage { } /** - * Write ObjectOperation to MessagePacker + * Write WireObjectOperation to MessagePacker */ -private fun ObjectOperation.writeMsgpack(packer: MessagePacker) { +private fun WireObjectOperation.writeMsgpack(packer: MessagePacker) { var fieldCount = 1 // action is always required require(objectId.isNotEmpty()) { "objectId must be non-empty per Objects protocol" } fieldCount++ @@ -233,22 +233,22 @@ private fun ObjectOperation.writeMsgpack(packer: MessagePacker) { } /** - * Read ObjectOperation from MessageUnpacker + * Read WireObjectOperation from MessageUnpacker */ -private fun readObjectOperation(unpacker: MessageUnpacker): ObjectOperation { +private fun readObjectOperation(unpacker: MessageUnpacker): WireObjectOperation { val fieldCount = unpacker.unpackMapHeader() - var action: ObjectOperationAction? = null + var action: WireObjectOperationAction? = null var objectId: String = "" - var mapCreate: MapCreate? = null - var mapSet: MapSet? = null - var mapRemove: MapRemove? = null - var counterCreate: CounterCreate? = null - var counterInc: CounterInc? = null - var objectDelete: ObjectDelete? = null - var mapCreateWithObjectId: MapCreateWithObjectId? = null - var counterCreateWithObjectId: CounterCreateWithObjectId? = null - var mapClear: MapClear? = null + var mapCreate: WireMapCreate? = null + var mapSet: WireMapSet? = null + var mapRemove: WireMapRemove? = null + var counterCreate: WireCounterCreate? = null + var counterInc: WireCounterInc? = null + var objectDelete: WireObjectDelete? = null + var mapCreateWithObjectId: WireMapCreateWithObjectId? = null + var counterCreateWithObjectId: WireCounterCreateWithObjectId? = null + var mapClear: WireMapClear? = null for (i in 0 until fieldCount) { val fieldName = unpacker.unpackString().intern() @@ -262,9 +262,9 @@ private fun readObjectOperation(unpacker: MessageUnpacker): ObjectOperation { when (fieldName) { "action" -> { val actionCode = unpacker.unpackInt() - action = ObjectOperationAction.entries.firstOrNull { it.code == actionCode } - ?: ObjectOperationAction.entries.firstOrNull { it.code == -1 } - ?: throw objectError("Unknown ObjectOperationAction code: $actionCode and no Unknown fallback found") + action = WireObjectOperationAction.entries.firstOrNull { it.code == actionCode } + ?: WireObjectOperationAction.entries.firstOrNull { it.code == -1 } + ?: throw objectStateError("Unknown WireObjectOperationAction code: $actionCode and no Unknown fallback found") } "objectId" -> objectId = unpacker.unpackString() "mapCreate" -> mapCreate = readMapCreate(unpacker) @@ -274,23 +274,23 @@ private fun readObjectOperation(unpacker: MessageUnpacker): ObjectOperation { "counterInc" -> counterInc = readCounterInc(unpacker) "objectDelete" -> { unpacker.skipValue() // empty map, just consume it - objectDelete = ObjectDelete + objectDelete = WireObjectDelete } "mapCreateWithObjectId" -> mapCreateWithObjectId = readMapCreateWithObjectId(unpacker) "counterCreateWithObjectId" -> counterCreateWithObjectId = readCounterCreateWithObjectId(unpacker) "mapClear" -> { unpacker.skipValue() // empty map, consume it - mapClear = MapClear + mapClear = WireMapClear } else -> unpacker.skipValue() } } if (action == null) { - throw objectError("Missing required 'action' field in ObjectOperation") + throw objectStateError("Missing required 'action' field in WireObjectOperation") } - return ObjectOperation( + return WireObjectOperation( action = action, objectId = objectId, mapCreate = mapCreate, @@ -306,9 +306,9 @@ private fun readObjectOperation(unpacker: MessageUnpacker): ObjectOperation { } /** - * Write ObjectState to MessagePacker + * Write WireObjectState to MessagePacker */ -private fun ObjectState.writeMsgpack(packer: MessagePacker) { +private fun WireObjectState.writeMsgpack(packer: MessagePacker) { var fieldCount = 3 // objectId, siteTimeserials, and tombstone are required if (createOp != null) fieldCount++ @@ -347,17 +347,17 @@ private fun ObjectState.writeMsgpack(packer: MessagePacker) { } /** - * Read ObjectState from MessageUnpacker + * Read WireObjectState from MessageUnpacker */ -private fun readObjectState(unpacker: MessageUnpacker): ObjectState { +private fun readObjectState(unpacker: MessageUnpacker): WireObjectState { val fieldCount = unpacker.unpackMapHeader() var objectId = "" var siteTimeserials = mapOf() var tombstone = false - var createOp: ObjectOperation? = null - var map: ObjectsMap? = null - var counter: ObjectsCounter? = null + var createOp: WireObjectOperation? = null + var map: WireObjectsMap? = null + var counter: WireObjectsCounter? = null for (i in 0 until fieldCount) { val fieldName = unpacker.unpackString().intern() @@ -388,7 +388,7 @@ private fun readObjectState(unpacker: MessageUnpacker): ObjectState { } } - return ObjectState( + return WireObjectState( objectId = objectId, siteTimeserials = siteTimeserials, tombstone = tombstone, @@ -399,9 +399,9 @@ private fun readObjectState(unpacker: MessageUnpacker): ObjectState { } /** - * Write MapCreate to MessagePacker + * Write WireMapCreate to MessagePacker */ -private fun MapCreate.writeMsgpack(packer: MessagePacker) { +private fun WireMapCreate.writeMsgpack(packer: MessagePacker) { packer.packMapHeader(2) packer.packString("semantics") packer.packInt(semantics.code) @@ -414,12 +414,12 @@ private fun MapCreate.writeMsgpack(packer: MessagePacker) { } /** - * Read MapCreate from MessageUnpacker + * Read WireMapCreate from MessageUnpacker */ -private fun readMapCreate(unpacker: MessageUnpacker): MapCreate { +private fun readMapCreate(unpacker: MessageUnpacker): WireMapCreate { val fieldCount = unpacker.unpackMapHeader() - var semantics: ObjectsMapSemantics = ObjectsMapSemantics.LWW - var entries: Map = emptyMap() + var semantics: WireObjectsMapSemantics = WireObjectsMapSemantics.LWW + var entries: Map = emptyMap() for (i in 0 until fieldCount) { val fieldName = unpacker.unpackString().intern() @@ -428,13 +428,13 @@ private fun readMapCreate(unpacker: MessageUnpacker): MapCreate { when (fieldName) { "semantics" -> { val code = unpacker.unpackInt() - semantics = ObjectsMapSemantics.entries.firstOrNull { it.code == code } - ?: ObjectsMapSemantics.entries.firstOrNull { it.code == -1 } - ?: throw objectError("Unknown MapSemantics code: $code and no UNKNOWN fallback found") + semantics = WireObjectsMapSemantics.entries.firstOrNull { it.code == code } + ?: WireObjectsMapSemantics.entries.firstOrNull { it.code == -1 } + ?: throw objectStateError("Unknown MapSemantics code: $code and no UNKNOWN fallback found") } "entries" -> { val mapSize = unpacker.unpackMapHeader() - val tempMap = mutableMapOf() + val tempMap = mutableMapOf() for (j in 0 until mapSize) { tempMap[unpacker.unpackString()] = readObjectMapEntry(unpacker) } @@ -443,13 +443,13 @@ private fun readMapCreate(unpacker: MessageUnpacker): MapCreate { else -> unpacker.skipValue() } } - return MapCreate(semantics = semantics, entries = entries) + return WireMapCreate(semantics = semantics, entries = entries) } /** - * Write MapSet to MessagePacker + * Write WireMapSet to MessagePacker */ -private fun MapSet.writeMsgpack(packer: MessagePacker) { +private fun WireMapSet.writeMsgpack(packer: MessagePacker) { packer.packMapHeader(2) packer.packString("key") packer.packString(key) @@ -458,12 +458,12 @@ private fun MapSet.writeMsgpack(packer: MessagePacker) { } /** - * Read MapSet from MessageUnpacker + * Read WireMapSet from MessageUnpacker */ -private fun readMapSet(unpacker: MessageUnpacker): MapSet { +private fun readMapSet(unpacker: MessageUnpacker): WireMapSet { val fieldCount = unpacker.unpackMapHeader() var key: String? = null - var value: ObjectData? = null + var value: WireObjectData? = null for (i in 0 until fieldCount) { val fieldName = unpacker.unpackString().intern() @@ -475,25 +475,25 @@ private fun readMapSet(unpacker: MessageUnpacker): MapSet { else -> unpacker.skipValue() } } - return MapSet( - key = key ?: throw objectError("Missing 'key' in MapSet payload"), - value = value ?: throw objectError("Missing 'value' in MapSet payload") + return WireMapSet( + key = key ?: throw objectStateError("Missing 'key' in WireMapSet payload"), + value = value ?: throw objectStateError("Missing 'value' in WireMapSet payload") ) } /** - * Write MapRemove to MessagePacker + * Write WireMapRemove to MessagePacker */ -private fun MapRemove.writeMsgpack(packer: MessagePacker) { +private fun WireMapRemove.writeMsgpack(packer: MessagePacker) { packer.packMapHeader(1) packer.packString("key") packer.packString(key) } /** - * Read MapRemove from MessageUnpacker + * Read WireMapRemove from MessageUnpacker */ -private fun readMapRemove(unpacker: MessageUnpacker): MapRemove { +private fun readMapRemove(unpacker: MessageUnpacker): WireMapRemove { val fieldCount = unpacker.unpackMapHeader() var key: String? = null @@ -506,22 +506,22 @@ private fun readMapRemove(unpacker: MessageUnpacker): MapRemove { else -> unpacker.skipValue() } } - return MapRemove(key = key ?: throw objectError("Missing 'key' in MapRemove payload")) + return WireMapRemove(key = key ?: throw objectStateError("Missing 'key' in WireMapRemove payload")) } /** - * Write CounterCreate to MessagePacker + * Write WireCounterCreate to MessagePacker */ -private fun CounterCreate.writeMsgpack(packer: MessagePacker) { +private fun WireCounterCreate.writeMsgpack(packer: MessagePacker) { packer.packMapHeader(1) packer.packString("count") packer.packDouble(count) } /** - * Read CounterCreate from MessageUnpacker + * Read WireCounterCreate from MessageUnpacker */ -private fun readCounterCreate(unpacker: MessageUnpacker): CounterCreate { +private fun readCounterCreate(unpacker: MessageUnpacker): WireCounterCreate { val fieldCount = unpacker.unpackMapHeader() var count: Double? = null @@ -534,22 +534,22 @@ private fun readCounterCreate(unpacker: MessageUnpacker): CounterCreate { else -> unpacker.skipValue() } } - return CounterCreate(count = count ?: throw objectError("Missing 'count' in CounterCreate payload")) + return WireCounterCreate(count = count ?: throw objectStateError("Missing 'count' in WireCounterCreate payload")) } /** - * Write CounterInc to MessagePacker + * Write WireCounterInc to MessagePacker */ -private fun CounterInc.writeMsgpack(packer: MessagePacker) { +private fun WireCounterInc.writeMsgpack(packer: MessagePacker) { packer.packMapHeader(1) packer.packString("number") packer.packDouble(number) } /** - * Read CounterInc from MessageUnpacker + * Read WireCounterInc from MessageUnpacker */ -private fun readCounterInc(unpacker: MessageUnpacker): CounterInc { +private fun readCounterInc(unpacker: MessageUnpacker): WireCounterInc { val fieldCount = unpacker.unpackMapHeader() var number: Double? = null @@ -562,13 +562,13 @@ private fun readCounterInc(unpacker: MessageUnpacker): CounterInc { else -> unpacker.skipValue() } } - return CounterInc(number = number ?: throw objectError("Missing 'number' in CounterInc payload")) + return WireCounterInc(number = number ?: throw objectStateError("Missing 'number' in WireCounterInc payload")) } /** - * Write MapCreateWithObjectId to MessagePacker + * Write WireMapCreateWithObjectId to MessagePacker */ -private fun MapCreateWithObjectId.writeMsgpack(packer: MessagePacker) { +private fun WireMapCreateWithObjectId.writeMsgpack(packer: MessagePacker) { packer.packMapHeader(2) packer.packString("initialValue") packer.packString(initialValue) @@ -577,9 +577,9 @@ private fun MapCreateWithObjectId.writeMsgpack(packer: MessagePacker) { } /** - * Read MapCreateWithObjectId from MessageUnpacker + * Read WireMapCreateWithObjectId from MessageUnpacker */ -private fun readMapCreateWithObjectId(unpacker: MessageUnpacker): MapCreateWithObjectId { +private fun readMapCreateWithObjectId(unpacker: MessageUnpacker): WireMapCreateWithObjectId { val fieldCount = unpacker.unpackMapHeader() var initialValue: String? = null var nonce: String? = null @@ -594,16 +594,16 @@ private fun readMapCreateWithObjectId(unpacker: MessageUnpacker): MapCreateWithO else -> unpacker.skipValue() } } - return MapCreateWithObjectId( - initialValue = initialValue ?: throw objectError("Missing 'initialValue' in MapCreateWithObjectId payload"), - nonce = nonce ?: throw objectError("Missing 'nonce' in MapCreateWithObjectId payload") + return WireMapCreateWithObjectId( + initialValue = initialValue ?: throw objectStateError("Missing 'initialValue' in WireMapCreateWithObjectId payload"), + nonce = nonce ?: throw objectStateError("Missing 'nonce' in WireMapCreateWithObjectId payload") ) } /** - * Write CounterCreateWithObjectId to MessagePacker + * Write WireCounterCreateWithObjectId to MessagePacker */ -private fun CounterCreateWithObjectId.writeMsgpack(packer: MessagePacker) { +private fun WireCounterCreateWithObjectId.writeMsgpack(packer: MessagePacker) { packer.packMapHeader(2) packer.packString("initialValue") packer.packString(initialValue) @@ -612,9 +612,9 @@ private fun CounterCreateWithObjectId.writeMsgpack(packer: MessagePacker) { } /** - * Read CounterCreateWithObjectId from MessageUnpacker + * Read WireCounterCreateWithObjectId from MessageUnpacker */ -private fun readCounterCreateWithObjectId(unpacker: MessageUnpacker): CounterCreateWithObjectId { +private fun readCounterCreateWithObjectId(unpacker: MessageUnpacker): WireCounterCreateWithObjectId { val fieldCount = unpacker.unpackMapHeader() var initialValue: String? = null var nonce: String? = null @@ -629,16 +629,16 @@ private fun readCounterCreateWithObjectId(unpacker: MessageUnpacker): CounterCre else -> unpacker.skipValue() } } - return CounterCreateWithObjectId( - initialValue = initialValue ?: throw objectError("Missing 'initialValue' in CounterCreateWithObjectId payload"), - nonce = nonce ?: throw objectError("Missing 'nonce' in CounterCreateWithObjectId payload") + return WireCounterCreateWithObjectId( + initialValue = initialValue ?: throw objectStateError("Missing 'initialValue' in WireCounterCreateWithObjectId payload"), + nonce = nonce ?: throw objectStateError("Missing 'nonce' in WireCounterCreateWithObjectId payload") ) } /** * Write ObjectMap to MessagePacker */ -private fun ObjectsMap.writeMsgpack(packer: MessagePacker) { +private fun WireObjectsMap.writeMsgpack(packer: MessagePacker) { var fieldCount = 0 if (semantics != null) fieldCount++ @@ -670,11 +670,11 @@ private fun ObjectsMap.writeMsgpack(packer: MessagePacker) { /** * Read ObjectMap from MessageUnpacker */ -private fun readObjectMap(unpacker: MessageUnpacker): ObjectsMap { +private fun readObjectMap(unpacker: MessageUnpacker): WireObjectsMap { val fieldCount = unpacker.unpackMapHeader() - var semantics: ObjectsMapSemantics? = null - var entries: Map? = null + var semantics: WireObjectsMapSemantics? = null + var entries: Map? = null var clearTimeserial: String? = null for (i in 0 until fieldCount) { @@ -689,13 +689,13 @@ private fun readObjectMap(unpacker: MessageUnpacker): ObjectsMap { when (fieldName) { "semantics" -> { val semanticsCode = unpacker.unpackInt() - semantics = ObjectsMapSemantics.entries.firstOrNull { it.code == semanticsCode } - ?: ObjectsMapSemantics.entries.firstOrNull { it.code == -1 } - ?: throw objectError("Unknown MapSemantics code: $semanticsCode and no UNKNOWN fallback found") + semantics = WireObjectsMapSemantics.entries.firstOrNull { it.code == semanticsCode } + ?: WireObjectsMapSemantics.entries.firstOrNull { it.code == -1 } + ?: throw objectStateError("Unknown MapSemantics code: $semanticsCode and no UNKNOWN fallback found") } "entries" -> { val mapSize = unpacker.unpackMapHeader() - val tempMap = mutableMapOf() + val tempMap = mutableMapOf() for (j in 0 until mapSize) { val key = unpacker.unpackString() val value = readObjectMapEntry(unpacker) @@ -708,13 +708,13 @@ private fun readObjectMap(unpacker: MessageUnpacker): ObjectsMap { } } - return ObjectsMap(semantics = semantics, entries = entries, clearTimeserial = clearTimeserial) + return WireObjectsMap(semantics = semantics, entries = entries, clearTimeserial = clearTimeserial) } /** * Write ObjectCounter to MessagePacker */ -private fun ObjectsCounter.writeMsgpack(packer: MessagePacker) { +private fun WireObjectsCounter.writeMsgpack(packer: MessagePacker) { var fieldCount = 0 if (count != null) fieldCount++ @@ -730,7 +730,7 @@ private fun ObjectsCounter.writeMsgpack(packer: MessagePacker) { /** * Read ObjectCounter from MessageUnpacker */ -private fun readObjectCounter(unpacker: MessageUnpacker): ObjectsCounter { +private fun readObjectCounter(unpacker: MessageUnpacker): WireObjectsCounter { val fieldCount = unpacker.unpackMapHeader() var count: Double? = null @@ -750,13 +750,13 @@ private fun readObjectCounter(unpacker: MessageUnpacker): ObjectsCounter { } } - return ObjectsCounter(count = count) + return WireObjectsCounter(count = count) } /** * Write ObjectMapEntry to MessagePacker */ -private fun ObjectsMapEntry.writeMsgpack(packer: MessagePacker) { +private fun WireObjectsMapEntry.writeMsgpack(packer: MessagePacker) { var fieldCount = 0 if (tombstone != null) fieldCount++ @@ -790,13 +790,13 @@ private fun ObjectsMapEntry.writeMsgpack(packer: MessagePacker) { /** * Read ObjectMapEntry from MessageUnpacker */ -private fun readObjectMapEntry(unpacker: MessageUnpacker): ObjectsMapEntry { +private fun readObjectMapEntry(unpacker: MessageUnpacker): WireObjectsMapEntry { val fieldCount = unpacker.unpackMapHeader() var tombstone: Boolean? = null var timeserial: String? = null var serialTimestamp: Long? = null - var data: ObjectData? = null + var data: WireObjectData? = null for (i in 0 until fieldCount) { val fieldName = unpacker.unpackString().intern() @@ -816,13 +816,13 @@ private fun readObjectMapEntry(unpacker: MessageUnpacker): ObjectsMapEntry { } } - return ObjectsMapEntry(tombstone = tombstone, timeserial = timeserial, serialTimestamp = serialTimestamp, data = data) + return WireObjectsMapEntry(tombstone = tombstone, timeserial = timeserial, serialTimestamp = serialTimestamp, data = data) } /** - * Write ObjectData to MessagePacker + * Write WireObjectData to MessagePacker */ -private fun ObjectData.writeMsgpack(packer: MessagePacker) { +private fun WireObjectData.writeMsgpack(packer: MessagePacker) { var fieldCount = 0 if (objectId != null) fieldCount++ @@ -868,9 +868,9 @@ private fun ObjectData.writeMsgpack(packer: MessagePacker) { } /** - * Read ObjectData from MessageUnpacker + * Read WireObjectData from MessageUnpacker */ -private fun readObjectData(unpacker: MessageUnpacker): ObjectData { +private fun readObjectData(unpacker: MessageUnpacker): WireObjectData { val fieldCount = unpacker.unpackMapHeader() var objectId: String? = null var string: String? = null @@ -904,5 +904,5 @@ private fun readObjectData(unpacker: MessageUnpacker): ObjectData { } } - return ObjectData(objectId = objectId, string = string, number = number, boolean = boolean, bytes = bytes, json = json) + return WireObjectData(objectId = objectId, string = string, number = number, boolean = boolean, bytes = bytes, json = json) } From 863f1f632c516950989d6558c27964d2229a9374 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 19 Jun 2026 17:54:53 +0530 Subject: [PATCH 4/6] Replace gson star import with explicit imports in ObjectJsonSerializer Fixes checkstyle AvoidStarImport violation on com.google.gson.*. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../lib/object/serialization/ObjectJsonSerializer.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/src/main/java/io/ably/lib/object/serialization/ObjectJsonSerializer.java b/lib/src/main/java/io/ably/lib/object/serialization/ObjectJsonSerializer.java index 08e5d6b71..9deeb01fb 100644 --- a/lib/src/main/java/io/ably/lib/object/serialization/ObjectJsonSerializer.java +++ b/lib/src/main/java/io/ably/lib/object/serialization/ObjectJsonSerializer.java @@ -1,6 +1,12 @@ package io.ably.lib.object.serialization; -import com.google.gson.*; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; import io.ably.lib.util.Log; import java.lang.reflect.Type; From bfa574f7a52d6893c5e0e6211132633386370617 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Fri, 19 Jun 2026 17:56:16 +0530 Subject: [PATCH 5/6] Implemented `LiveObjectsPlugin` interface with relevant Factory method --- .../io/ably/lib/object/LiveObjectsPlugin.java | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 lib/src/main/java/io/ably/lib/object/LiveObjectsPlugin.java diff --git a/lib/src/main/java/io/ably/lib/object/LiveObjectsPlugin.java b/lib/src/main/java/io/ably/lib/object/LiveObjectsPlugin.java new file mode 100644 index 000000000..20d78fc1f --- /dev/null +++ b/lib/src/main/java/io/ably/lib/object/LiveObjectsPlugin.java @@ -0,0 +1,109 @@ +package io.ably.lib.object; + +import io.ably.lib.object.adapter.AblyClientAdapter; +import io.ably.lib.object.adapter.Adapter; +import io.ably.lib.objects.RealtimeObjects; +import io.ably.lib.realtime.AblyRealtime; +import io.ably.lib.realtime.ChannelState; +import io.ably.lib.types.ProtocolMessage; +import io.ably.lib.util.Log; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.InvocationTargetException; + +/** + * The LiveObjectsPlugin interface provides a mechanism for managing and interacting with + * live data objects in a real-time environment. It allows for the retrieval, disposal, and + * management of Objects instances associated with specific channel names. + */ +public interface LiveObjectsPlugin { + + /** + * Retrieves an instance of RealtimeObjects associated with the specified channel name. + * This method ensures that a RealtimeObjects instance is available for the given channel, + * creating one if it does not already exist. + * + * @param channelName the name of the channel for which the RealtimeObjects instance is to be retrieved. + * @return the RealtimeObjects instance associated with the specified channel name. + */ + @NotNull + RealtimeObjects getInstance(@NotNull String channelName); + + /** + * Handles a protocol message. + * This method is invoked whenever a protocol message is received, allowing the implementation + * to process the message and take appropriate actions. + * + * @param message the protocol message to handle. + */ + void handle(@NotNull ProtocolMessage message); + + /** + * Handles state changes for a specific channel. + * This method is invoked whenever a channel's state changes, allowing the implementation + * to update the RealtimeObjects instances accordingly based on the new state and presence of objects. + * + * @param channelName the name of the channel whose state has changed. + * @param state the new state of the channel. + * @param hasObjects flag indicates whether the channel has any associated objects. + */ + void handleStateChange(@NotNull String channelName, @NotNull ChannelState state, boolean hasObjects); + + /** + * Disposes of the RealtimeObjects instance associated with the specified channel name. + * This method removes the RealtimeObjects instance for the given channel, releasing any + * resources associated with it. + * This is invoked when ablyRealtimeClient.channels.release(channelName) is called + * + * @param channelName the name of the channel whose RealtimeObjects instance is to be removed. + */ + void dispose(@NotNull String channelName); + + /** + * Disposes of the plugin instance and all underlying resources. + * This is invoked when ablyRealtimeClient.close() is called + */ + void dispose(); + + /** + * Attempts to initialize the LiveObjects plugin by reflectively loading its implementation + * from the classpath. Returns a new plugin instance on every successful invocation, or + * {@code null} if the LiveObjects plugin is not present in the classpath. + * + * @param ablyRealtime the AblyRealtime client used to build the adapter the plugin runs against. + * @return a new {@link LiveObjectsPlugin} instance, or {@code null} if the plugin is unavailable. + */ + @Nullable + static LiveObjectsPlugin tryInitialize(@NotNull AblyRealtime ablyRealtime) { + return Factory.create(ablyRealtime); + } + + /** + * Reflectively constructs the LiveObjects plugin implementation. Lives in a nested class so the + * implementation-class name stays {@code private} (interface fields are forced {@code public}), + * mirroring {@link io.ably.lib.object.serialization.ObjectSerializer.Holder}. Unlike {@code Holder} + * this is stateless: {@link #create} returns a new instance on every call. + */ + final class Factory { + private static final String TAG = LiveObjectsPlugin.Factory.class.getName(); + private static final String IMPLEMENTATION_CLASS = "io.ably.lib.object.DefaultLiveObjectsPlugin"; + + private Factory() {} + + @Nullable + static LiveObjectsPlugin create(@NotNull AblyRealtime ablyRealtime) { + try { + Class objectsImplementation = Class.forName(IMPLEMENTATION_CLASS); + AblyClientAdapter adapter = new Adapter(ablyRealtime); + return (LiveObjectsPlugin) objectsImplementation + .getDeclaredConstructor(AblyClientAdapter.class) + .newInstance(adapter); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | + InvocationTargetException e) { + Log.i(TAG, "LiveObjects plugin not found in classpath. LiveObjects functionality will not be available.", e); + return null; + } + } + } +} From c489ac05769541257bc24ed31f9b50971bd0cd48 Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Mon, 22 Jun 2026 13:14:45 +0530 Subject: [PATCH 6/6] Updated validation checks/log messages as per review comments --- .../lib/object/serialization/ObjectJsonSerializer.java | 4 ++-- .../ably/lib/object/serialization/ObjectSerializer.java | 2 +- .../ably/lib/object/serialization/MsgpackSerialization.kt | 8 ++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/src/main/java/io/ably/lib/object/serialization/ObjectJsonSerializer.java b/lib/src/main/java/io/ably/lib/object/serialization/ObjectJsonSerializer.java index 9deeb01fb..8c8566490 100644 --- a/lib/src/main/java/io/ably/lib/object/serialization/ObjectJsonSerializer.java +++ b/lib/src/main/java/io/ably/lib/object/serialization/ObjectJsonSerializer.java @@ -18,7 +18,7 @@ public class ObjectJsonSerializer implements JsonSerializer, JsonDeser public Object[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { ObjectSerializer serializer = ObjectSerializer.tryGet(); if (serializer == null) { - Log.w(TAG, "Skipping 'state' field json deserialization because ObjectsSerializer not found."); + Log.w(TAG, "Skipping 'state' field json deserialization because ObjectSerializer not found."); return null; } if (!json.isJsonArray()) { @@ -31,7 +31,7 @@ public Object[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationC public JsonElement serialize(Object[] src, Type typeOfSrc, JsonSerializationContext context) { ObjectSerializer serializer = ObjectSerializer.tryGet(); if (serializer == null) { - Log.w(TAG, "Skipping 'state' field json serialization because ObjectsSerializer not found."); + Log.w(TAG, "Skipping 'state' field json serialization because ObjectSerializer not found."); return JsonNull.INSTANCE; } return serializer.asJsonArray(src); diff --git a/lib/src/main/java/io/ably/lib/object/serialization/ObjectSerializer.java b/lib/src/main/java/io/ably/lib/object/serialization/ObjectSerializer.java index 67b21b77c..78d237104 100644 --- a/lib/src/main/java/io/ably/lib/object/serialization/ObjectSerializer.java +++ b/lib/src/main/java/io/ably/lib/object/serialization/ObjectSerializer.java @@ -86,7 +86,7 @@ static ObjectSerializer getSerializer() { } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { - Log.w(TAG, "Failed to init ObjectsSerializer, LiveObjects plugin not included in the classpath", e); + Log.w(TAG, "Failed to init ObjectSerializer, LiveObjects plugin not included in the classpath", e); return null; } } diff --git a/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/MsgpackSerialization.kt b/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/MsgpackSerialization.kt index 0e5648002..849f41a4e 100644 --- a/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/MsgpackSerialization.kt +++ b/liveobjects/src/main/kotlin/io/ably/lib/object/serialization/MsgpackSerialization.kt @@ -290,6 +290,10 @@ private fun readObjectOperation(unpacker: MessageUnpacker): WireObjectOperation throw objectStateError("Missing required 'action' field in WireObjectOperation") } + if (objectId.isEmpty()) { + throw objectStateError("Missing required 'objectId' field in WireObjectOperation") + } + return WireObjectOperation( action = action, objectId = objectId, @@ -388,6 +392,10 @@ private fun readObjectState(unpacker: MessageUnpacker): WireObjectState { } } + if (objectId.isEmpty()) { + throw objectStateError("Missing required 'objectId' field in WireObjectState") + } + return WireObjectState( objectId = objectId, siteTimeserials = siteTimeserials,