Skip to content

Commit ef1b3d7

Browse files
committed
Merge branch 'feature/server-provided-tombstone-serial' into feature/liveobjects-realtime-write-api
# Conflicts: # lib/src/main/java/io/ably/lib/objects/LiveObjects.java # lib/src/main/java/io/ably/lib/objects/type/counter/LiveCounter.java # lib/src/main/java/io/ably/lib/objects/type/map/LiveMap.java # live-objects/src/main/kotlin/io/ably/lib/objects/DefaultLiveObjects.kt # live-objects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt # live-objects/src/main/kotlin/io/ably/lib/objects/type/livemap/DefaultLiveMap.kt # live-objects/src/test/kotlin/io/ably/lib/objects/integration/DefaultLiveObjectsTest.kt # live-objects/src/test/kotlin/io/ably/lib/objects/unit/UtilsTest.kt
2 parents 825480b + 7023ff6 commit ef1b3d7

25 files changed

Lines changed: 406 additions & 268 deletions

lib/src/main/java/io/ably/lib/objects/LiveObjects.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public interface LiveObjects extends ObjectsStateChange {
112112
* @param callback the callback to handle the result or error.
113113
*/
114114
@NonBlocking
115-
void getRootAsync(@NotNull Callback<@NotNull LiveMap> callback);
115+
void getRootAsync(@NotNull ObjectsCallback<@NotNull LiveMap> callback);
116116

117117
/**
118118
* Asynchronously creates a new empty LiveMap with no entries.
@@ -124,7 +124,7 @@ public interface LiveObjects extends ObjectsStateChange {
124124
* @param callback the callback to handle the result or error.
125125
*/
126126
@NonBlocking
127-
void createMapAsync(@NotNull Callback<@NotNull LiveMap> callback);
127+
void createMapAsync(@NotNull ObjectsCallback<@NotNull LiveMap> callback);
128128

129129
/**
130130
* Asynchronously creates a new LiveMap with type-safe entries that can be Boolean, Binary, Number, String, JsonArray, JsonObject, LiveCounter, or LiveMap.
@@ -138,7 +138,7 @@ public interface LiveObjects extends ObjectsStateChange {
138138
* @param callback the callback to handle the result or error.
139139
*/
140140
@NonBlocking
141-
void createMapAsync(@NotNull Map<String, LiveMapValue> entries, @NotNull Callback<@NotNull LiveMap> callback);
141+
void createMapAsync(@NotNull Map<String, LiveMapValue> entries, @NotNull ObjectsCallback<@NotNull LiveMap> callback);
142142

143143
/**
144144
* Asynchronously creates a new LiveCounter with an initial value of 0.
@@ -150,7 +150,7 @@ public interface LiveObjects extends ObjectsStateChange {
150150
* @param callback the callback to handle the result or error.
151151
*/
152152
@NonBlocking
153-
void createCounterAsync(@NotNull Callback<@NotNull LiveCounter> callback);
153+
void createCounterAsync(@NotNull ObjectsCallback<@NotNull LiveCounter> callback);
154154

155155
/**
156156
* Asynchronously creates a new LiveCounter with an initial value.
@@ -163,5 +163,5 @@ public interface LiveObjects extends ObjectsStateChange {
163163
* @param callback the callback to handle the result or error.
164164
*/
165165
@NonBlocking
166-
void createCounterAsync(@NotNull Number initialValue, @NotNull Callback<@NotNull LiveCounter> callback);
166+
void createCounterAsync(@NotNull Number initialValue, @NotNull ObjectsCallback<@NotNull LiveCounter> callback);
167167
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.ably.lib.objects;
2+
3+
import io.ably.lib.types.AblyException;
4+
5+
/**
6+
* Callback interface for handling results of asynchronous LiveObjects operations.
7+
* Used for operations like creating LiveMaps/LiveCounters, modifying entries, and retrieving objects.
8+
* Callbacks are executed on background threads managed by the LiveObjects system.
9+
*
10+
* @param <T> the type of the result returned by the asynchronous operation
11+
*/
12+
public interface ObjectsCallback<T> {
13+
14+
/**
15+
* Called when the asynchronous operation completes successfully.
16+
* For modification operations (set, remove, increment), result is typically Void.
17+
* For creation/retrieval operations, result contains the created/retrieved object.
18+
*
19+
* @param result the result of the operation, may be null for modification operations
20+
*/
21+
void onSuccess(T result);
22+
23+
/**
24+
* Called when the asynchronous operation fails.
25+
* The exception contains detailed error information including error codes and messages.
26+
* Common errors include network issues, authentication failures, and validation errors.
27+
*
28+
* @param exception the exception that occurred during the operation
29+
*/
30+
void onError(AblyException exception);
31+
}

lib/src/main/java/io/ably/lib/objects/type/counter/LiveCounter.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package io.ably.lib.objects.type.counter;
22

3-
import io.ably.lib.types.Callback;
3+
import io.ably.lib.objects.ObjectsCallback;
44
import org.jetbrains.annotations.Blocking;
55
import org.jetbrains.annotations.NonBlocking;
66
import org.jetbrains.annotations.NotNull;
@@ -48,18 +48,18 @@ public interface LiveCounter extends LiveCounterChange {
4848
* @param callback the callback to be invoked upon completion of the operation.
4949
*/
5050
@NonBlocking
51-
void incrementAsync(@NotNull Number amount, @NotNull Callback<Void> callback);
51+
void incrementAsync(@NotNull Number amount, @NotNull ObjectsCallback<Void> callback);
5252

5353
/**
5454
* Decrements the value of the counter by the specified amount asynchronously.
55-
* An alias for calling {@link LiveCounter#incrementAsync(Number, Callback)} with a negative amount.
55+
* An alias for calling {@link LiveCounter#incrementAsync(Number, ObjectsCallback)} with a negative amount.
5656
* Spec: RTLC13
5757
*
5858
* @param amount the amount by which to decrement the counter
5959
* @param callback the callback to be invoked upon completion of the operation.
6060
*/
6161
@NonBlocking
62-
void decrementAsync(@NotNull Number amount, @NotNull Callback<Void> callback);
62+
void decrementAsync(@NotNull Number amount, @NotNull ObjectsCallback<Void> callback);
6363

6464
/**
6565
* Retrieves the current value of the counter.

lib/src/main/java/io/ably/lib/objects/type/map/LiveMap.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package io.ably.lib.objects.type.map;
22

3-
import io.ably.lib.types.Callback;
3+
import io.ably.lib.objects.ObjectsCallback;
44
import org.jetbrains.annotations.Blocking;
55
import org.jetbrains.annotations.NonBlocking;
66
import org.jetbrains.annotations.Contract;
@@ -112,7 +112,7 @@ public interface LiveMap extends LiveMapChange {
112112
* @param callback the callback to handle the result or any errors.
113113
*/
114114
@NonBlocking
115-
void setAsync(@NotNull String keyName, @NotNull LiveMapValue value, @NotNull Callback<Void> callback);
115+
void setAsync(@NotNull String keyName, @NotNull LiveMapValue value, @NotNull ObjectsCallback<Void> callback);
116116

117117
/**
118118
* Asynchronously removes the specified key and its associated value from the map.
@@ -126,5 +126,5 @@ public interface LiveMap extends LiveMapChange {
126126
* @param callback the callback to handle the result or any errors.
127127
*/
128128
@NonBlocking
129-
void removeAsync(@NotNull String keyName, @NotNull Callback<Void> callback);
129+
void removeAsync(@NotNull String keyName, @NotNull ObjectsCallback<Void> callback);
130130
}

live-objects/src/main/kotlin/io/ably/lib/objects/DefaultLiveObjects.kt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import io.ably.lib.objects.type.map.LiveMap
1111
import io.ably.lib.objects.type.map.LiveMapValue
1212
import io.ably.lib.realtime.ChannelState
1313
import io.ably.lib.types.AblyException
14-
import io.ably.lib.types.Callback
1514
import io.ably.lib.types.ProtocolMessage
1615
import io.ably.lib.util.Log
1716
import kotlinx.coroutines.*
@@ -69,19 +68,19 @@ internal class DefaultLiveObjects(internal val channelName: String, internal val
6968

7069
override fun createCounter(initialValue: Number): LiveCounter = runBlocking { createCounterAsync(initialValue) }
7170

72-
override fun getRootAsync(callback: Callback<LiveMap>) {
71+
override fun getRootAsync(callback: ObjectsCallback<LiveMap>) {
7372
asyncScope.launchWithCallback(callback) { getRootAsync() }
7473
}
7574

76-
override fun createMapAsync(callback: Callback<LiveMap>) = createMapAsync(mutableMapOf(), callback)
75+
override fun createMapAsync(callback: ObjectsCallback<LiveMap>) = createMapAsync(mutableMapOf(), callback)
7776

78-
override fun createMapAsync(entries: MutableMap<String, LiveMapValue>, callback: Callback<LiveMap>) {
77+
override fun createMapAsync(entries: MutableMap<String, LiveMapValue>, callback: ObjectsCallback<LiveMap>) {
7978
asyncScope.launchWithCallback(callback) { createMapAsync(entries) }
8079
}
8180

82-
override fun createCounterAsync(callback: Callback<LiveCounter>) = createCounterAsync(0, callback)
81+
override fun createCounterAsync(callback: ObjectsCallback<LiveCounter>) = createCounterAsync(0, callback)
8382

84-
override fun createCounterAsync(initialValue: Number, callback: Callback<LiveCounter>) {
83+
override fun createCounterAsync(initialValue: Number, callback: ObjectsCallback<LiveCounter>) {
8584
asyncScope.launchWithCallback(callback) { createCounterAsync(initialValue) }
8685
}
8786

live-objects/src/main/kotlin/io/ably/lib/objects/DefaultLiveObjectsPlugin.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public class DefaultLiveObjectsPlugin(private val adapter: LiveObjectsAdapter) :
2222
}
2323

2424
override fun dispose(channelName: String) {
25-
liveObjects[channelName]?.dispose(clientError("Channel has ben released using channels.release()"))
25+
liveObjects[channelName]?.dispose(clientError("Channel has been released using channels.release()"))
2626
liveObjects.remove(channelName)
2727
}
2828

live-objects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.ably.lib.objects
22

3-
import com.google.gson.JsonArray
43
import com.google.gson.JsonObject
54

65
import com.google.gson.annotations.JsonAdapter
@@ -52,28 +51,18 @@ internal data class ObjectData(
5251

5352
/**
5453
* Represents a value that can be a String, Number, Boolean, Binary, JsonObject or JsonArray.
55-
* Performs a type check on initialization.
54+
* Provides compile-time type safety through sealed class pattern.
5655
* Spec: OD2c
5756
*/
58-
internal data class ObjectValue(
59-
/**
60-
* The concrete value of the object. Can be a String, Number, Boolean, Binary, JsonObject or JsonArray.
61-
* Spec: OD2c
62-
*/
63-
val value: Any,
64-
) {
65-
init {
66-
require(
67-
value is String ||
68-
value is Number ||
69-
value is Boolean ||
70-
value is Binary ||
71-
value is JsonObject ||
72-
value is JsonArray
73-
) {
74-
"value must be String, Number, Boolean, Binary, JsonObject or JsonArray"
75-
}
76-
}
57+
internal sealed class ObjectValue {
58+
abstract val value: Any
59+
60+
data class String(override val value: kotlin.String) : ObjectValue()
61+
data class Number(override val value: kotlin.Number) : ObjectValue()
62+
data class Boolean(override val value: kotlin.Boolean) : ObjectValue()
63+
data class Binary(override val value: io.ably.lib.objects.Binary) : ObjectValue()
64+
data class JsonObject(override val value: com.google.gson.JsonObject) : ObjectValue()
65+
data class JsonArray(override val value: com.google.gson.JsonArray) : ObjectValue()
7766
}
7867

7968
/**
@@ -456,13 +445,12 @@ private fun ObjectData.size(): Int {
456445
* Spec: OD3*
457446
*/
458447
private fun ObjectValue.size(): Int {
459-
return when (value) {
460-
is Boolean -> 1 // Spec: OD3b
461-
is Binary -> value.size() // Spec: OD3c
462-
is Number -> 8 // Spec: OD3d
463-
is String -> value.byteSize // Spec: OD3e
464-
is JsonObject, is JsonArray -> value.toString().byteSize // Spec: OD3e
465-
else -> 0 // Spec: OD3f
448+
return when (this) {
449+
is ObjectValue.Boolean -> 1 // Spec: OD3b
450+
is ObjectValue.Binary -> value.size() // Spec: OD3c
451+
is ObjectValue.Number -> 8 // Spec: OD3d
452+
is ObjectValue.String -> value.byteSize // Spec: OD3e
453+
is ObjectValue.JsonObject, is ObjectValue.JsonArray -> value.toString().byteSize // Spec: OD3e
466454
}
467455
}
468456

live-objects/src/main/kotlin/io/ably/lib/objects/Utils.kt

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package io.ably.lib.objects
22

33
import io.ably.lib.types.AblyException
4-
import io.ably.lib.types.Callback
54
import io.ably.lib.types.ErrorInfo
65
import io.ably.lib.util.Log
76
import kotlinx.coroutines.*
@@ -61,30 +60,40 @@ internal class ObjectsAsyncScope(channelName: String) {
6160
private val scope =
6261
CoroutineScope(Dispatchers.Default + CoroutineName(tag) + SupervisorJob())
6362

64-
internal fun <T> launchWithCallback(callback: Callback<T>, block: suspend () -> T) {
63+
internal fun <T> launchWithCallback(callback: ObjectsCallback<T>, block: suspend () -> T) {
6564
scope.launch {
6665
try {
6766
val result = block()
6867
try { callback.onSuccess(result) } catch (t: Throwable) {
6968
Log.e(tag, "Error occurred while executing callback's onSuccess handler", t)
7069
} // catch and don't rethrow error from callback
7170
} catch (throwable: Throwable) {
72-
val exception = throwable as? AblyException
73-
callback.onError(exception?.errorInfo)
71+
when (throwable) {
72+
is AblyException -> { callback.onError(throwable) }
73+
else -> {
74+
val ex = ablyException("Error executing operation", ErrorCode.BadRequest, cause = throwable)
75+
callback.onError(ex)
76+
}
77+
}
7478
}
7579
}
7680
}
7781

78-
internal fun launchWithVoidCallback(callback: Callback<Void>, block: suspend () -> Unit) {
82+
internal fun launchWithVoidCallback(callback: ObjectsCallback<Void>, block: suspend () -> Unit) {
7983
scope.launch {
8084
try {
8185
block()
8286
try { callback.onSuccess(null) } catch (t: Throwable) {
8387
Log.e(tag, "Error occurred while executing callback's onSuccess handler", t)
8488
} // catch and don't rethrow error from callback
8589
} catch (throwable: Throwable) {
86-
val exception = throwable as? AblyException
87-
callback.onError(exception?.errorInfo)
90+
when (throwable) {
91+
is AblyException -> { callback.onError(throwable) }
92+
else -> {
93+
val ex = ablyException("Error executing operation", ErrorCode.BadRequest, cause = throwable)
94+
callback.onError(ex)
95+
}
96+
}
8897
}
8998
}
9099
}

live-objects/src/main/kotlin/io/ably/lib/objects/serialization/JsonSerialization.kt

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,13 @@ internal class ObjectDataJsonSerializer : JsonSerializer<ObjectData>, JsonDeseri
4646
val obj = JsonObject()
4747
src.objectId?.let { obj.addProperty("objectId", it) }
4848

49-
src.value?.let { value ->
50-
when (val v = value.value) {
51-
is Boolean -> obj.addProperty("boolean", v)
52-
is String -> obj.addProperty("string", v)
53-
is Number -> obj.addProperty("number", v.toDouble())
54-
is Binary -> obj.addProperty("bytes", Base64.getEncoder().encodeToString(v.data))
55-
is JsonObject, is JsonArray -> obj.addProperty("json", v.toString()) // Spec: OD4c5
49+
src.value?.let { v ->
50+
when (v) {
51+
is ObjectValue.Boolean -> obj.addProperty("boolean", v.value)
52+
is ObjectValue.String -> obj.addProperty("string", v.value)
53+
is ObjectValue.Number -> obj.addProperty("number", v.value.toDouble())
54+
is ObjectValue.Binary -> obj.addProperty("bytes", Base64.getEncoder().encodeToString(v.value.data))
55+
is ObjectValue.JsonObject, is ObjectValue.JsonArray -> obj.addProperty("json", v.value.toString()) // Spec: OD4c5
5656
}
5757
}
5858
return obj
@@ -62,11 +62,18 @@ internal class ObjectDataJsonSerializer : JsonSerializer<ObjectData>, JsonDeseri
6262
val obj = if (json.isJsonObject) json.asJsonObject else throw JsonParseException("Expected JsonObject")
6363
val objectId = if (obj.has("objectId")) obj.get("objectId").asString else null
6464
val value = when {
65-
obj.has("boolean") -> ObjectValue(obj.get("boolean").asBoolean)
66-
obj.has("string") -> ObjectValue(obj.get("string").asString)
67-
obj.has("number") -> ObjectValue(obj.get("number").asDouble)
68-
obj.has("bytes") -> ObjectValue(Binary(Base64.getDecoder().decode(obj.get("bytes").asString)))
69-
obj.has("json") -> ObjectValue(JsonParser.parseString(obj.get("json").asString))
65+
obj.has("boolean") -> ObjectValue.Boolean(obj.get("boolean").asBoolean)
66+
obj.has("string") -> ObjectValue.String(obj.get("string").asString)
67+
obj.has("number") -> ObjectValue.Number(obj.get("number").asDouble)
68+
obj.has("bytes") -> ObjectValue.Binary(Binary(Base64.getDecoder().decode(obj.get("bytes").asString)))
69+
obj.has("json") -> {
70+
val jsonElement = JsonParser.parseString(obj.get("json").asString)
71+
when {
72+
jsonElement.isJsonObject -> ObjectValue.JsonObject(jsonElement.asJsonObject)
73+
jsonElement.isJsonArray -> ObjectValue.JsonArray(jsonElement.asJsonArray)
74+
else -> throw JsonParseException("Invalid JSON structure")
75+
}
76+
}
7077
else -> {
7178
if (objectId != null)
7279
null

0 commit comments

Comments
 (0)