diff --git a/lib/src/main/java/io/ably/lib/transport/Defaults.java b/lib/src/main/java/io/ably/lib/transport/Defaults.java index 83d5d83e4..66e3e897c 100644 --- a/lib/src/main/java/io/ably/lib/transport/Defaults.java +++ b/lib/src/main/java/io/ably/lib/transport/Defaults.java @@ -12,7 +12,7 @@ public class Defaults { * spec: G4 *

*/ - public static final String ABLY_PROTOCOL_VERSION = "5"; + public static final String ABLY_PROTOCOL_VERSION = "6"; public static final String ABLY_AGENT_VERSION = String.format("%s/%s", "ably-java", BuildConfig.VERSION); diff --git a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java index 0aa8f9c1a..fb258afb9 100644 --- a/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java +++ b/lib/src/test/java/io/ably/lib/test/realtime/RealtimeHttpHeaderTest.java @@ -81,7 +81,7 @@ public void realtime_websocket_param_test() { * Defaults.ABLY_VERSION_PARAM, as ultimately the request param has been derived from those values. */ assertEquals("Verify correct version", requestParameters.get("v"), - Collections.singletonList("5")); + Collections.singletonList("6")); /* Spec RSC7d3 * This test should not directly validate version against Defaults.ABLY_AGENT_VERSION, nor diff --git a/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java b/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java index f26fda4eb..14578a03b 100644 --- a/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java +++ b/lib/src/test/java/io/ably/lib/test/rest/HttpHeaderTest.java @@ -81,7 +81,7 @@ public void header_lib_channel_publish() { * from those values. */ Assert.assertNotNull("Expected headers", headers); - Assert.assertEquals(headers.get("x-ably-version"), "5"); + Assert.assertEquals(headers.get("x-ably-version"), "6"); Assert.assertEquals(headers.get("ably-agent"), expectedAblyAgentHeader); // RSA7e2 Assert.assertNull("Shouldn't include 'x-ably-clientid' if `clientId` is not specified", headers.get("x-ably-clientid")); diff --git a/lib/src/test/java/io/ably/lib/transport/DefaultsTest.java b/lib/src/test/java/io/ably/lib/transport/DefaultsTest.java index 7f52ecb83..ae41df572 100644 --- a/lib/src/test/java/io/ably/lib/transport/DefaultsTest.java +++ b/lib/src/test/java/io/ably/lib/transport/DefaultsTest.java @@ -9,7 +9,7 @@ public class DefaultsTest { @Test public void protocol_version_CSV2() { - assertThat(Defaults.ABLY_PROTOCOL_VERSION, is("5")); + assertThat(Defaults.ABLY_PROTOCOL_VERSION, is("6")); } @Test diff --git a/liveobjects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt b/liveobjects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt index 056969aa8..2a89f957d 100644 --- a/liveobjects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt +++ b/liveobjects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt @@ -126,7 +126,7 @@ internal class DefaultRealtimeObjects(internal val channelName: String, internal operation = ObjectOperation( action = ObjectOperationAction.MapCreate, objectId = objectId, - map = initialMapValue.map, + mapCreate = initialMapValue, nonce = nonce, initialValue = initialValueJSONString, ) @@ -161,7 +161,7 @@ internal class DefaultRealtimeObjects(internal val channelName: String, internal operation = ObjectOperation( action = ObjectOperationAction.CounterCreate, objectId = objectId, - counter = initialCounterValue.counter, + counterCreate = initialCounterValue, nonce = nonce, initialValue = initialValueJSONString ) diff --git a/liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt b/liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt index 72d72eab1..6a855868c 100644 --- a/liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt +++ b/liveobjects/src/main/kotlin/io/ably/lib/objects/Helpers.kt @@ -161,26 +161,4 @@ internal fun ObjectsAdapter.throwIfEchoMessagesDisabled() { } } -internal class Binary(val data: ByteArray) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Binary) return false - return data.contentEquals(other.data) - } - - override fun hashCode(): Int { - return data.contentHashCode() - } -} - -internal fun Binary.size(): Int { - return data.size -} - -internal data class CounterCreatePayload( - val counter: ObjectsCounter -) -internal data class MapCreatePayload( - val map: ObjectsMap -) diff --git a/liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt b/liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt index 0415cc8d5..09015a87c 100644 --- a/liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt +++ b/liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt @@ -1,11 +1,13 @@ package io.ably.lib.objects +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.objects.serialization.ObjectDataJsonSerializer import io.ably.lib.objects.serialization.gson +import java.util.Base64 /** * An enum class representing the different actions that can be performed on an object. @@ -42,57 +44,87 @@ internal data class ObjectData( */ val objectId: String? = null, - /** - * String, number, boolean or binary - a concrete value of the object - * Spec: OD2c - */ - val value: ObjectValue? = null, + /** String value. Spec: OD2c */ + val string: String? = null, + + /** Numeric value. Spec: OD2c */ + val number: Double? = null, + + /** Boolean value. Spec: OD2c */ + val boolean: Boolean? = null, + + /** Binary value encoded as a base64 string. Spec: OD2c */ + val bytes: String? = null, + + /** JSON object or array value. Spec: OD2c */ + val json: JsonElement? = null, ) /** - * Represents a value that can be a String, Number, Boolean, Binary, JsonObject or JsonArray. - * Provides compile-time type safety through sealed class pattern. - * Spec: OD2c + * Payload for MAP_CREATE operation. + * Spec: MCR* */ -internal sealed class ObjectValue { - abstract val value: Any - - data class String(override val value: kotlin.String) : ObjectValue() - data class Number(override val value: kotlin.Number) : ObjectValue() - data class Boolean(override val value: kotlin.Boolean) : ObjectValue() - data class Binary(override val value: io.ably.lib.objects.Binary) : ObjectValue() - data class JsonObject(override val value: com.google.gson.JsonObject) : ObjectValue() - data class JsonArray(override val value: com.google.gson.JsonArray) : ObjectValue() -} +internal data class MapCreate( + val semantics: ObjectsMapSemantics, // MCR2a + val entries: Map // MCR2b +) /** - * A MapOp describes an operation to be applied to a Map object. - * Spec: OMO1 + * Payload for MAP_SET operation. + * Spec: MST* */ -internal data class ObjectsMapOp( - /** - * The key of the map entry to which the operation should be applied. - * Spec: OMO2a - */ - val key: String, +internal data class MapSet( + val key: String, // MST2a + val value: ObjectData // MST2b - REQUIRED +) - /** - * The data that the map entry should contain if the operation is a MAP_SET operation. - * Spec: OMO2b - */ - val data: ObjectData? = null +/** + * Payload for MAP_REMOVE operation. + * Spec: MRM* + */ +internal data class MapRemove( + val key: String // MRM2a ) /** - * A CounterOp describes an operation to be applied to a Counter object. - * Spec: OCO1 + * Payload for COUNTER_CREATE operation. + * Spec: CCR* */ -internal data class ObjectsCounterOp( - /** - * The data value that should be added to the counter - * Spec: OCO2a - */ - val amount: Double? = null +internal data class CounterCreate( + val count: Double // CCR2a - REQUIRED +) + +/** + * Payload for COUNTER_INC operation. + * Spec: CIN* + */ +internal data class CounterInc( + val number: Double // CIN2a - REQUIRED +) + +/** + * Payload for OBJECT_DELETE operation. + * Spec: ODE* + * No fields - action is sufficient + */ +internal object ObjectDelete + +/** + * Payload for MAP_CREATE_WITH_OBJECT_ID operation. + * Spec: MCRO* + */ +internal data class MapCreateWithObjectId( + val initialValue: String, // MCRO2a + val nonce: String // MCRO2b +) + +/** + * Payload for COUNTER_CREATE_WITH_OBJECT_ID operation. + * Spec: CCRO* + */ +internal data class CounterCreateWithObjectId( + val initialValue: String, // CCRO2a + val nonce: String // CCRO2b ) /** @@ -175,32 +207,52 @@ internal data class ObjectOperation( val objectId: String, /** - * The payload for the operation if it is an operation on a Map object type. - * i.e. MAP_SET, MAP_REMOVE. - * Spec: OOP3c + * Payload for MAP_CREATE operation. + * Spec: OOP3j */ - val mapOp: ObjectsMapOp? = null, + val mapCreate: MapCreate? = null, /** - * The payload for the operation if it is an operation on a Counter object type. - * i.e. COUNTER_INC. - * Spec: OOP3d + * Payload for MAP_SET operation. + * Spec: OOP3k */ - val counterOp: ObjectsCounterOp? = null, + val mapSet: MapSet? = null, /** - * The payload for the operation if the operation is MAP_CREATE. - * Defines the initial value for the Map object. - * Spec: OOP3e + * Payload for MAP_REMOVE operation. + * Spec: OOP3l */ - val map: ObjectsMap? = null, + val mapRemove: MapRemove? = null, /** - * The payload for the operation if the operation is COUNTER_CREATE. - * Defines the initial value for the Counter object. - * Spec: OOP3f + * Payload for COUNTER_CREATE operation. + * Spec: OOP3m */ - val counter: ObjectsCounter? = null, + val counterCreate: CounterCreate? = null, + + /** + * Payload for COUNTER_INC operation. + * Spec: OOP3n + */ + val counterInc: CounterInc? = null, + + /** + * Payload for OBJECT_DELETE operation. + * Spec: OOP3o + */ + val objectDelete: ObjectDelete? = null, + + /** + * Payload for MAP_CREATE_WITH_OBJECT_ID operation. + * Spec: OOP3p + */ + val mapCreateWithObjectId: MapCreateWithObjectId? = null, + + /** + * Payload for COUNTER_CREATE_WITH_OBJECT_ID operation. + * Spec: OOP3q + */ + val counterCreateWithObjectId: CounterCreateWithObjectId? = null, /** * The nonce, must be present on create operations. This is the random part @@ -362,12 +414,17 @@ internal fun ObjectMessage.size(): Int { * Spec: OOP4 */ private fun ObjectOperation.size(): Int { - val mapOpSize = mapOp?.size() ?: 0 // Spec: OOP4b, OMO3 - val counterOpSize = counterOp?.size() ?: 0 // Spec: OOP4c, OCO3 - val mapSize = map?.size() ?: 0 // Spec: OOP4d, OMP4 - val counterSize = counter?.size() ?: 0 // Spec: OOP4e, OCN3 - - return mapOpSize + counterOpSize + mapSize + counterSize + val mapCreateSize = mapCreate?.size() ?: 0 + val mapSetSize = mapSet?.size() ?: 0 + val mapRemoveSize = mapRemove?.size() ?: 0 + val counterCreateSize = counterCreate?.size() ?: 0 + val counterIncSize = counterInc?.size() ?: 0 + val mapCreateWithObjectIdSize = mapCreateWithObjectId?.size() ?: 0 + val counterCreateWithObjectIdSize = counterCreateWithObjectId?.size() ?: 0 + + return mapCreateSize + mapSetSize + mapRemoveSize + + counterCreateSize + counterIncSize + + mapCreateWithObjectIdSize + counterCreateWithObjectIdSize } /** @@ -383,22 +440,52 @@ private fun ObjectState.size(): Int { } /** - * Calculates the size of an ObjectMapOp in bytes. - * Spec: OMO3 + * Calculates the size of a MapCreate payload in bytes. */ -private fun ObjectsMapOp.size(): Int { - val keySize = key.length // Spec: OMO3d - Size of the key - val dataSize = data?.size() ?: 0 // Spec: OMO3b - Size of the data, calculated per "OD3" - return keySize + dataSize +private fun MapCreate.size(): Int { + return entries.entries.sumOf { it.key.length + it.value.size() } } /** - * Calculates the size of a CounterOp in bytes. - * Spec: OCO3 + * Calculates the size of a MapSet payload in bytes. */ -private fun ObjectsCounterOp.size(): Int { - // Size is 8 if amount is a number, 0 if amount is null or omitted - return if (amount != null) 8 else 0 // Spec: OCO3a, OCO3b +private fun MapSet.size(): Int { + return key.length + value.size() +} + +/** + * Calculates the size of a MapRemove payload in bytes. + */ +private fun MapRemove.size(): Int { + return key.length +} + +/** + * Calculates the size of a CounterCreate payload in bytes. + */ +private fun CounterCreate.size(): Int { + return 8 // Double is 8 bytes +} + +/** + * Calculates the size of a CounterInc payload in bytes. + */ +private fun CounterInc.size(): Int { + return 8 // Double is 8 bytes +} + +/** + * Calculates the size of a MapCreateWithObjectId payload in bytes. + */ +private fun MapCreateWithObjectId.size(): Int { + return initialValue.length + nonce.length +} + +/** + * Calculates the size of a CounterCreateWithObjectId payload in bytes. + */ +private fun CounterCreateWithObjectId.size(): Int { + return initialValue.length + nonce.length } /** @@ -437,23 +524,19 @@ private fun ObjectsMapEntry.size(): Int { * Spec: OD3 */ private fun ObjectData.size(): Int { - return value?.size() ?: 0 // Spec: OD3f -} - -/** - * Calculates the size of an ObjectValue in bytes. - * Spec: OD3* - */ -private fun ObjectValue.size(): Int { - return when (this) { - is ObjectValue.Boolean -> 1 // Spec: OD3b - is ObjectValue.Binary -> value.size() // Spec: OD3c - is ObjectValue.Number -> 8 // Spec: OD3d - is ObjectValue.String -> value.byteSize // Spec: OD3e - is ObjectValue.JsonObject, is ObjectValue.JsonArray -> value.toString().byteSize // Spec: OD3e - } + string?.let { return it.byteSize } // Spec: OD3e + number?.let { return 8 } // Spec: OD3d + boolean?.let { return 1 } // Spec: OD3b + bytes?.let { return Base64.getDecoder().decode(it).size } // Spec: OD3c + json?.let { return it.toString().byteSize } // Spec: OD3e + return 0 } internal fun ObjectData?.isInvalid(): Boolean { - return this?.objectId.isNullOrEmpty() && this?.value == null + return this?.objectId.isNullOrEmpty() && + this?.string == null && + this?.number == null && + this?.boolean == null && + this?.bytes == null && + this?.json == null } diff --git a/liveobjects/src/main/kotlin/io/ably/lib/objects/serialization/JsonSerialization.kt b/liveobjects/src/main/kotlin/io/ably/lib/objects/serialization/JsonSerialization.kt index e610ddc6d..fbf5acb88 100644 --- a/liveobjects/src/main/kotlin/io/ably/lib/objects/serialization/JsonSerialization.kt +++ b/liveobjects/src/main/kotlin/io/ably/lib/objects/serialization/JsonSerialization.kt @@ -1,14 +1,11 @@ package io.ably.lib.objects.serialization import com.google.gson.* -import io.ably.lib.objects.Binary 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.ObjectValue import java.lang.reflect.Type -import java.util.* import kotlin.enums.EnumEntries // Gson instance for JSON serialization/deserialization @@ -45,42 +42,26 @@ internal class ObjectDataJsonSerializer : JsonSerializer, JsonDeseri override fun serialize(src: ObjectData, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { val obj = JsonObject() src.objectId?.let { obj.addProperty("objectId", it) } - - src.value?.let { v -> - when (v) { - is ObjectValue.Boolean -> obj.addProperty("boolean", v.value) - is ObjectValue.String -> obj.addProperty("string", v.value) - is ObjectValue.Number -> obj.addProperty("number", v.value.toDouble()) - is ObjectValue.Binary -> obj.addProperty("bytes", Base64.getEncoder().encodeToString(v.value.data)) - is ObjectValue.JsonObject, is ObjectValue.JsonArray -> obj.addProperty("json", v.value.toString()) // Spec: OD4c5 - } - } + 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 value = when { - obj.has("boolean") -> ObjectValue.Boolean(obj.get("boolean").asBoolean) - obj.has("string") -> ObjectValue.String(obj.get("string").asString) - obj.has("number") -> ObjectValue.Number(obj.get("number").asDouble) - obj.has("bytes") -> ObjectValue.Binary(Binary(Base64.getDecoder().decode(obj.get("bytes").asString))) - obj.has("json") -> { - val jsonElement = JsonParser.parseString(obj.get("json").asString) - when { - jsonElement.isJsonObject -> ObjectValue.JsonObject(jsonElement.asJsonObject) - jsonElement.isJsonArray -> ObjectValue.JsonArray(jsonElement.asJsonArray) - else -> throw JsonParseException("Invalid JSON structure") - } - } - else -> { - if (objectId != null) - null - else - throw JsonParseException("Since objectId is not present, at least one of the value fields must be present") - } + 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, value) + return ObjectData(objectId = objectId, string = string, number = number, boolean = boolean, bytes = bytes, json = json) } } diff --git a/liveobjects/src/main/kotlin/io/ably/lib/objects/serialization/MsgpackSerialization.kt b/liveobjects/src/main/kotlin/io/ably/lib/objects/serialization/MsgpackSerialization.kt index 797977a39..0feef39bc 100644 --- a/liveobjects/src/main/kotlin/io/ably/lib/objects/serialization/MsgpackSerialization.kt +++ b/liveobjects/src/main/kotlin/io/ably/lib/objects/serialization/MsgpackSerialization.kt @@ -1,22 +1,28 @@ package io.ably.lib.objects.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.Binary +import io.ably.lib.objects.CounterCreate +import io.ably.lib.objects.CounterCreateWithObjectId +import io.ably.lib.objects.CounterInc import io.ably.lib.objects.ErrorCode +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.ObjectDelete import io.ably.lib.objects.ObjectsMapSemantics import io.ably.lib.objects.ObjectsCounter -import io.ably.lib.objects.ObjectsCounterOp import io.ably.lib.objects.ObjectData import io.ably.lib.objects.ObjectsMap import io.ably.lib.objects.ObjectsMapEntry -import io.ably.lib.objects.ObjectsMapOp 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 io.ably.lib.objects.ObjectValue +import java.util.Base64 import io.ably.lib.util.Serialisation import org.msgpack.core.MessageFormat import org.msgpack.core.MessagePacker @@ -160,10 +166,14 @@ private fun ObjectOperation.writeMsgpack(packer: MessagePacker) { require(objectId.isNotEmpty()) { "objectId must be non-empty per Objects protocol" } fieldCount++ - if (mapOp != null) fieldCount++ - if (counterOp != null) fieldCount++ - if (map != null) fieldCount++ - if (counter != null) 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 (nonce != null) fieldCount++ if (initialValue != null) fieldCount++ @@ -176,24 +186,44 @@ private fun ObjectOperation.writeMsgpack(packer: MessagePacker) { packer.packString("objectId") packer.packString(objectId) - if (mapOp != null) { - packer.packString("mapOp") - mapOp.writeMsgpack(packer) + if (mapCreate != null) { + packer.packString("mapCreate") + mapCreate.writeMsgpack(packer) } - if (counterOp != null) { - packer.packString("counterOp") - counterOp.writeMsgpack(packer) + if (mapSet != null) { + packer.packString("mapSet") + mapSet.writeMsgpack(packer) } - if (map != null) { - packer.packString("map") - map.writeMsgpack(packer) + if (mapRemove != null) { + packer.packString("mapRemove") + mapRemove.writeMsgpack(packer) } - if (counter != null) { - packer.packString("counter") - counter.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 (nonce != null) { @@ -215,10 +245,14 @@ private fun readObjectOperation(unpacker: MessageUnpacker): ObjectOperation { var action: ObjectOperationAction? = null var objectId: String = "" - var mapOp: ObjectsMapOp? = null - var counterOp: ObjectsCounterOp? = null - var map: ObjectsMap? = null - var counter: ObjectsCounter? = null + 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 nonce: String? = null var initialValue: String? = null @@ -239,10 +273,17 @@ private fun readObjectOperation(unpacker: MessageUnpacker): ObjectOperation { ?: throw objectError("Unknown ObjectOperationAction code: $actionCode and no Unknown fallback found") } "objectId" -> objectId = unpacker.unpackString() - "mapOp" -> mapOp = readObjectMapOp(unpacker) - "counterOp" -> counterOp = readObjectCounterOp(unpacker) - "map" -> map = readObjectMap(unpacker) - "counter" -> counter = readObjectCounter(unpacker) + "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) "nonce" -> nonce = unpacker.unpackString() "initialValue" -> initialValue = unpacker.unpackString() else -> unpacker.skipValue() @@ -256,10 +297,14 @@ private fun readObjectOperation(unpacker: MessageUnpacker): ObjectOperation { return ObjectOperation( action = action, objectId = objectId, - mapOp = mapOp, - counterOp = counterOp, - map = map, - counter = counter, + mapCreate = mapCreate, + mapSet = mapSet, + mapRemove = mapRemove, + counterCreate = counterCreate, + counterInc = counterInc, + objectDelete = objectDelete, + mapCreateWithObjectId = mapCreateWithObjectId, + counterCreateWithObjectId = counterCreateWithObjectId, nonce = nonce, initialValue = initialValue, ) @@ -359,92 +404,240 @@ private fun readObjectState(unpacker: MessageUnpacker): ObjectState { } /** - * Write ObjectMapOp to MessagePacker + * Write MapCreate to MessagePacker */ -private fun ObjectsMapOp.writeMsgpack(packer: MessagePacker) { - var fieldCount = 1 // key is required +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) + } +} - if (data != null) fieldCount++ +/** + * Read MapCreate from MessageUnpacker + */ +private fun readMapCreate(unpacker: MessageUnpacker): MapCreate { + val fieldCount = unpacker.unpackMapHeader() + var semantics: ObjectsMapSemantics = ObjectsMapSemantics.LWW + var entries: Map = emptyMap() - packer.packMapHeader(fieldCount) + 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) - - if (data != null) { - packer.packString("data") - data.writeMsgpack(packer) - } + packer.packString("value") + value.writeMsgpack(packer) } /** - * Read ObjectMapOp from MessageUnpacker + * Read MapSet from MessageUnpacker */ -private fun readObjectMapOp(unpacker: MessageUnpacker): ObjectsMapOp { +private fun readMapSet(unpacker: MessageUnpacker): MapSet { val fieldCount = unpacker.unpackMapHeader() - - var key = "" - var data: ObjectData? = null + 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 + 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() - "data" -> data = readObjectData(unpacker) else -> unpacker.skipValue() } } + return MapRemove(key = key ?: throw objectError("Missing 'key' in MapRemove payload")) +} - return ObjectsMapOp(key = key, data = data) +/** + * Write CounterCreate to MessagePacker + */ +private fun CounterCreate.writeMsgpack(packer: MessagePacker) { + packer.packMapHeader(1) + packer.packString("count") + packer.packDouble(count) } /** - * Write ObjectCounterOp to MessagePacker + * Read CounterCreate from MessageUnpacker */ -private fun ObjectsCounterOp.writeMsgpack(packer: MessagePacker) { - var fieldCount = 0 +private fun readCounterCreate(unpacker: MessageUnpacker): CounterCreate { + val fieldCount = unpacker.unpackMapHeader() + var count: Double? = null - if (amount != null) fieldCount++ + 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")) +} - packer.packMapHeader(fieldCount) +/** + * Write CounterInc to MessagePacker + */ +private fun CounterInc.writeMsgpack(packer: MessagePacker) { + packer.packMapHeader(1) + packer.packString("number") + packer.packDouble(number) +} - if (amount != null) { - packer.packString("amount") - packer.packDouble(amount) +/** + * 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")) } /** - * Read ObjectCounterOp from MessageUnpacker + * Write MapCreateWithObjectId to MessagePacker */ -private fun readObjectCounterOp(unpacker: MessageUnpacker): ObjectsCounterOp { - val fieldCount = unpacker.unpackMapHeader() +private fun MapCreateWithObjectId.writeMsgpack(packer: MessagePacker) { + packer.packMapHeader(2) + packer.packString("initialValue") + packer.packString(initialValue) + packer.packString("nonce") + packer.packString(nonce) +} - var amount: Double? = null +/** + * 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 + 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) { - "amount" -> amount = unpacker.unpackDouble() + "initialValue" -> initialValue = unpacker.unpackString() + "nonce" -> nonce = unpacker.unpackString() else -> unpacker.skipValue() } } - - return ObjectsCounterOp(amount = amount) + return CounterCreateWithObjectId( + initialValue = initialValue ?: throw objectError("Missing 'initialValue' in CounterCreateWithObjectId payload"), + nonce = nonce ?: throw objectError("Missing 'nonce' in CounterCreateWithObjectId payload") + ) } /** @@ -630,9 +823,11 @@ private fun ObjectData.writeMsgpack(packer: MessagePacker) { var fieldCount = 0 if (objectId != null) fieldCount++ - value?.let { - fieldCount++ - } + if (string != null) fieldCount++ + if (number != null) fieldCount++ + if (boolean != null) fieldCount++ + if (bytes != null) fieldCount++ + if (json != null) fieldCount++ packer.packMapHeader(fieldCount) @@ -641,34 +836,31 @@ private fun ObjectData.writeMsgpack(packer: MessagePacker) { packer.packString(objectId) } - value?.let { v -> - when (v) { - is ObjectValue.Boolean -> { - packer.packString("boolean") - packer.packBoolean(v.value) - } - is ObjectValue.String -> { - packer.packString("string") - packer.packString(v.value) - } - is ObjectValue.Number -> { - packer.packString("number") - packer.packDouble(v.value.toDouble()) - } - is ObjectValue.Binary -> { - packer.packString("bytes") - packer.packBinaryHeader(v.value.data.size) - packer.writePayload(v.value.data) - } - is ObjectValue.JsonObject -> { - packer.packString("json") - packer.packString(v.value.toString()) - } - is ObjectValue.JsonArray -> { - packer.packString("json") - packer.packString(v.value.toString()) - } - } + 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()) } } @@ -678,7 +870,11 @@ private fun ObjectData.writeMsgpack(packer: MessagePacker) { private fun readObjectData(unpacker: MessageUnpacker): ObjectData { val fieldCount = unpacker.unpackMapHeader() var objectId: String? = null - var value: ObjectValue? = 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() @@ -691,28 +887,19 @@ private fun readObjectData(unpacker: MessageUnpacker): ObjectData { when (fieldName) { "objectId" -> objectId = unpacker.unpackString() - "boolean" -> value = ObjectValue.Boolean(unpacker.unpackBoolean()) - "string" -> value = ObjectValue.String(unpacker.unpackString()) - "number" -> value = ObjectValue.Number(unpacker.unpackDouble()) + "string" -> string = unpacker.unpackString() + "number" -> number = unpacker.unpackDouble() + "boolean" -> boolean = unpacker.unpackBoolean() "bytes" -> { val size = unpacker.unpackBinaryHeader() - val bytes = ByteArray(size) - unpacker.readPayload(bytes) - value = ObjectValue.Binary(Binary(bytes)) - } - "json" -> { - val jsonString = unpacker.unpackString() - val parsed = JsonParser.parseString(jsonString) - value = when { - parsed.isJsonObject -> ObjectValue.JsonObject(parsed.asJsonObject) - parsed.isJsonArray -> ObjectValue.JsonArray(parsed.asJsonArray) - else -> - throw ablyException("Invalid JSON string for json field", ErrorCode.MapValueDataTypeUnsupported, HttpStatusCode.InternalServerError) - } + 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, value = value) + return ObjectData(objectId = objectId, string = string, number = number, boolean = boolean, bytes = bytes, json = json) } diff --git a/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt b/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt index 164cdb28a..87c1de3b5 100644 --- a/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt +++ b/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt @@ -1,6 +1,8 @@ package io.ably.lib.objects.type.livecounter import io.ably.lib.objects.* +import io.ably.lib.objects.CounterCreate +import io.ably.lib.objects.CounterInc import io.ably.lib.objects.ObjectOperation import io.ably.lib.objects.ObjectState import io.ably.lib.objects.type.BaseRealtimeObject @@ -81,7 +83,7 @@ internal class DefaultLiveCounter private constructor( operation = ObjectOperation( action = ObjectOperationAction.CounterInc, objectId = objectId, - counterOp = ObjectsCounterOp(amount = amount) + counterInc = CounterInc(number = amount) ) ) @@ -124,13 +126,11 @@ internal class DefaultLiveCounter private constructor( } /** - * Creates initial value operation for counter creation. + * Creates initial value payload for counter creation. * Spec: RTO12f2 */ - internal fun initialValue(count: Number): CounterCreatePayload { - return CounterCreatePayload( - counter = ObjectsCounter(count = count.toDouble()) - ) + internal fun initialValue(count: Number): CounterCreate { + return CounterCreate(count = count.toDouble()) } } } diff --git a/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/LiveCounterManager.kt b/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/LiveCounterManager.kt index 943faf4ce..d4e8b349a 100644 --- a/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/LiveCounterManager.kt +++ b/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/LiveCounterManager.kt @@ -1,6 +1,7 @@ package io.ably.lib.objects.type.livecounter import io.ably.lib.objects.* +import io.ably.lib.objects.CounterInc import io.ably.lib.objects.ObjectOperation import io.ably.lib.objects.ObjectOperationAction import io.ably.lib.objects.ObjectState @@ -47,8 +48,8 @@ internal class LiveCounterManager(private val liveCounter: DefaultLiveCounter): true // RTLC7d1b } ObjectOperationAction.CounterInc -> { - if (operation.counterOp != null) { - val update = applyCounterInc(operation.counterOp) // RTLC7d2 + if (operation.counterInc != null) { + val update = applyCounterInc(operation.counterInc) // RTLC7d2 liveCounter.notifyUpdated(update) // RTLC7d2a true // RTLC7d2b } else { @@ -89,8 +90,8 @@ internal class LiveCounterManager(private val liveCounter: DefaultLiveCounter): /** * @spec RTLC9 - Applies counter increment operation */ - private fun applyCounterInc(counterOp: ObjectsCounterOp): LiveCounterUpdate { - val amount = counterOp.amount ?: 0.0 + private fun applyCounterInc(counterInc: CounterInc): LiveCounterUpdate { + val amount = counterInc.number val previousValue = liveCounter.data.get() liveCounter.data.set(previousValue + amount) // RTLC9b return LiveCounterUpdate(amount) @@ -108,7 +109,7 @@ internal class LiveCounterManager(private val liveCounter: DefaultLiveCounter): // note that it is intentional to SUM the incoming count from the create op. // if we got here, it means that current counter instance is missing the initial value in its data reference, // which we're going to add now. - val count = operation.counter?.count ?: 0.0 + val count = operation.counterCreate?.count ?: 0.0 val previousValue = liveCounter.data.get() liveCounter.data.set(previousValue + count) // RTLC10a liveCounter.createOperationIsMerged = true // RTLC10b diff --git a/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/DefaultLiveMap.kt b/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/DefaultLiveMap.kt index cd0604dbf..e2da9461b 100644 --- a/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/DefaultLiveMap.kt +++ b/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/DefaultLiveMap.kt @@ -1,6 +1,9 @@ package io.ably.lib.objects.type.livemap import io.ably.lib.objects.* +import io.ably.lib.objects.MapCreate +import io.ably.lib.objects.MapRemove +import io.ably.lib.objects.MapSet import io.ably.lib.objects.ObjectsMapSemantics import io.ably.lib.objects.ObjectMessage import io.ably.lib.objects.ObjectOperation @@ -15,6 +18,7 @@ import io.ably.lib.objects.type.map.LiveMapValue import io.ably.lib.objects.type.noOp import io.ably.lib.util.Log import kotlinx.coroutines.runBlocking +import java.util.Base64 import java.util.concurrent.ConcurrentHashMap import java.util.AbstractMap @@ -128,9 +132,9 @@ internal class DefaultLiveMap private constructor( operation = ObjectOperation( action = ObjectOperationAction.MapSet, objectId = objectId, - mapOp = ObjectsMapOp( + mapSet = MapSet( key = keyName, - data = fromLiveMapValue(value) + value = fromLiveMapValue(value) ) ) ) @@ -153,7 +157,7 @@ internal class DefaultLiveMap private constructor( operation = ObjectOperation( action = ObjectOperationAction.MapRemove, objectId = objectId, - mapOp = ObjectsMapOp(key = keyName) + mapRemove = MapRemove(key = keyName) ) ) @@ -196,20 +200,18 @@ internal class DefaultLiveMap private constructor( } /** - * Creates an ObjectMap from map entries. + * Creates a MapCreate payload from map entries. * Spec: RTO11f4 */ - internal fun initialValue(entries: MutableMap): MapCreatePayload { - return MapCreatePayload( - map = ObjectsMap( - semantics = ObjectsMapSemantics.LWW, - entries = entries.mapValues { (_, value) -> - ObjectsMapEntry( - tombstone = false, - data = fromLiveMapValue(value) - ) - } - ) + internal fun initialValue(entries: MutableMap): MapCreate { + return MapCreate( + semantics = ObjectsMapSemantics.LWW, + entries = entries.mapValues { (_, value) -> + ObjectsMapEntry( + tombstone = false, + data = fromLiveMapValue(value) + ) + } ) } @@ -218,30 +220,22 @@ internal class DefaultLiveMap private constructor( */ private fun fromLiveMapValue(value: LiveMapValue): ObjectData { return when { - value.isLiveMap || value.isLiveCounter -> { + value.isLiveMap || value.isLiveCounter -> ObjectData(objectId = (value.value as BaseRealtimeObject).objectId) - } - value.isBoolean -> { - ObjectData(value = ObjectValue.Boolean(value.asBoolean)) - } - value.isBinary -> { - ObjectData(value = ObjectValue.Binary(Binary(value.asBinary))) - } - value.isNumber -> { - ObjectData(value = ObjectValue.Number(value.asNumber)) - } - value.isString -> { - ObjectData(value = ObjectValue.String(value.asString)) - } - value.isJsonObject -> { - ObjectData(value = ObjectValue.JsonObject(value.asJsonObject)) - } - value.isJsonArray -> { - ObjectData(value = ObjectValue.JsonArray(value.asJsonArray)) - } - else -> { + value.isBoolean -> + ObjectData(boolean = value.asBoolean) + value.isBinary -> + ObjectData(bytes = Base64.getEncoder().encodeToString(value.asBinary)) + value.isNumber -> + ObjectData(number = value.asNumber.toDouble()) + value.isString -> + ObjectData(string = value.asString) + value.isJsonObject -> + ObjectData(json = value.asJsonObject) + value.isJsonArray -> + ObjectData(json = value.asJsonArray) + else -> throw IllegalArgumentException("Unsupported value type") - } } } } diff --git a/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/LiveMapEntry.kt b/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/LiveMapEntry.kt index 4c32366e1..df2259583 100644 --- a/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/LiveMapEntry.kt +++ b/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/LiveMapEntry.kt @@ -9,6 +9,7 @@ import io.ably.lib.objects.type.ObjectType import io.ably.lib.objects.type.counter.LiveCounter import io.ably.lib.objects.type.map.LiveMap import io.ably.lib.objects.type.map.LiveMapValue +import java.util.Base64 /** * @spec RTLM3 - Map data structure storing entries @@ -45,14 +46,25 @@ internal fun LiveMapEntry.isEntryOrRefTombstoned(objectsPool: ObjectsPool): Bool internal fun LiveMapEntry.getResolvedValue(objectsPool: ObjectsPool): LiveMapValue? { if (isTombstoned) { return null } // RTLM5d2a - data?.value?.let { return fromObjectValue(it) } // RTLM5d2b, RTLM5d2c, RTLM5d2d, RTLM5d2e - - data?.objectId?.let { refId -> // RTLM5d2f -has an objectId reference - objectsPool.get(refId)?.let { refObject -> - if (refObject.isTombstoned) { - return null // tombstoned objects must not be surfaced to the end users + data?.let { d -> // RTLM5d2b, RTLM5d2c, RTLM5d2d, RTLM5d2e + d.string?.let { return LiveMapValue.of(it) } + d.number?.let { return LiveMapValue.of(it) } + d.boolean?.let { return LiveMapValue.of(it) } + d.bytes?.let { return LiveMapValue.of(Base64.getDecoder().decode(it)) } + d.json?.let { parsed -> + return when { + parsed.isJsonObject -> LiveMapValue.of(parsed.asJsonObject) + parsed.isJsonArray -> LiveMapValue.of(parsed.asJsonArray) + else -> null + } + } + d.objectId?.let { refId -> // RTLM5d2f - has an objectId reference + objectsPool.get(refId)?.let { refObject -> + if (refObject.isTombstoned) { + return null // tombstoned objects must not be surfaced to the end users + } + return fromRealtimeObject(refObject) // RTLM5d2f2 } - return fromRealtimeObject(refObject) // RTLM5d2f2 } } return null // RTLM5d2g, RTLM5d2f1 @@ -66,17 +78,6 @@ internal fun LiveMapEntry.isEligibleForGc(): Boolean { return isTombstoned && tombstonedAt?.let { currentTime - it >= ObjectsPoolDefaults.GC_GRACE_PERIOD_MS } == true } -private fun fromObjectValue(objValue: ObjectValue): LiveMapValue { - return when (objValue) { - is ObjectValue.String -> LiveMapValue.of(objValue.value) - is ObjectValue.Number -> LiveMapValue.of(objValue.value) - is ObjectValue.Boolean -> LiveMapValue.of(objValue.value) - is ObjectValue.Binary -> LiveMapValue.of(objValue.value.data) - is ObjectValue.JsonObject -> LiveMapValue.of(objValue.value) - is ObjectValue.JsonArray -> LiveMapValue.of(objValue.value) - } -} - private fun fromRealtimeObject(realtimeObject: BaseRealtimeObject): LiveMapValue { return when (realtimeObject.objectType) { ObjectType.Map -> LiveMapValue.of(realtimeObject as LiveMap) diff --git a/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/LiveMapManager.kt b/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/LiveMapManager.kt index 90c920cf2..0a5dd11fe 100644 --- a/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/LiveMapManager.kt +++ b/liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/LiveMapManager.kt @@ -1,7 +1,9 @@ package io.ably.lib.objects.type.livemap +import io.ably.lib.objects.MapCreate +import io.ably.lib.objects.MapRemove +import io.ably.lib.objects.MapSet import io.ably.lib.objects.ObjectsMapSemantics -import io.ably.lib.objects.ObjectsMapOp import io.ably.lib.objects.ObjectOperation import io.ably.lib.objects.ObjectOperationAction import io.ably.lib.objects.ObjectState @@ -59,8 +61,8 @@ internal class LiveMapManager(private val liveMap: DefaultLiveMap): LiveMapChang true // RTLM15d1b } ObjectOperationAction.MapSet -> { - if (operation.mapOp != null) { - val update = applyMapSet(operation.mapOp, serial) // RTLM15d2 + if (operation.mapSet != null) { + val update = applyMapSet(operation.mapSet, serial) // RTLM15d2 liveMap.notifyUpdated(update) // RTLM15d2a true // RTLM15d2b } else { @@ -68,8 +70,8 @@ internal class LiveMapManager(private val liveMap: DefaultLiveMap): LiveMapChang } } ObjectOperationAction.MapRemove -> { - if (operation.mapOp != null) { - val update = applyMapRemove(operation.mapOp, serial, serialTimestamp) // RTLM15d3 + if (operation.mapRemove != null) { + val update = applyMapRemove(operation.mapRemove, serial, serialTimestamp) // RTLM15d3 liveMap.notifyUpdated(update) // RTLM15d3a true // RTLM15d3b } else { @@ -104,7 +106,7 @@ internal class LiveMapManager(private val liveMap: DefaultLiveMap): LiveMapChang return noOpMapUpdate } - validateMapSemantics(operation.map?.semantics) // RTLM16c + validateMapSemantics(operation.mapCreate?.semantics) // RTLM16c return mergeInitialDataFromCreateOperation(operation) // RTLM16d } @@ -113,27 +115,27 @@ internal class LiveMapManager(private val liveMap: DefaultLiveMap): LiveMapChang * @spec RTLM7 - Applies MAP_SET operation to LiveMap */ private fun applyMapSet( - mapOp: ObjectsMapOp, // RTLM7d1 + mapSet: MapSet, // RTLM7d1 timeSerial: String?, // RTLM7d2 ): LiveMapUpdate { - val existingEntry = liveMap.data[mapOp.key] + val existingEntry = liveMap.data[mapSet.key] // RTLM7a if (existingEntry != null && !canApplyMapOperation(existingEntry.timeserial, timeSerial)) { // RTLM7a1 - the operation's serial <= the entry's serial, ignore the operation Log.v(tag, - "Skipping update for key=\"${mapOp.key}\": op serial $timeSerial <= entry serial ${existingEntry.timeserial};" + + "Skipping update for key=\"${mapSet.key}\": op serial $timeSerial <= entry serial ${existingEntry.timeserial};" + " objectId=${objectId}" ) return noOpMapUpdate } - if (mapOp.data.isInvalid()) { - throw objectError("Invalid object data for MAP_SET op on objectId=${objectId} on key=${mapOp.key}") + if (mapSet.value.isInvalid()) { + throw objectError("Invalid object data for MAP_SET op on objectId=${objectId} on key=${mapSet.key}") } // RTLM7c - mapOp.data?.objectId?.let { + mapSet.value.objectId?.let { // this MAP_SET op is setting a key to point to another object via its object id, // but it is possible that we don't have the corresponding object in the pool yet (for example, we haven't seen the *_CREATE op for it). // we don't want to return undefined from this map's .get() method even if we don't have the object, @@ -143,39 +145,39 @@ internal class LiveMapManager(private val liveMap: DefaultLiveMap): LiveMapChang if (existingEntry != null) { // RTLM7a2 - Replace existing entry with new one instead of mutating - liveMap.data[mapOp.key] = LiveMapEntry( + liveMap.data[mapSet.key] = LiveMapEntry( isTombstoned = false, // RTLM7a2c timeserial = timeSerial, // RTLM7a2b - data = mapOp.data // RTLM7a2a + data = mapSet.value // RTLM7a2a ) } else { // RTLM7b, RTLM7b1 - liveMap.data[mapOp.key] = LiveMapEntry( + liveMap.data[mapSet.key] = LiveMapEntry( isTombstoned = false, // RTLM7b2 timeserial = timeSerial, - data = mapOp.data + data = mapSet.value ) } - return LiveMapUpdate(mapOf(mapOp.key to LiveMapUpdate.Change.UPDATED)) + return LiveMapUpdate(mapOf(mapSet.key to LiveMapUpdate.Change.UPDATED)) } /** * @spec RTLM8 - Applies MAP_REMOVE operation to LiveMap */ private fun applyMapRemove( - mapOp: ObjectsMapOp, // RTLM8c1 + mapRemove: MapRemove, // RTLM8c1 timeSerial: String?, // RTLM8c2 timeStamp: Long?, // RTLM8c3 ): LiveMapUpdate { - val existingEntry = liveMap.data[mapOp.key] + val existingEntry = liveMap.data[mapRemove.key] // RTLM8a if (existingEntry != null && !canApplyMapOperation(existingEntry.timeserial, timeSerial)) { // RTLM8a1 - the operation's serial <= the entry's serial, ignore the operation Log.v( tag, - "Skipping remove for key=\"${mapOp.key}\": op serial $timeSerial <= entry serial ${existingEntry.timeserial}; " + + "Skipping remove for key=\"${mapRemove.key}\": op serial $timeSerial <= entry serial ${existingEntry.timeserial}; " + "objectId=${objectId}" ) return noOpMapUpdate @@ -184,7 +186,7 @@ internal class LiveMapManager(private val liveMap: DefaultLiveMap): LiveMapChang val tombstonedAt = if (timeStamp != null) timeStamp else { Log.w( tag, - "No timestamp provided for MAP_REMOVE op on key=\"${mapOp.key}\"; using current time as tombstone time; " + + "No timestamp provided for MAP_REMOVE op on key=\"${mapRemove.key}\"; using current time as tombstone time; " + "objectId=${objectId}" ) System.currentTimeMillis() @@ -192,7 +194,7 @@ internal class LiveMapManager(private val liveMap: DefaultLiveMap): LiveMapChang if (existingEntry != null) { // RTLM8a2 - Replace existing entry with new one instead of mutating - liveMap.data[mapOp.key] = LiveMapEntry( + liveMap.data[mapRemove.key] = LiveMapEntry( isTombstoned = true, // RTLM8a2c tombstonedAt = tombstonedAt, timeserial = timeSerial, // RTLM8a2b @@ -200,14 +202,14 @@ internal class LiveMapManager(private val liveMap: DefaultLiveMap): LiveMapChang ) } else { // RTLM8b, RTLM8b1 - liveMap.data[mapOp.key] = LiveMapEntry( + liveMap.data[mapRemove.key] = LiveMapEntry( isTombstoned = true, // RTLM8b2 tombstonedAt = tombstonedAt, timeserial = timeSerial ) } - return LiveMapUpdate(mapOf(mapOp.key to LiveMapUpdate.Change.REMOVED)) + return LiveMapUpdate(mapOf(mapRemove.key to LiveMapUpdate.Change.REMOVED)) } /** @@ -232,7 +234,7 @@ internal class LiveMapManager(private val liveMap: DefaultLiveMap): LiveMapChang * @spec RTLM17 - Merges initial data from create operation */ private fun mergeInitialDataFromCreateOperation(operation: ObjectOperation): LiveMapUpdate { - if (operation.map?.entries.isNullOrEmpty()) { // no map entries in MAP_CREATE op + if (operation.mapCreate?.entries.isNullOrEmpty()) { // no map entries in MAP_CREATE op return noOpMapUpdate } @@ -241,15 +243,15 @@ internal class LiveMapManager(private val liveMap: DefaultLiveMap): LiveMapChang // RTLM17a // in order to apply MAP_CREATE op for an existing map, we should merge their underlying entries keys. // we can do this by iterating over entries from MAP_CREATE op and apply changes on per-key basis as if we had MAP_SET, MAP_REMOVE operations. - operation.map?.entries?.forEach { (key, entry) -> + operation.mapCreate?.entries?.forEach { (key, entry) -> // for a MAP_CREATE operation we must use the serial value available on an entry, instead of a serial on a message val opTimeserial = entry.timeserial val update = if (entry.tombstone == true) { // RTLM17a2 - entry in MAP_CREATE op is removed, try to apply MAP_REMOVE op - applyMapRemove(ObjectsMapOp(key), opTimeserial, entry.serialTimestamp) + applyMapRemove(MapRemove(key), opTimeserial, entry.serialTimestamp) } else { // RTLM17a1 - entry in MAP_CREATE op is not removed, try to set it via MAP_SET op - applyMapSet(ObjectsMapOp(key, entry.data), opTimeserial) + applyMapSet(MapSet(key, entry.data ?: return@forEach), opTimeserial) } // skip noop updates @@ -327,7 +329,7 @@ internal class LiveMapManager(private val liveMap: DefaultLiveMap): LiveMapChang state.createOp?.let { createOp -> liveMap.validateObjectId(createOp.objectId) validateMapCreateAction(createOp.action) - validateMapSemantics(createOp.map?.semantics) + validateMapSemantics(createOp.mapCreate?.semantics) } } diff --git a/liveobjects/src/test/kotlin/io/ably/lib/objects/integration/DefaultLiveMapTest.kt b/liveobjects/src/test/kotlin/io/ably/lib/objects/integration/DefaultLiveMapTest.kt index e3043abc1..0f2abb567 100644 --- a/liveobjects/src/test/kotlin/io/ably/lib/objects/integration/DefaultLiveMapTest.kt +++ b/liveobjects/src/test/kotlin/io/ably/lib/objects/integration/DefaultLiveMapTest.kt @@ -2,7 +2,6 @@ package io.ably.lib.objects.integration import io.ably.lib.objects.* import io.ably.lib.objects.ObjectData -import io.ably.lib.objects.ObjectValue import io.ably.lib.objects.integration.helpers.fixtures.createUserMapObject import io.ably.lib.objects.integration.helpers.fixtures.createUserProfileMapObject import io.ably.lib.objects.integration.setup.IntegrationTest @@ -112,9 +111,9 @@ class DefaultLiveMapTest: IntegrationTest() { val testMapObjectId = restObjects.createMap( channelName, data = mapOf( - "name" to ObjectData(value = ObjectValue.String("Alice")), - "age" to ObjectData(value = ObjectValue.Number(30)), - "isActive" to ObjectData(value = ObjectValue.Boolean(true)) + "name" to ObjectData(string = "Alice"), + "age" to ObjectData(number = 30.0), + "isActive" to ObjectData(boolean = true) ) ) restObjects.setMapRef(channelName, "root", "testMap", testMapObjectId) @@ -131,7 +130,7 @@ class DefaultLiveMapTest: IntegrationTest() { assertEquals(true, testMap.get("isActive")?.asBoolean, "Initial active status should be true") // Step 2: Update an existing field (name from "Alice" to "Bob") - restObjects.setMapValue(channelName, testMapObjectId, "name", ObjectValue.String("Bob")) + restObjects.setMapValue(channelName, testMapObjectId, "name", ObjectData(string = "Bob")) // Wait for the map to be updated assertWaiter { testMap.get("name")?.asString == "Bob" } @@ -142,7 +141,7 @@ class DefaultLiveMapTest: IntegrationTest() { assertEquals(true, testMap.get("isActive")?.asBoolean, "Active status should remain unchanged") // Step 3: Add a new field (email) - restObjects.setMapValue(channelName, testMapObjectId, "email", ObjectValue.String("bob@example.com")) + restObjects.setMapValue(channelName, testMapObjectId, "email", ObjectData(string = "bob@example.com")) // Wait for the map to be updated assertWaiter { testMap.get("email")?.asString == "bob@example.com" } @@ -154,7 +153,7 @@ class DefaultLiveMapTest: IntegrationTest() { assertEquals("bob@example.com", testMap.get("email")?.asString, "Email should be added successfully") // Step 4: Add another new field with different data type (score as number) - restObjects.setMapValue(channelName, testMapObjectId, "score", ObjectValue.Number(85)) + restObjects.setMapValue(channelName, testMapObjectId, "score", ObjectData(number = 85.0)) // Wait for the map to be updated assertWaiter { testMap.get("score")?.asNumber == 85.0 } @@ -167,7 +166,7 @@ class DefaultLiveMapTest: IntegrationTest() { assertEquals(85.0, testMap.get("score")?.asNumber, "Score should be added as numeric value") // Step 5: Update the boolean field - restObjects.setMapValue(channelName, testMapObjectId, "isActive", ObjectValue.Boolean(false)) + restObjects.setMapValue(channelName, testMapObjectId, "isActive", ObjectData(boolean = false)) // Wait for the map to be updated assertWaiter { testMap.get("isActive")?.asBoolean == false } @@ -357,7 +356,7 @@ class DefaultLiveMapTest: IntegrationTest() { val userProfileSubscription = userProfile.subscribe { update -> userProfileUpdates.add(update) } // Step 1: Update an existing field in the user profile map (change the name) - restObjects.setMapValue(channelName, userProfileObjectId, "name", ObjectValue.String("Bob Smith")) + restObjects.setMapValue(channelName, userProfileObjectId, "name", ObjectData(string = "Bob Smith")) // Wait for the update to be received assertWaiter { userProfileUpdates.isNotEmpty() } @@ -374,7 +373,7 @@ class DefaultLiveMapTest: IntegrationTest() { // Step 2: Update another field in the user profile map (change the email) userProfileUpdates.clear() - restObjects.setMapValue(channelName, userProfileObjectId, "email", ObjectValue.String("bob@example.com")) + restObjects.setMapValue(channelName, userProfileObjectId, "email", ObjectData(string = "bob@example.com")) // Wait for the second update assertWaiter { userProfileUpdates.isNotEmpty() } @@ -414,7 +413,7 @@ class DefaultLiveMapTest: IntegrationTest() { userProfileUpdates.clear() userProfileSubscription.unsubscribe() // No updates should be received after unsubscribing - restObjects.setMapValue(channelName, userProfileObjectId, "country", ObjectValue.String("uk")) + restObjects.setMapValue(channelName, userProfileObjectId, "country", ObjectData(string = "uk")) // Wait for a moment to ensure no updates are received assertWaiter { userProfile.size() == 4L } diff --git a/liveobjects/src/test/kotlin/io/ably/lib/objects/integration/helpers/RestObjects.kt b/liveobjects/src/test/kotlin/io/ably/lib/objects/integration/helpers/RestObjects.kt index 165563bd2..d06559377 100644 --- a/liveobjects/src/test/kotlin/io/ably/lib/objects/integration/helpers/RestObjects.kt +++ b/liveobjects/src/test/kotlin/io/ably/lib/objects/integration/helpers/RestObjects.kt @@ -2,7 +2,6 @@ package io.ably.lib.objects.integration.helpers import com.google.gson.JsonObject import io.ably.lib.objects.ObjectData -import io.ably.lib.objects.ObjectValue import io.ably.lib.rest.AblyRest import io.ably.lib.http.HttpUtils import io.ably.lib.objects.integration.helpers.fixtures.DataFixtures @@ -28,8 +27,7 @@ internal class RestObjects(options: ClientOptions) { /** * Sets a value (primitives, JsonObject, JsonArray, etc.) at the specified key in an existing map. */ - internal fun setMapValue(channelName: String, mapObjectId: String, key: String, value: ObjectValue) { - val data = ObjectData(value = value) + internal fun setMapValue(channelName: String, mapObjectId: String, key: String, data: ObjectData) { val mapCreateOp = PayloadBuilder.mapSetRestOp(mapObjectId, key, data) operationRequest(channelName, mapCreateOp) } diff --git a/liveobjects/src/test/kotlin/io/ably/lib/objects/integration/helpers/fixtures/DataFixtures.kt b/liveobjects/src/test/kotlin/io/ably/lib/objects/integration/helpers/fixtures/DataFixtures.kt index 18928cd19..f6f305aba 100644 --- a/liveobjects/src/test/kotlin/io/ably/lib/objects/integration/helpers/fixtures/DataFixtures.kt +++ b/liveobjects/src/test/kotlin/io/ably/lib/objects/integration/helpers/fixtures/DataFixtures.kt @@ -2,53 +2,52 @@ package io.ably.lib.objects.integration.helpers.fixtures import com.google.gson.JsonArray import com.google.gson.JsonObject -import io.ably.lib.objects.Binary import io.ably.lib.objects.ObjectData -import io.ably.lib.objects.ObjectValue +import java.util.Base64 internal object DataFixtures { /** Test fixture for string value ("stringValue") data type */ - internal val stringData = ObjectData(value = ObjectValue.String("stringValue")) + internal val stringData = ObjectData(string = "stringValue") /** Test fixture for empty string data type */ - internal val emptyStringData = ObjectData(value = ObjectValue.String("")) + internal val emptyStringData = ObjectData(string = "") /** Test fixture for binary data containing encoded JSON */ internal val bytesData = ObjectData( - value = ObjectValue.Binary(Binary("eyJwcm9kdWN0SWQiOiAiMDAxIiwgInByb2R1Y3ROYW1lIjogImNhciJ9".toByteArray()))) + bytes = Base64.getEncoder().encodeToString("eyJwcm9kdWN0SWQiOiAiMDAxIiwgInByb2R1Y3ROYW1lIjogImNhciJ9".toByteArray())) /** Test fixture for empty binary data (zero-length byte array) */ - internal val emptyBytesData = ObjectData(value = ObjectValue.Binary(Binary(ByteArray(0)))) + internal val emptyBytesData = ObjectData(bytes = Base64.getEncoder().encodeToString(ByteArray(0))) /** Test fixture for maximum safe number value */ - internal val maxSafeNumberData = ObjectData(value = ObjectValue.Number(99999999.0)) + internal val maxSafeNumberData = ObjectData(number = 99999999.0) /** Test fixture for minimum safe number value */ - internal val negativeMaxSafeNumberData = ObjectData(value = ObjectValue.Number(-99999999.0)) + internal val negativeMaxSafeNumberData = ObjectData(number = -99999999.0) /** Test fixture for positive number value (1) */ - internal val numberData = ObjectData(value = ObjectValue.Number(1.0)) + internal val numberData = ObjectData(number = 1.0) /** Test fixture for zero number value */ - internal val zeroData = ObjectData(value = ObjectValue.Number(0.0)) + internal val zeroData = ObjectData(number = 0.0) /** Test fixture for boolean true value */ - internal val trueData = ObjectData(value = ObjectValue.Boolean(true)) + internal val trueData = ObjectData(boolean = true) /** Test fixture for boolean false value */ - internal val falseData = ObjectData(value = ObjectValue.Boolean(false)) + internal val falseData = ObjectData(boolean = false) /** Test fixture for JSON object value with single property */ - internal val objectData = ObjectData(value = ObjectValue.JsonObject(JsonObject().apply { addProperty("foo", "bar")})) + internal val objectData = ObjectData(json = JsonObject().apply { addProperty("foo", "bar") }) /** Test fixture for JSON array value with three string elements */ internal val arrayData = ObjectData( - value = ObjectValue.JsonArray(JsonArray().apply { + json = JsonArray().apply { add("foo") add("bar") add("baz") - }) + } ) /** diff --git a/liveobjects/src/test/kotlin/io/ably/lib/objects/integration/helpers/fixtures/MapFixtures.kt b/liveobjects/src/test/kotlin/io/ably/lib/objects/integration/helpers/fixtures/MapFixtures.kt index b7979310c..475bbe86a 100644 --- a/liveobjects/src/test/kotlin/io/ably/lib/objects/integration/helpers/fixtures/MapFixtures.kt +++ b/liveobjects/src/test/kotlin/io/ably/lib/objects/integration/helpers/fixtures/MapFixtures.kt @@ -1,7 +1,6 @@ package io.ably.lib.objects.integration.helpers.fixtures import io.ably.lib.objects.ObjectData -import io.ably.lib.objects.ObjectValue import io.ably.lib.objects.integration.helpers.RestObjects /** @@ -115,10 +114,10 @@ internal fun RestObjects.createUserMapObject(channelName: String): String { val preferencesMapObjectId = createMap( channelName, data = mapOf( - "theme" to ObjectData(value = ObjectValue.String("dark")), - "notifications" to ObjectData(value = ObjectValue.Boolean(true)), - "language" to ObjectData(value = ObjectValue.String("en")), - "maxRetries" to ObjectData(value = ObjectValue.Number(3)) + "theme" to ObjectData(string = "dark"), + "notifications" to ObjectData(boolean = true), + "language" to ObjectData(string = "en"), + "maxRetries" to ObjectData(number = 3.0) ) ) @@ -128,8 +127,8 @@ internal fun RestObjects.createUserMapObject(channelName: String): String { data = mapOf( "totalLogins" to DataFixtures.mapRef(loginCounterObjectId), "activeSessions" to DataFixtures.mapRef(sessionCounterObjectId), - "lastLoginTime" to ObjectData(value = ObjectValue.String("2024-01-01T08:30:00Z")), - "profileViews" to ObjectData(value = ObjectValue.Number(42)) + "lastLoginTime" to ObjectData(string = "2024-01-01T08:30:00Z"), + "profileViews" to ObjectData(number = 42.0) ) ) @@ -175,10 +174,10 @@ internal fun RestObjects.createUserProfileMapObject(channelName: String): String return createMap( channelName, data = mapOf( - "userId" to ObjectData(value = ObjectValue.String("user123")), - "name" to ObjectData(value = ObjectValue.String("John Doe")), - "email" to ObjectData(value = ObjectValue.String("john@example.com")), - "isActive" to ObjectData(value = ObjectValue.Boolean(true)), + "userId" to ObjectData(string = "user123"), + "name" to ObjectData(string = "John Doe"), + "email" to ObjectData(string = "john@example.com"), + "isActive" to ObjectData(boolean = true), ) ) } diff --git a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt index b2db8d292..5750046b0 100644 --- a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt +++ b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/HelpersTest.kt @@ -428,21 +428,4 @@ class HelpersTest { assertEquals(ErrorCode.ChannelStateError.code, ex2.errorInfo.code) } - // Binary utilities - @Test - fun testBinaryEqualityHashCodeAndSize() { - val data1 = byteArrayOf(1, 2, 3, 4) - val data2 = byteArrayOf(1, 2, 3, 4) - val data3 = byteArrayOf(4, 3, 2, 1) - - val b1 = Binary(data1) - val b2 = Binary(data2) - val b3 = Binary(data3) - - assertEquals(b1, b2) - assertEquals(b1.hashCode(), b2.hashCode()) - assertNotEquals(b1, b3) - - assertEquals(4, b1.size()) - } } diff --git a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSerializationTest.kt b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSerializationTest.kt index de90b8648..b5b21f526 100644 --- a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSerializationTest.kt +++ b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSerializationTest.kt @@ -158,9 +158,7 @@ class ObjectMessageSerializationTest { extras = null, operation = objectMessage.operation?.copy( initialValue = null, // initialValue set to null - mapOp = objectMessage.operation.mapOp?.copy( - data = null // objectData set to null - ) + mapCreate = null ), objectState = null, serial = null, diff --git a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSizeTest.kt b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSizeTest.kt index 32a51069a..1e7e0dfee 100644 --- a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSizeTest.kt +++ b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/ObjectMessageSizeTest.kt @@ -2,12 +2,14 @@ package io.ably.lib.objects.unit import com.google.gson.JsonObject import io.ably.lib.objects.* +import io.ably.lib.objects.CounterCreate +import io.ably.lib.objects.CounterInc +import io.ably.lib.objects.MapCreate +import io.ably.lib.objects.MapSet import io.ably.lib.objects.ObjectData -import io.ably.lib.objects.ObjectsMapOp import io.ably.lib.objects.ObjectMessage import io.ably.lib.objects.ObjectOperation import io.ably.lib.objects.ObjectOperationAction -import io.ably.lib.objects.ObjectValue import io.ably.lib.objects.ensureMessageSizeWithinLimit import io.ably.lib.objects.size import io.ably.lib.transport.Defaults @@ -40,43 +42,43 @@ class ObjectMessageSizeTest { action = ObjectOperationAction.MapCreate, objectId = "obj_54321", // Not counted in operation size - // MapOp contributes to operation size - mapOp = ObjectsMapOp( + // MapSet contributes to operation size + mapSet = MapSet( key = "mapKey", // Size: 6 bytes (UTF-8 byte length) - data = ObjectData( + value = ObjectData( objectId = "ref_obj", // Not counted in data size - value = ObjectValue.String("sample") // Size: 6 bytes (UTF-8 byte length) + string = "sample" // Size: 6 bytes (UTF-8 byte length) ) // Total ObjectData size: 6 bytes - ), // Total ObjectMapOp size: 6 + 6 = 12 bytes + ), // Total MapSet size: 6 + 6 = 12 bytes - // CounterOp contributes to operation size - counterOp = ObjectsCounterOp( - amount = 10.0 // Size: 8 bytes (number is always 8 bytes) - ), // Total ObjectCounterOp size: 8 bytes + // CounterInc contributes to operation size + counterInc = CounterInc( + number = 10.0 // Size: 8 bytes (number is always 8 bytes) + ), // Total CounterInc size: 8 bytes - // Map contributes to operation size (for MAP_CREATE operations) - map = ObjectsMap( + // MapCreate contributes to operation size (for MAP_CREATE operations) + mapCreate = MapCreate( semantics = ObjectsMapSemantics.LWW, // Not counted in size entries = mapOf( "entry1" to ObjectsMapEntry( // Key size: 6 bytes tombstone = false, // Not counted in entry size timeserial = "ts_123", // Not counted in entry size data = ObjectData( - value = ObjectValue.String("value1") // Size: 6 bytes + string = "value1" // Size: 6 bytes ) // ObjectMapEntry size: 6 bytes ), // Total for this entry: 6 (key) + 6 (entry) = 12 bytes "entry2" to ObjectsMapEntry( // Key size: 6 bytes data = ObjectData( - value = ObjectValue.Number(42) // Size: 8 bytes (number) + number = 42.0 // Size: 8 bytes (number) ) // ObjectMapEntry size: 8 bytes ) // Total for this entry: 6 (key) + 8 (entry) = 14 bytes ) // Total entries size: 12 + 14 = 26 bytes - ), // Total ObjectMap size: 26 bytes + ), // Total MapCreate size: 26 bytes - // Counter contributes to operation size (for COUNTER_CREATE operations) - counter = ObjectsCounter( + // CounterCreate contributes to operation size (for COUNTER_CREATE operations) + counterCreate = CounterCreate( count = 100.0 // Size: 8 bytes (number is always 8 bytes) - ), // Total ObjectCounter size: 8 bytes + ), // Total CounterCreate size: 8 bytes nonce = "nonce123", // Not counted in operation size initialValue = "some-value", // Not counted in operation size @@ -91,12 +93,12 @@ class ObjectMessageSizeTest { createOp = ObjectOperation( action = ObjectOperationAction.MapSet, objectId = "create_obj", - mapOp = ObjectsMapOp( + mapSet = MapSet( key = "createKey", // Size: 9 bytes - data = ObjectData( - value = ObjectValue.String("createValue") // Size: 11 bytes + value = ObjectData( + string = "createValue" // Size: 11 bytes ) // ObjectData size: 11 bytes - ) // ObjectMapOp size: 9 + 11 = 20 bytes + ) // MapSet size: 9 + 11 = 20 bytes ), // Total createOp size: 20 bytes // map contributes to state size @@ -104,7 +106,7 @@ class ObjectMessageSizeTest { entries = mapOf( "stateKey" to ObjectsMapEntry( // Key size: 8 bytes data = ObjectData( - value = ObjectValue.String("stateValue") // Size: 10 bytes + string = "stateValue" // Size: 10 bytes ) // ObjectMapEntry size: 10 bytes ) // Total: 8 + 10 = 18 bytes ) @@ -133,11 +135,11 @@ class ObjectMessageSizeTest { val objectMessage = ObjectMessage( operation = ObjectOperation( objectId = "", - action = ObjectOperationAction.MapCreate, - mapOp = ObjectsMapOp( + action = ObjectOperationAction.MapSet, + mapSet = MapSet( key = "", - data = ObjectData( - value = ObjectValue.String("你😊") // 你 -> 3 bytes, 😊 -> 4 bytes + value = ObjectData( + string = "你😊" // 你 -> 3 bytes, 😊 -> 4 bytes ), ), ) diff --git a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/fixtures/ObjectMessageFixtures.kt b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/fixtures/ObjectMessageFixtures.kt index e09101ac0..2cd28574a 100644 --- a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/fixtures/ObjectMessageFixtures.kt +++ b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/fixtures/ObjectMessageFixtures.kt @@ -3,25 +3,27 @@ package io.ably.lib.objects.unit.fixtures import com.google.gson.JsonArray import com.google.gson.JsonObject import io.ably.lib.objects.* -import io.ably.lib.objects.Binary +import io.ably.lib.objects.CounterCreate +import io.ably.lib.objects.MapCreate +import io.ably.lib.objects.MapSet import io.ably.lib.objects.ObjectData import io.ably.lib.objects.ObjectMessage import io.ably.lib.objects.ObjectState -import io.ably.lib.objects.ObjectValue +import java.util.Base64 -internal val dummyObjectDataStringValue = ObjectData(objectId = "object-id", ObjectValue.String("dummy string")) +internal val dummyObjectDataStringValue = ObjectData(objectId = "object-id", string = "dummy string") -internal val dummyBinaryObjectValue = ObjectData(objectId = "object-id", ObjectValue.Binary(Binary(byteArrayOf(1, 2, 3)))) +internal val dummyBinaryObjectValue = ObjectData(objectId = "object-id", bytes = Base64.getEncoder().encodeToString(byteArrayOf(1, 2, 3))) -internal val dummyNumberObjectValue = ObjectData(objectId = "object-id", ObjectValue.Number(42.0)) +internal val dummyNumberObjectValue = ObjectData(objectId = "object-id", number = 42.0) -internal val dummyBooleanObjectValue = ObjectData(objectId = "object-id", ObjectValue.Boolean(true)) +internal val dummyBooleanObjectValue = ObjectData(objectId = "object-id", boolean = true) val dummyJsonObject = JsonObject().apply { addProperty("foo", "bar") } -internal val dummyJsonObjectValue = ObjectData(objectId = "object-id", ObjectValue.JsonObject(dummyJsonObject)) +internal val dummyJsonObjectValue = ObjectData(objectId = "object-id", json = dummyJsonObject) val dummyJsonArray = JsonArray().apply { add(1); add(2); add(3) } -internal val dummyJsonArrayValue = ObjectData(objectId = "object-id", ObjectValue.JsonArray(dummyJsonArray)) +internal val dummyJsonArrayValue = ObjectData(objectId = "object-id", json = dummyJsonArray) internal val dummyObjectsMapEntry = ObjectsMapEntry( tombstone = false, @@ -38,22 +40,15 @@ internal val dummyObjectsCounter = ObjectsCounter( count = 123.0 ) -internal val dummyObjectsMapOp = ObjectsMapOp( - key = "dummy-key", - data = dummyObjectDataStringValue -) - -internal val dummyObjectsCounterOp = ObjectsCounterOp( - amount = 10.0 +internal val dummyMapCreate = MapCreate( + semantics = ObjectsMapSemantics.LWW, + entries = mapOf("dummy-key" to dummyObjectsMapEntry) ) internal val dummyObjectOperation = ObjectOperation( action = ObjectOperationAction.MapCreate, objectId = "dummy-object-id", - mapOp = dummyObjectsMapOp, - counterOp = dummyObjectsCounterOp, - map = dummyObjectsMap, - counter = dummyObjectsCounter, + mapCreate = dummyMapCreate, nonce = "dummy-nonce", initialValue = "{\"foo\":\"bar\"}" ) @@ -86,11 +81,8 @@ internal fun dummyObjectMessageWithStringData(): ObjectMessage { internal fun dummyObjectMessageWithBinaryData(): ObjectMessage { val binaryObjectMapEntry = dummyObjectsMapEntry.copy(data = dummyBinaryObjectValue) val binaryObjectMap = dummyObjectsMap.copy(entries = mapOf("dummy-key" to binaryObjectMapEntry)) - val binaryObjectMapOp = dummyObjectsMapOp.copy(data = dummyBinaryObjectValue) - val binaryObjectOperation = dummyObjectOperation.copy( - mapOp = binaryObjectMapOp, - map = binaryObjectMap - ) + val binaryMapCreate = dummyMapCreate.copy(entries = mapOf("dummy-key" to binaryObjectMapEntry)) + val binaryObjectOperation = dummyObjectOperation.copy(mapCreate = binaryMapCreate) val binaryObjectState = dummyObjectState.copy( map = binaryObjectMap, createOp = binaryObjectOperation @@ -104,11 +96,8 @@ internal fun dummyObjectMessageWithBinaryData(): ObjectMessage { internal fun dummyObjectMessageWithNumberData(): ObjectMessage { val numberObjectMapEntry = dummyObjectsMapEntry.copy(data = dummyNumberObjectValue) val numberObjectMap = dummyObjectsMap.copy(entries = mapOf("dummy-key" to numberObjectMapEntry)) - val numberObjectMapOp = dummyObjectsMapOp.copy(data = dummyNumberObjectValue) - val numberObjectOperation = dummyObjectOperation.copy( - mapOp = numberObjectMapOp, - map = numberObjectMap - ) + val numberMapCreate = dummyMapCreate.copy(entries = mapOf("dummy-key" to numberObjectMapEntry)) + val numberObjectOperation = dummyObjectOperation.copy(mapCreate = numberMapCreate) val numberObjectState = dummyObjectState.copy( map = numberObjectMap, createOp = numberObjectOperation @@ -122,11 +111,8 @@ internal fun dummyObjectMessageWithNumberData(): ObjectMessage { internal fun dummyObjectMessageWithBooleanData(): ObjectMessage { val booleanObjectMapEntry = dummyObjectsMapEntry.copy(data = dummyBooleanObjectValue) val booleanObjectMap = dummyObjectsMap.copy(entries = mapOf("dummy-key" to booleanObjectMapEntry)) - val booleanObjectMapOp = dummyObjectsMapOp.copy(data = dummyBooleanObjectValue) - val booleanObjectOperation = dummyObjectOperation.copy( - mapOp = booleanObjectMapOp, - map = booleanObjectMap - ) + val booleanMapCreate = dummyMapCreate.copy(entries = mapOf("dummy-key" to booleanObjectMapEntry)) + val booleanObjectOperation = dummyObjectOperation.copy(mapCreate = booleanMapCreate) val booleanObjectState = dummyObjectState.copy( map = booleanObjectMap, createOp = booleanObjectOperation @@ -140,11 +126,11 @@ internal fun dummyObjectMessageWithBooleanData(): ObjectMessage { internal fun dummyObjectMessageWithJsonObjectData(): ObjectMessage { val jsonObjectMapEntry = dummyObjectsMapEntry.copy(data = dummyJsonObjectValue) val jsonObjectMap = dummyObjectsMap.copy(entries = mapOf("dummy-key" to jsonObjectMapEntry)) - val jsonObjectMapOp = dummyObjectsMapOp.copy(data = dummyJsonObjectValue) + val jsonMapCreate = dummyMapCreate.copy(entries = mapOf("dummy-key" to jsonObjectMapEntry)) val jsonObjectOperation = dummyObjectOperation.copy( action = ObjectOperationAction.MapSet, - mapOp = jsonObjectMapOp, - map = jsonObjectMap + mapCreate = null, + mapSet = MapSet(key = "dummy-key", value = dummyJsonObjectValue) ) val jsonObjectState = dummyObjectState.copy( map = jsonObjectMap, @@ -159,11 +145,10 @@ internal fun dummyObjectMessageWithJsonObjectData(): ObjectMessage { internal fun dummyObjectMessageWithJsonArrayData(): ObjectMessage { val jsonArrayMapEntry = dummyObjectsMapEntry.copy(data = dummyJsonArrayValue) val jsonArrayMap = dummyObjectsMap.copy(entries = mapOf("dummy-key" to jsonArrayMapEntry)) - val jsonArrayMapOp = dummyObjectsMapOp.copy(data = dummyJsonArrayValue) val jsonArrayOperation = dummyObjectOperation.copy( action = ObjectOperationAction.MapSet, - mapOp = jsonArrayMapOp, - map = jsonArrayMap + mapCreate = null, + mapSet = MapSet(key = "dummy-key", value = dummyJsonArrayValue) ) val jsonArrayState = dummyObjectState.copy( map = jsonArrayMap, diff --git a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/objects/DefaultRealtimeObjectsTest.kt b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/objects/DefaultRealtimeObjectsTest.kt index 40565cabe..8afcd691a 100644 --- a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/objects/DefaultRealtimeObjectsTest.kt +++ b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/objects/DefaultRealtimeObjectsTest.kt @@ -1,7 +1,6 @@ package io.ably.lib.objects.unit.objects import io.ably.lib.objects.* -import io.ably.lib.objects.ObjectsCounterOp import io.ably.lib.objects.ObjectData import io.ably.lib.objects.ObjectMessage import io.ably.lib.objects.ObjectOperation @@ -105,7 +104,7 @@ class DefaultRealtimeObjectsTest { operation = ObjectOperation( action = ObjectOperationAction.CounterInc, objectId = "counter:testObject@1", - counterOp = ObjectsCounterOp(amount = 5.0) + counterInc = CounterInc(number = 5.0) ), serial = "serial1", siteCode = "site1" diff --git a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/objects/ObjectsManagerTest.kt b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/objects/ObjectsManagerTest.kt index 0a4ee5008..fd6cc874a 100644 --- a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/objects/ObjectsManagerTest.kt +++ b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/objects/ObjectsManagerTest.kt @@ -197,7 +197,7 @@ class ObjectsManagerTest { operation = ObjectOperation( action = ObjectOperationAction.CounterCreate, objectId = "counter:testObject@1", - counterOp = ObjectsCounterOp(amount = 5.0) + counterInc = CounterInc(number = 5.0) ), serial = "serial1", siteCode = "site1" @@ -236,7 +236,7 @@ class ObjectsManagerTest { operation = ObjectOperation( action = ObjectOperationAction.CounterInc, objectId = "counter:test@1", - counterOp = ObjectsCounterOp(amount = 5.0) + counterInc = CounterInc(number = 5.0) ), serial = "ser-ack-01", siteCode = "site1" @@ -267,7 +267,7 @@ class ObjectsManagerTest { operation = ObjectOperation( action = ObjectOperationAction.MapSet, objectId = "map:testMap@1", - mapOp = ObjectsMapOp(key = "key1", data = ObjectData(value = ObjectValue.String("value1"))) + mapSet = MapSet(key = "key1", value = ObjectData(string = "value1")) ), serial = "ser-map-01", siteCode = "site1" @@ -277,7 +277,7 @@ class ObjectsManagerTest { objectsManager.applyAckResult(listOf(msg)) // Verify entry was set (LOCAL source) - assertEquals("value1", liveMap.data["key1"]?.data?.value?.value, + assertEquals("value1", liveMap.data["key1"]?.data?.string, "MAP_SET should be applied locally on ACK") // Entry timeserial should be updated (within LiveMapManager, regardless of source) assertEquals("ser-map-01", liveMap.data["key1"]?.timeserial, @@ -306,7 +306,7 @@ class ObjectsManagerTest { operation = ObjectOperation( action = ObjectOperationAction.CounterInc, objectId = "counter:test@1", - counterOp = ObjectsCounterOp(amount = 5.0) + counterInc = CounterInc(number = 5.0) ), serial = "ser-echo-01", siteCode = "site1" @@ -338,7 +338,7 @@ class ObjectsManagerTest { operation = ObjectOperation( action = ObjectOperationAction.CounterInc, objectId = "counter:test@1", - counterOp = ObjectsCounterOp(amount = 3.0) + counterInc = CounterInc(number = 3.0) ), serial = "ser-channel-01", siteCode = "site1" @@ -369,7 +369,7 @@ class ObjectsManagerTest { operation = ObjectOperation( action = ObjectOperationAction.CounterInc, objectId = "counter:test@1", - counterOp = ObjectsCounterOp(amount = 5.0) + counterInc = CounterInc(number = 5.0) ), serial = "ser-ack-01", siteCode = "site1" @@ -411,7 +411,7 @@ class ObjectsManagerTest { operation = ObjectOperation( action = ObjectOperationAction.CounterInc, objectId = "counter:test@1", - counterOp = ObjectsCounterOp(amount = 5.0) + counterInc = CounterInc(number = 5.0) ), serial = "ser-01", siteCode = "site1" @@ -468,7 +468,7 @@ class ObjectsManagerTest { operation = ObjectOperation( action = ObjectOperationAction.CounterInc, objectId = "counter:test@1", - counterOp = ObjectsCounterOp(amount = 5.0) + counterInc = CounterInc(number = 5.0) ), serial = "ser-buffered", siteCode = "site1" @@ -527,7 +527,7 @@ class ObjectsManagerTest { operation = ObjectOperation( action = ObjectOperationAction.CounterInc, objectId = "counter:test@1", - counterOp = ObjectsCounterOp(amount = 5.0) + counterInc = CounterInc(number = 5.0) ), serial = "ser-01", siteCode = "site1" diff --git a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/DefaultLiveCounterTest.kt b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/DefaultLiveCounterTest.kt index 9c6bca377..3e82cebc9 100644 --- a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/DefaultLiveCounterTest.kt +++ b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/DefaultLiveCounterTest.kt @@ -1,7 +1,7 @@ package io.ably.lib.objects.unit.type.livecounter -import io.ably.lib.objects.ObjectsCounter -import io.ably.lib.objects.ObjectsCounterOp +import io.ably.lib.objects.CounterCreate +import io.ably.lib.objects.CounterInc import io.ably.lib.objects.ObjectMessage import io.ably.lib.objects.ObjectOperation import io.ably.lib.objects.ObjectOperationAction @@ -49,7 +49,7 @@ class DefaultLiveCounterTest { val operation = ObjectOperation( action = ObjectOperationAction.CounterCreate, objectId = "counter:testCounter@2", // Different objectId - counter = ObjectsCounter(count = 20.0) + counterCreate = CounterCreate(count = 20.0) ) val message = ObjectMessage( @@ -81,7 +81,7 @@ class DefaultLiveCounterTest { val operation = ObjectOperation( action = ObjectOperationAction.CounterCreate, objectId = "counter:testCounter@1", // Matching objectId - counter = ObjectsCounter(count = 20.0) + counterCreate = CounterCreate(count = 20.0) ) val message = ObjectMessage( @@ -108,7 +108,7 @@ class DefaultLiveCounterTest { val operation = ObjectOperation( action = ObjectOperationAction.CounterCreate, objectId = "counter:testCounter@1", // Matching objectId - counter = ObjectsCounter(count = 20.0) + counterCreate = CounterCreate(count = 20.0) ) val message = ObjectMessage( @@ -135,7 +135,7 @@ class DefaultLiveCounterTest { operation = ObjectOperation( action = ObjectOperationAction.CounterInc, objectId = "counter:testCounter@1", - counterOp = ObjectsCounterOp(amount = 5.0) + counterInc = io.ably.lib.objects.CounterInc(number = 5.0) ), serial = "serial1", siteCode = "site1" @@ -160,7 +160,7 @@ class DefaultLiveCounterTest { operation = ObjectOperation( action = ObjectOperationAction.CounterInc, objectId = "counter:testCounter@1", - counterOp = ObjectsCounterOp(amount = 5.0) + counterInc = io.ably.lib.objects.CounterInc(number = 5.0) ), serial = "serial1", // Older than "serial5" siteCode = "site1" @@ -184,7 +184,7 @@ class DefaultLiveCounterTest { operation = ObjectOperation( action = ObjectOperationAction.CounterInc, objectId = "counter:testCounter@1", - counterOp = ObjectsCounterOp(amount = 5.0) + counterInc = io.ably.lib.objects.CounterInc(number = 5.0) ), serial = "serial1", siteCode = "site1" @@ -205,7 +205,7 @@ class DefaultLiveCounterTest { operation = ObjectOperation( action = ObjectOperationAction.CounterInc, objectId = "counter:testCounter@1", - counterOp = ObjectsCounterOp(amount = 5.0) + counterInc = io.ably.lib.objects.CounterInc(number = 5.0) ), serial = "serial1", siteCode = "site1" @@ -227,7 +227,7 @@ class DefaultLiveCounterTest { operation = ObjectOperation( action = ObjectOperationAction.CounterCreate, objectId = "counter:testCounter@1", - counter = ObjectsCounter(count = 20.0) + counterCreate = CounterCreate(count = 20.0) ), serial = "serial1", siteCode = "site1" diff --git a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/LiveCounterManagerTest.kt b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/LiveCounterManagerTest.kt index 813f44dc5..99124237c 100644 --- a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/LiveCounterManagerTest.kt +++ b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/LiveCounterManagerTest.kt @@ -1,6 +1,9 @@ package io.ably.lib.objects.unit.type.livecounter import io.ably.lib.objects.* +import io.ably.lib.objects.CounterCreate +import io.ably.lib.objects.CounterInc +import io.ably.lib.objects.MapCreate import io.ably.lib.objects.unit.LiveCounterManager import io.ably.lib.objects.unit.TombstonedAt import io.ably.lib.objects.unit.getDefaultLiveCounterWithMockedDeps @@ -44,7 +47,7 @@ class DefaultLiveCounterManagerTest { val createOp = ObjectOperation( action = ObjectOperationAction.CounterCreate, objectId = "testCounterId", - counter = ObjectsCounter(count = 10.0) + counterCreate = CounterCreate(count = 10.0) ) val objectState = ObjectState( @@ -71,7 +74,7 @@ class DefaultLiveCounterManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.CounterCreate, objectId = "testCounterId", - counter = ObjectsCounter(count = 10.0) + counterCreate = CounterCreate(count = 10.0) ) // RTLC7d1b - Should return true for successful COUNTER_CREATE @@ -87,7 +90,7 @@ class DefaultLiveCounterManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.CounterInc, objectId = "testCounterId", - counterOp = ObjectsCounterOp(amount = 5.0) + counterInc = CounterInc(number = 5.0) ) // RTLC7d2b - Should return true for successful COUNTER_INC @@ -119,7 +122,7 @@ class DefaultLiveCounterManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.MapCreate, // Unsupported action for counter objectId = "testCounterId", - map = ObjectsMap(semantics = ObjectsMapSemantics.LWW, entries = emptyMap()) + mapCreate = MapCreate(semantics = ObjectsMapSemantics.LWW, entries = emptyMap()) ) // RTLC7d3 - Should return false for unsupported action (no longer throws) @@ -135,7 +138,7 @@ class DefaultLiveCounterManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.CounterCreate, objectId = "testCounterId", - counter = ObjectsCounter(count = 20.0) + counterCreate = CounterCreate(count = 20.0) ) // RTLC7d1 - Apply counter create operation @@ -158,7 +161,7 @@ class DefaultLiveCounterManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.CounterCreate, objectId = "testCounterId", - counter = ObjectsCounter(count = 20.0) + counterCreate = CounterCreate(count = 20.0) ) // RTLC8b - Should skip if already merged @@ -181,7 +184,7 @@ class DefaultLiveCounterManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.CounterCreate, objectId = "testCounterId", - counter = ObjectsCounter(count = 20.0) + counterCreate = CounterCreate(count = 20.0) ) // RTLC8c - Should apply if not merged @@ -203,7 +206,7 @@ class DefaultLiveCounterManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.CounterCreate, objectId = "testCounterId", - counter = null // No count specified + counterCreate = null // No count specified ) // RTLC10a - Should default to 0 @@ -225,7 +228,7 @@ class DefaultLiveCounterManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.CounterInc, objectId = "testCounterId", - counterOp = ObjectsCounterOp(amount = 5.0) + counterInc = CounterInc(number = 5.0) ) // RTLC7d2 - Apply counter increment operation @@ -242,7 +245,7 @@ class DefaultLiveCounterManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.CounterInc, objectId = "testCounterId", - counterOp = null // Missing payload + counterInc = null // Missing payload ) // RTLC7d2 - Should throw error for missing payload @@ -265,36 +268,36 @@ class DefaultLiveCounterManagerTest { // Set initial data liveCounter.data.set(10.0) - val counterOp = ObjectsCounterOp(amount = 7.0) + val counterInc = CounterInc(number = 7.0) // RTLC9b - Apply counter increment liveCounterManager.applyOperation(ObjectOperation( action = ObjectOperationAction.CounterInc, objectId = "testCounterId", - counterOp = counterOp + counterInc = counterInc ), null) assertEquals(17.0, liveCounter.data.get()) // 10 + 7 } @Test - fun `(RTLC9, RTLC9b) LiveCounterManager should handle null amount in counter increment`() { + fun `(RTLC7, RTLC7d2) LiveCounterManager should throw error when counterInc payload missing`() { val liveCounter = getDefaultLiveCounterWithMockedDeps() val liveCounterManager = liveCounter.LiveCounterManager // Set initial data liveCounter.data.set(10.0) - val counterOp = ObjectsCounterOp(amount = null) // Null amount - - // RTLC9b - Apply counter increment with null amount - liveCounterManager.applyOperation(ObjectOperation( - action = ObjectOperationAction.CounterInc, - objectId = "testCounterId", - counterOp = counterOp - ), null) - - assertEquals(10.0, liveCounter.data.get()) // Should not change (null defaults to 0) + // RTLC7d2 - Apply counter increment with no payload - throws error + val exception = assertFailsWith { + liveCounterManager.applyOperation(ObjectOperation( + action = ObjectOperationAction.CounterInc, + objectId = "testCounterId", + counterInc = null + ), null) + } + assertNotNull(exception.errorInfo) + assertEquals(92000, exception.errorInfo.code) } @Test diff --git a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/DefaultLiveMapTest.kt b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/DefaultLiveMapTest.kt index 4746b91ee..7ddd43937 100644 --- a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/DefaultLiveMapTest.kt +++ b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/DefaultLiveMapTest.kt @@ -2,7 +2,9 @@ package io.ably.lib.objects.unit.type.livemap import io.ably.lib.objects.ObjectsMapSemantics import io.ably.lib.objects.ObjectsMap -import io.ably.lib.objects.ObjectsMapOp +import io.ably.lib.objects.MapCreate +import io.ably.lib.objects.MapSet +import io.ably.lib.objects.MapRemove import io.ably.lib.objects.ObjectsOperationSource import io.ably.lib.objects.ObjectState import io.ably.lib.objects.ObjectMessage @@ -53,7 +55,7 @@ class DefaultLiveMapTest { val operation = ObjectOperation( action = ObjectOperationAction.MapCreate, objectId = "map:testMap@2", // Different objectId - map = ObjectsMap( + mapCreate = MapCreate( semantics = ObjectsMapSemantics.LWW, entries = emptyMap() ) @@ -88,7 +90,7 @@ class DefaultLiveMapTest { val operation = ObjectOperation( action = ObjectOperationAction.MapCreate, objectId = "map:testMap@1", // Matching objectId - map = ObjectsMap( + mapCreate = MapCreate( semantics = ObjectsMapSemantics.LWW, entries = emptyMap() ) @@ -118,7 +120,7 @@ class DefaultLiveMapTest { val operation = ObjectOperation( action = ObjectOperationAction.MapCreate, objectId = "map:testMap@1", // Matching objectId - map = ObjectsMap( + mapCreate = MapCreate( semantics = ObjectsMapSemantics.LWW, entries = emptyMap() ) @@ -148,7 +150,7 @@ class DefaultLiveMapTest { operation = ObjectOperation( action = ObjectOperationAction.MapSet, objectId = "map:testMap@1", - mapOp = ObjectsMapOp(key = "key1", data = io.ably.lib.objects.ObjectData(value = io.ably.lib.objects.ObjectValue.String("value1"))) + mapSet = io.ably.lib.objects.MapSet(key = "key1", value = io.ably.lib.objects.ObjectData(string = "value1")) ), serial = "serial1", siteCode = "site1" @@ -158,7 +160,7 @@ class DefaultLiveMapTest { val result = liveMap.applyObject(message, ObjectsOperationSource.LOCAL) assertTrue(result, "applyObject should return true for successful MAP_SET") - assertEquals("value1", liveMap.data["key1"]?.data?.value?.value, "map entry should be updated for LOCAL source") + assertEquals("value1", liveMap.data["key1"]?.data?.string, "map entry should be updated for LOCAL source") assertFalse(liveMap.siteTimeserials.containsKey("site1"), "siteTimeserials should NOT be updated for LOCAL source") } @@ -173,7 +175,7 @@ class DefaultLiveMapTest { operation = ObjectOperation( action = ObjectOperationAction.MapSet, objectId = "map:testMap@1", - mapOp = ObjectsMapOp(key = "key1", data = io.ably.lib.objects.ObjectData(value = io.ably.lib.objects.ObjectValue.String("value1"))) + mapSet = io.ably.lib.objects.MapSet(key = "key1", value = io.ably.lib.objects.ObjectData(string = "value1")) ), serial = "serial1", // Older than "serial5" siteCode = "site1" @@ -196,7 +198,7 @@ class DefaultLiveMapTest { operation = ObjectOperation( action = ObjectOperationAction.MapSet, objectId = "map:testMap@1", - mapOp = ObjectsMapOp(key = "key1", data = io.ably.lib.objects.ObjectData(value = io.ably.lib.objects.ObjectValue.String("value1"))) + mapSet = io.ably.lib.objects.MapSet(key = "key1", value = io.ably.lib.objects.ObjectData(string = "value1")) ), serial = "serial1", siteCode = "site1" @@ -217,7 +219,7 @@ class DefaultLiveMapTest { operation = ObjectOperation( action = ObjectOperationAction.MapSet, objectId = "map:testMap@1", - mapOp = ObjectsMapOp(key = "key1", data = io.ably.lib.objects.ObjectData(value = io.ably.lib.objects.ObjectValue.String("value1"))) + mapSet = io.ably.lib.objects.MapSet(key = "key1", value = io.ably.lib.objects.ObjectData(string = "value1")) ), serial = "serial1", siteCode = "site1" @@ -227,7 +229,7 @@ class DefaultLiveMapTest { val result = liveMap.applyObject(message, ObjectsOperationSource.CHANNEL) assertTrue(result, "applyObject should return true for successful MAP_SET") - assertEquals("value1", liveMap.data["key1"]?.data?.value?.value) + assertEquals("value1", liveMap.data["key1"]?.data?.string) } @Test @@ -239,7 +241,7 @@ class DefaultLiveMapTest { operation = ObjectOperation( action = ObjectOperationAction.MapRemove, objectId = "map:testMap@1", - mapOp = ObjectsMapOp(key = "key1") + mapRemove = io.ably.lib.objects.MapRemove(key = "key1") ), serial = "serial1", siteCode = "site1" diff --git a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/LiveMapManagerTest.kt b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/LiveMapManagerTest.kt index a1da570fe..238e1798d 100644 --- a/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/LiveMapManagerTest.kt +++ b/liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/LiveMapManagerTest.kt @@ -1,6 +1,9 @@ package io.ably.lib.objects.unit.type.livemap import io.ably.lib.objects.* +import io.ably.lib.objects.MapCreate +import io.ably.lib.objects.MapRemove +import io.ably.lib.objects.MapSet import io.ably.lib.objects.type.livemap.LiveMapEntry import io.ably.lib.objects.type.livemap.LiveMapManager import io.ably.lib.objects.type.map.LiveMapUpdate @@ -26,7 +29,7 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "1", - data = ObjectData(value = ObjectValue.String("oldValue")) + data = ObjectData(string = "oldValue") ) val objectState = ObjectState( @@ -35,11 +38,11 @@ class LiveMapManagerTest { semantics = ObjectsMapSemantics.LWW, entries = mapOf( "key1" to ObjectsMapEntry( - data = ObjectData(value = ObjectValue.String("newValue1")), + data = ObjectData(string = "newValue1"), timeserial = "serial1" ), "key2" to ObjectsMapEntry( - data = ObjectData(value = ObjectValue.String("value2")), + data = ObjectData(string = "value2"), timeserial = "serial2" ) ) @@ -52,8 +55,8 @@ class LiveMapManagerTest { assertFalse(liveMap.createOperationIsMerged) // RTLM6b assertEquals(2, liveMap.data.size) // RTLM6c - assertEquals("newValue1", liveMap.data["key1"]?.data?.value?.value) // RTLM6c - assertEquals("value2", liveMap.data["key2"]?.data?.value?.value) // RTLM6c + assertEquals("newValue1", liveMap.data["key1"]?.data?.string) // RTLM6c + assertEquals("value2", liveMap.data["key2"]?.data?.string) // RTLM6c // Assert on update field - should show changes from old to new state val expectedUpdate = mapOf( @@ -72,7 +75,7 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "1", - data = ObjectData(value = ObjectValue.String("oldValue")) + data = ObjectData(string = "oldValue") ) val objectState = ObjectState( @@ -104,7 +107,7 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "1", - data = ObjectData(value = ObjectValue.String("oldValue")) + data = ObjectData(string = "oldValue") ) val objectState = ObjectState( @@ -133,21 +136,21 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "1", - data = ObjectData(value = ObjectValue.String("existingValue")) + data = ObjectData(string = "existingValue") ) val createOp = ObjectOperation( action = ObjectOperationAction.MapCreate, objectId = "map:testMap@1", - map = ObjectsMap( + mapCreate = MapCreate( semantics = ObjectsMapSemantics.LWW, entries = mapOf( "key1" to ObjectsMapEntry( - data = ObjectData(value = ObjectValue.String("createValue")), + data = ObjectData(string = "createValue"), timeserial = "serial1" ), "key2" to ObjectsMapEntry( - data = ObjectData(value = ObjectValue.String("newValue")), + data = ObjectData(string = "newValue"), timeserial = "serial2" ) ) @@ -160,7 +163,7 @@ class LiveMapManagerTest { semantics = ObjectsMapSemantics.LWW, entries = mapOf( "key1" to ObjectsMapEntry( - data = ObjectData(value = ObjectValue.String("stateValue")), + data = ObjectData(string = "stateValue"), timeserial = "serial3" ) ) @@ -174,8 +177,8 @@ class LiveMapManagerTest { val update = liveMapManager.applyState(objectState, null) assertEquals(2, liveMap.data.size) // Should have both state and create op entries - assertEquals("stateValue", liveMap.data["key1"]?.data?.value?.value) // State value takes precedence - assertEquals("newValue", liveMap.data["key2"]?.data?.value?.value) // Create op value + assertEquals("stateValue", liveMap.data["key1"]?.data?.string) // State value takes precedence + assertEquals("newValue", liveMap.data["key2"]?.data?.string) // Create op value // Assert on update field - should show changes from create operation val expectedUpdate = mapOf( @@ -194,7 +197,7 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "1", - data = ObjectData(value = ObjectValue.String("oldValue")) + data = ObjectData(string = "oldValue") ) val expectedTimestamp = 1234567890L @@ -204,13 +207,13 @@ class LiveMapManagerTest { semantics = ObjectsMapSemantics.LWW, entries = mapOf( "key1" to ObjectsMapEntry( - data = ObjectData(value = ObjectValue.String("newValue")), + data = ObjectData(string = "newValue"), timeserial = "serial1", tombstone = true, serialTimestamp = expectedTimestamp ), "key2" to ObjectsMapEntry( - data = ObjectData(value = ObjectValue.String("value2")), + data = ObjectData(string = "value2"), timeserial = "serial2" ) ) @@ -225,7 +228,7 @@ class LiveMapManagerTest { assertEquals(2, liveMap.data.size) // RTLM6c assertTrue(liveMap.data["key1"]?.isTombstoned == true) // Should be tombstoned assertEquals(expectedTimestamp, liveMap.data["key1"]?.tombstonedAt) // Should use provided serialTimestamp - assertEquals("value2", liveMap.data["key2"]?.data?.value?.value) // RTLM6c + assertEquals("value2", liveMap.data["key2"]?.data?.string) // RTLM6c // Assert on update field - should show that key1 was removed (tombstoned) val expectedUpdate = mapOf( @@ -244,7 +247,7 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "1", - data = ObjectData(value = ObjectValue.String("oldValue")) + data = ObjectData(string = "oldValue") ) val objectState = ObjectState( @@ -253,13 +256,13 @@ class LiveMapManagerTest { semantics = ObjectsMapSemantics.LWW, entries = mapOf( "key1" to ObjectsMapEntry( - data = ObjectData(value = ObjectValue.String("newValue")), + data = ObjectData(string = "newValue"), timeserial = "serial1", tombstone = true, serialTimestamp = null // No timestamp provided ), "key2" to ObjectsMapEntry( - data = ObjectData(value = ObjectValue.String("value2")), + data = ObjectData(string = "value2"), timeserial = "serial2" ) ) @@ -278,7 +281,7 @@ class LiveMapManagerTest { assertNotNull(liveMap.data["key1"]?.tombstonedAt) // Should have timestamp assertTrue(liveMap.data["key1"]?.tombstonedAt!! >= beforeOperation) // Should be after operation start assertTrue(liveMap.data["key1"]?.tombstonedAt!! <= afterOperation) // Should be before operation end - assertEquals("value2", liveMap.data["key2"]?.data?.value?.value) // RTLM6c + assertEquals("value2", liveMap.data["key2"]?.data?.string) // RTLM6c // Assert on update field - should show that key1 was removed (tombstoned) val expectedUpdate = mapOf( @@ -297,15 +300,15 @@ class LiveMapManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.MapCreate, objectId = "map:testMap@1", - map = ObjectsMap( + mapCreate = MapCreate( semantics = ObjectsMapSemantics.LWW, entries = mapOf( "key1" to ObjectsMapEntry( - data = ObjectData(value = ObjectValue.String("value1")), + data = ObjectData(string = "value1"), timeserial = "serial1" ), "key2" to ObjectsMapEntry( - data = ObjectData(value = ObjectValue.String("value2")), + data = ObjectData(string = "value2"), timeserial = "serial2" ) ) @@ -316,8 +319,8 @@ class LiveMapManagerTest { liveMapManager.applyOperation(operation, "serial1", null) assertEquals(2, liveMap.data.size) // Should have both entries - assertEquals("value1", liveMap.data["key1"]?.data?.value?.value) // Should have value1 - assertEquals("value2", liveMap.data["key2"]?.data?.value?.value) // Should have value2 + assertEquals("value1", liveMap.data["key1"]?.data?.string) // Should have value1 + assertEquals("value2", liveMap.data["key2"]?.data?.string) // Should have value2 assertTrue(liveMap.createOperationIsMerged) // Should be marked as merged } @@ -330,24 +333,24 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "serial1", - data = ObjectData(value = ObjectValue.String("existingValue")) + data = ObjectData(string = "existingValue") ) val expectedTimestamp = 1234567890L val operation = ObjectOperation( action = ObjectOperationAction.MapCreate, objectId = "map:testMap@1", - map = ObjectsMap( + mapCreate = MapCreate( semantics = ObjectsMapSemantics.LWW, entries = mapOf( "key1" to ObjectsMapEntry( - data = ObjectData(value = ObjectValue.String("createValue")), + data = ObjectData(string = "createValue"), timeserial = "serial2", tombstone = true, serialTimestamp = expectedTimestamp ), "key2" to ObjectsMapEntry( - data = ObjectData(value = ObjectValue.String("newValue")), + data = ObjectData(string = "newValue"), timeserial = "serial3" ), "key3" to ObjectsMapEntry( @@ -365,7 +368,7 @@ class LiveMapManagerTest { assertEquals(3, liveMap.data.size) // Should have all entries assertTrue(liveMap.data["key1"]?.isTombstoned == true) // RTLM17a2 - Should be tombstoned assertEquals(expectedTimestamp, liveMap.data["key1"]?.tombstonedAt) // Should use provided serialTimestamp - assertEquals("newValue", liveMap.data["key2"]?.data?.value?.value) // RTLM17a1 - Should be added + assertEquals("newValue", liveMap.data["key2"]?.data?.string) // RTLM17a1 - Should be added assertTrue(liveMap.data["key3"]?.isTombstoned == true) // RTLM17a2 - Should be tombstoned assertTrue(liveMap.createOperationIsMerged) // RTLM17b - Should be marked as merged } @@ -379,22 +382,22 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "serial1", - data = ObjectData(value = ObjectValue.String("oldValue")) + data = ObjectData(string = "oldValue") ) val operation = ObjectOperation( action = ObjectOperationAction.MapSet, objectId = "map:testMap@1", - mapOp = ObjectsMapOp( + mapSet = MapSet( key = "key1", - data = ObjectData(value = ObjectValue.String("newValue")) + value = ObjectData(string = "newValue") ) ) // RTLM15d2 - Apply map set operation liveMapManager.applyOperation(operation, "serial2", null) - assertEquals("newValue", liveMap.data["key1"]?.data?.value?.value) // RTLM7a2a + assertEquals("newValue", liveMap.data["key1"]?.data?.string) // RTLM7a2a assertEquals("serial2", liveMap.data["key1"]?.timeserial) // RTLM7a2b assertFalse(liveMap.data["key1"]?.isTombstoned == true) // RTLM7a2c } @@ -408,13 +411,13 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "serial1", - data = ObjectData(value = ObjectValue.String("value1")) + data = ObjectData(string = "value1") ) val operation = ObjectOperation( action = ObjectOperationAction.MapRemove, objectId = "map:testMap@1", - mapOp = ObjectsMapOp(key = "key1") + mapRemove = MapRemove(key = "key1") ) val expectedTimestamp = 1234567890L @@ -436,13 +439,13 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "serial1", - data = ObjectData(value = ObjectValue.String("value1")) + data = ObjectData(string = "value1") ) val operation = ObjectOperation( action = ObjectOperationAction.MapRemove, objectId = "map:testMap@1", - mapOp = ObjectsMapOp(key = "key1") + mapRemove = MapRemove(key = "key1") ) val beforeOperation = System.currentTimeMillis() @@ -466,7 +469,7 @@ class LiveMapManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.MapCreate, objectId = "map:testMap@1", - map = ObjectsMap(semantics = ObjectsMapSemantics.LWW, entries = emptyMap()) + mapCreate = MapCreate(semantics = ObjectsMapSemantics.LWW, entries = emptyMap()) ) // RTLM15d1b - Should return true for successful MAP_CREATE @@ -482,7 +485,7 @@ class LiveMapManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.MapSet, objectId = "map:testMap@1", - mapOp = ObjectsMapOp(key = "key1", data = ObjectData(value = ObjectValue.String("value1"))) + mapSet = MapSet(key = "key1", value = ObjectData(string = "value1")) ) // RTLM15d2b - Should return true for successful MAP_SET @@ -498,7 +501,7 @@ class LiveMapManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.MapRemove, objectId = "map:testMap@1", - mapOp = ObjectsMapOp(key = "key1") + mapRemove = MapRemove(key = "key1") ) // RTLM15d3b - Should return true for successful MAP_REMOVE @@ -530,7 +533,7 @@ class LiveMapManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.CounterCreate, // Unsupported action for map objectId = "map:testMap@1", - counter = ObjectsCounter(count = 20.0) + counterCreate = io.ably.lib.objects.CounterCreate(count = 20.0) ) // RTLM15d4 - Should return false for unsupported action (no longer throws) @@ -549,11 +552,11 @@ class LiveMapManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.MapCreate, objectId = "map:testMap@1", - map = ObjectsMap( + mapCreate = MapCreate( semantics = ObjectsMapSemantics.LWW, entries = mapOf( "key1" to ObjectsMapEntry( - data = ObjectData(value = ObjectValue.String("value1")), + data = ObjectData(string = "value1"), timeserial = "serial1" ) ) @@ -578,21 +581,21 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "serial1", - data = ObjectData(value = ObjectValue.String("existingValue")) + data = ObjectData(string = "existingValue") ) val operation = ObjectOperation( action = ObjectOperationAction.MapCreate, objectId = "map:testMap@1", - map = ObjectsMap( + mapCreate = MapCreate( semantics = ObjectsMapSemantics.LWW, entries = mapOf( "key1" to ObjectsMapEntry( - data = ObjectData(value = ObjectValue.String("createValue")), + data = ObjectData(string = "createValue"), timeserial = "serial2" ), "key2" to ObjectsMapEntry( - data = ObjectData(value = ObjectValue.String("newValue")), + data = ObjectData(string = "newValue"), timeserial = "serial3" ), "key3" to ObjectsMapEntry( @@ -608,8 +611,8 @@ class LiveMapManagerTest { liveMapManager.applyOperation(operation, "serial1", null) assertEquals(3, liveMap.data.size) // Should have all entries - assertEquals("createValue", liveMap.data["key1"]?.data?.value?.value) // RTLM17a1 - Should be updated - assertEquals("newValue", liveMap.data["key2"]?.data?.value?.value) // RTLM17a1 - Should be added + assertEquals("createValue", liveMap.data["key1"]?.data?.string) // RTLM17a1 - Should be updated + assertEquals("newValue", liveMap.data["key2"]?.data?.string) // RTLM17a1 - Should be added assertTrue(liveMap.data["key3"]?.isTombstoned == true) // RTLM17a2 - Should be tombstoned assertTrue(liveMap.createOperationIsMerged) // RTLM17b - Should be marked as merged } @@ -622,9 +625,9 @@ class LiveMapManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.MapSet, objectId = "map:testMap@1", - mapOp = ObjectsMapOp( + mapSet = MapSet( key = "newKey", - data = ObjectData(value = ObjectValue.String("newValue")) + value = ObjectData(string = "newValue") ) ) @@ -632,7 +635,7 @@ class LiveMapManagerTest { liveMapManager.applyOperation(operation, "serial1", null) assertEquals(1, liveMap.data.size) // Should have one entry - assertEquals("newValue", liveMap.data["newKey"]?.data?.value?.value) // RTLM7b1 + assertEquals("newValue", liveMap.data["newKey"]?.data?.string) // RTLM7b1 assertEquals("serial1", liveMap.data["newKey"]?.timeserial) // Should have serial assertFalse(liveMap.data["newKey"]?.isTombstoned == true) // RTLM7b2 } @@ -646,22 +649,22 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "serial2", // Higher than "serial1" - data = ObjectData(value = ObjectValue.String("existingValue")) + data = ObjectData(string = "existingValue") ) val operation = ObjectOperation( action = ObjectOperationAction.MapSet, objectId = "map:testMap@1", - mapOp = ObjectsMapOp( + mapSet = MapSet( key = "key1", - data = ObjectData(value = ObjectValue.String("newValue")) + value = ObjectData(string = "newValue") ) ) // RTLM7a - Should skip operation with lower serial liveMapManager.applyOperation(operation, "serial1", null) - assertEquals("existingValue", liveMap.data["key1"]?.data?.value?.value) // Should not change + assertEquals("existingValue", liveMap.data["key1"]?.data?.string) // Should not change assertEquals("serial2", liveMap.data["key1"]?.timeserial) // Should keep original serial } @@ -673,7 +676,7 @@ class LiveMapManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.MapRemove, objectId = "map:testMap@1", - mapOp = ObjectsMapOp(key = "nonExistingKey") + mapRemove = MapRemove(key = "nonExistingKey") ) // RTLM8b - Create tombstoned entry for non-existing key @@ -694,19 +697,19 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "serial2", // Higher than "serial1" - data = ObjectData(value = ObjectValue.String("existingValue")) + data = ObjectData(string = "existingValue") ) val operation = ObjectOperation( action = ObjectOperationAction.MapRemove, objectId = "map:testMap@1", - mapOp = ObjectsMapOp(key = "key1") + mapRemove = MapRemove(key = "key1") ) // RTLM8a - Should skip operation with lower serial liveMapManager.applyOperation(operation, "serial1", null) - assertEquals("existingValue", liveMap.data["key1"]?.data?.value?.value) // Should not change + assertEquals("existingValue", liveMap.data["key1"]?.data?.string) // Should not change assertEquals("serial2", liveMap.data["key1"]?.timeserial) // Should keep original serial assertFalse(liveMap.data["key1"]?.isTombstoned == true) // Should not be tombstoned } @@ -720,22 +723,22 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = null, - data = ObjectData(value = ObjectValue.String("existingValue")) + data = ObjectData(string = "existingValue") ) val operation = ObjectOperation( action = ObjectOperationAction.MapSet, objectId = "map:testMap@1", - mapOp = ObjectsMapOp( + mapSet = MapSet( key = "key1", - data = ObjectData(value = ObjectValue.String("newValue")) + value = ObjectData(string = "newValue") ) ) // RTLM9b - Both null serials should be treated as equal liveMapManager.applyOperation(operation, null, null) - assertEquals("existingValue", liveMap.data["key1"]?.data?.value?.value) // Should not change + assertEquals("existingValue", liveMap.data["key1"]?.data?.string) // Should not change } @Test @@ -747,22 +750,22 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = null, - data = ObjectData(value = ObjectValue.String("existingValue")) + data = ObjectData(string = "existingValue") ) val operation = ObjectOperation( action = ObjectOperationAction.MapSet, objectId = "map:testMap@1", - mapOp = ObjectsMapOp( + mapSet = MapSet( key = "key1", - data = ObjectData(value = ObjectValue.String("newValue")) + value = ObjectData(string = "newValue") ) ) // RTLM9d - Operation serial is greater than missing entry serial liveMapManager.applyOperation(operation, "serial1", null) - assertEquals("newValue", liveMap.data["key1"]?.data?.value?.value) // Should be updated + assertEquals("newValue", liveMap.data["key1"]?.data?.string) // Should be updated assertEquals("serial1", liveMap.data["key1"]?.timeserial) // Should have new serial } @@ -775,22 +778,22 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "serial1", - data = ObjectData(value = ObjectValue.String("existingValue")) + data = ObjectData(string = "existingValue") ) val operation = ObjectOperation( action = ObjectOperationAction.MapSet, objectId = "map:testMap@1", - mapOp = ObjectsMapOp( + mapSet = MapSet( key = "key1", - data = ObjectData(value = ObjectValue.String("newValue")) + value = ObjectData(string = "newValue") ) ) // RTLM9c - Missing operation serial is lower than existing entry serial liveMapManager.applyOperation(operation, null, null) - assertEquals("existingValue", liveMap.data["key1"]?.data?.value?.value) // Should not change + assertEquals("existingValue", liveMap.data["key1"]?.data?.string) // Should not change assertEquals("serial1", liveMap.data["key1"]?.timeserial) // Should keep original serial } @@ -803,22 +806,22 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "serial1", - data = ObjectData(value = ObjectValue.String("existingValue")) + data = ObjectData(string = "existingValue") ) val operation = ObjectOperation( action = ObjectOperationAction.MapSet, objectId = "map:testMap@1", - mapOp = ObjectsMapOp( + mapSet = MapSet( key = "key1", - data = ObjectData(value = ObjectValue.String("newValue")) + value = ObjectData(string = "newValue") ) ) // RTLM9e - Higher serial should be applied liveMapManager.applyOperation(operation, "serial2", null) - assertEquals("newValue", liveMap.data["key1"]?.data?.value?.value) // Should be updated + assertEquals("newValue", liveMap.data["key1"]?.data?.string) // Should be updated assertEquals("serial2", liveMap.data["key1"]?.timeserial) // Should have new serial } @@ -831,22 +834,22 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "serial2", - data = ObjectData(value = ObjectValue.String("existingValue")) + data = ObjectData(string = "existingValue") ) val operation = ObjectOperation( action = ObjectOperationAction.MapSet, objectId = "map:testMap@1", - mapOp = ObjectsMapOp( + mapSet = MapSet( key = "key1", - data = ObjectData(value = ObjectValue.String("newValue")) + value = ObjectData(string = "newValue") ) ) // RTLM9e - Lower serial should be skipped liveMapManager.applyOperation(operation, "serial1", null) - assertEquals("existingValue", liveMap.data["key1"]?.data?.value?.value) // Should not change + assertEquals("existingValue", liveMap.data["key1"]?.data?.string) // Should not change assertEquals("serial2", liveMap.data["key1"]?.timeserial) // Should keep original serial } @@ -858,7 +861,7 @@ class LiveMapManagerTest { val operation = ObjectOperation( action = ObjectOperationAction.MapCreate, objectId = "map:testMap@1", - map = ObjectsMap( + mapCreate = MapCreate( semantics = ObjectsMapSemantics.Unknown, // This should match, but we'll test error case entries = emptyMap() ) @@ -890,7 +893,7 @@ class LiveMapManagerTest { "key1" to LiveMapEntry( isTombstoned = false, timeserial = "1", - data = ObjectData(value = ObjectValue.String("value1")) + data = ObjectData(string = "value1") ) ) val result2 = livemapManager.calculateUpdateFromDataDiff(prevData2, newData2) @@ -901,7 +904,7 @@ class LiveMapManagerTest { "key1" to LiveMapEntry( isTombstoned = false, timeserial = "1", - data = ObjectData(value = ObjectValue.String("value1")) + data = ObjectData(string = "value1") ) ) val newData3 = mapOf() @@ -913,14 +916,14 @@ class LiveMapManagerTest { "key1" to LiveMapEntry( isTombstoned = false, timeserial = "1", - data = ObjectData(value = ObjectValue.String("value1")) + data = ObjectData(string = "value1") ) ) val newData4 = mapOf( "key1" to LiveMapEntry( isTombstoned = false, timeserial = "2", - data = ObjectData(value = ObjectValue.String("value2")) + data = ObjectData(string = "value2") ) ) val result4 = livemapManager.calculateUpdateFromDataDiff(prevData4, newData4) @@ -931,7 +934,7 @@ class LiveMapManagerTest { "key1" to LiveMapEntry( isTombstoned = false, timeserial = "1", - data = ObjectData(value = ObjectValue.String("value1")) + data = ObjectData(string = "value1") ) ) val newData5 = mapOf( @@ -956,7 +959,7 @@ class LiveMapManagerTest { "key1" to LiveMapEntry( isTombstoned = false, timeserial = "2", - data = ObjectData(value = ObjectValue.String("value1")) + data = ObjectData(string = "value1") ) ) val result6 = livemapManager.calculateUpdateFromDataDiff(prevData6, newData6) @@ -974,7 +977,7 @@ class LiveMapManagerTest { "key1" to LiveMapEntry( isTombstoned = true, timeserial = "2", - data = ObjectData(value = ObjectValue.String("value1")) + data = ObjectData(string = "value1") ) ) val result7 = livemapManager.calculateUpdateFromDataDiff(prevData7, newData7) @@ -997,24 +1000,24 @@ class LiveMapManagerTest { "key1" to LiveMapEntry( isTombstoned = false, timeserial = "1", - data = ObjectData(value = ObjectValue.String("value1")) + data = ObjectData(string = "value1") ), "key2" to LiveMapEntry( isTombstoned = false, timeserial = "1", - data = ObjectData(value = ObjectValue.String("value2")) + data = ObjectData(string = "value2") ) ) val newData9 = mapOf( "key1" to LiveMapEntry( isTombstoned = false, timeserial = "2", - data = ObjectData(value = ObjectValue.String("value1_updated")) + data = ObjectData(string = "value1_updated") ), "key3" to LiveMapEntry( isTombstoned = false, timeserial = "1", - data = ObjectData(value = ObjectValue.String("value3")) + data = ObjectData(string = "value3") ) ) val result9 = livemapManager.calculateUpdateFromDataDiff(prevData9, newData9) @@ -1048,14 +1051,14 @@ class LiveMapManagerTest { "key1" to LiveMapEntry( isTombstoned = false, timeserial = "1", - data = ObjectData(value = ObjectValue.String("value1")) + data = ObjectData(string = "value1") ) ) val newData11 = mapOf( "key1" to LiveMapEntry( isTombstoned = false, timeserial = "2", - data = ObjectData(value = ObjectValue.String("value1")) + data = ObjectData(string = "value1") ) ) val result11 = livemapManager.calculateUpdateFromDataDiff(prevData11, newData11) @@ -1071,7 +1074,7 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "1", - data = ObjectData(value = ObjectValue.String("oldValue")) + data = ObjectData(string = "oldValue") ) val expectedTimestamp = 1234567890L @@ -1102,7 +1105,7 @@ class LiveMapManagerTest { liveMap.data["key1"] = LiveMapEntry( isTombstoned = false, timeserial = "1", - data = ObjectData(value = ObjectValue.String("oldValue")) + data = ObjectData(string = "oldValue") ) val objectState = ObjectState(