Skip to content

Commit b02cfd7

Browse files
committed
[ECO-5482] Implemented realtime write methods for LiveMap and LiveCounter
- Added integration test performing counter ops using realtime write API - Added integration test performing map ops using realtime write API
1 parent 814ac2f commit b02cfd7

8 files changed

Lines changed: 267 additions & 33 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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 Object value, @NotNull Callback<Void> callback);
115+
void setAsync(@NotNull String keyName, @NotNull LiveMapValue value, @NotNull Callback<Void> callback);
116116

117117
/**
118118
* Asynchronously removes the specified key and its associated value from the map.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ internal class DefaultLiveObjects(internal val channelName: String, internal val
5353
/**
5454
* Provides a channel-specific scope for safely executing asynchronous operations with callbacks.
5555
*/
56-
private val asyncScope = ObjectsAsyncScope(channelName)
56+
internal val asyncScope = ObjectsAsyncScope(channelName)
5757

5858
init {
5959
incomingObjectsHandler = initializeHandlerForIncomingObjectMessages()

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

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

3-
import io.ably.lib.objects.type.BaseLiveObject
4-
import io.ably.lib.objects.type.map.LiveMapValue
53
import io.ably.lib.realtime.ChannelState
64
import io.ably.lib.realtime.CompletionListener
75
import io.ably.lib.types.ChannelMode
@@ -113,10 +111,3 @@ internal data class CounterCreatePayload(
113111
internal data class MapCreatePayload(
114112
val map: ObjectMap
115113
)
116-
117-
internal fun fromLiveMapValue(value: LiveMapValue) : ObjectData {
118-
return when {
119-
value.isLiveMap || value.isLiveCounter -> ObjectData(objectId = (value.value as BaseLiveObject).objectId)
120-
else -> ObjectData(value = ObjectValue(value.value))
121-
}
122-
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,20 @@ internal class ObjectsAsyncScope(channelName: String) {
7575
}
7676
}
7777

78+
internal fun launchWithVoidCallback(callback: Callback<Void>, block: suspend () -> Unit) {
79+
scope.launch {
80+
try {
81+
block()
82+
try { callback.onSuccess(null) } catch (t: Throwable) {
83+
Log.e(tag, "Error occurred while executing callback's onSuccess handler", t)
84+
} // catch and don't rethrow error from callback
85+
} catch (throwable: Throwable) {
86+
val exception = throwable as? AblyException
87+
callback.onError(exception?.errorInfo)
88+
}
89+
}
90+
}
91+
7892
internal fun cancel(cause: CancellationException) {
7993
scope.coroutineContext.cancelChildren(cause)
8094
}

live-objects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import io.ably.lib.objects.type.noOp
1313
import io.ably.lib.types.Callback
1414
import java.util.concurrent.atomic.AtomicReference
1515
import io.ably.lib.util.Log
16+
import kotlinx.coroutines.runBlocking
1617

1718
/**
1819
* Implementation of LiveObject for LiveCounter.
@@ -39,21 +40,18 @@ internal class DefaultLiveCounter private constructor(
3940

4041
private val channelName = liveObjects.channelName
4142
private val adapter: LiveObjectsAdapter get() = liveObjects.adapter
43+
private val asyncScope get() = liveObjects.asyncScope
4244

43-
override fun increment(amount: Number) {
44-
TODO("Not yet implemented")
45-
}
45+
override fun increment(amount: Number) = runBlocking { incrementAsync(amount.toDouble()) }
4646

47-
override fun decrement(amount: Number) {
48-
TODO("Not yet implemented")
49-
}
47+
override fun decrement(amount: Number) = runBlocking { incrementAsync(-amount.toDouble()) }
5048

5149
override fun incrementAsync(amount: Number, callback: Callback<Void>) {
52-
TODO("Not yet implemented")
50+
asyncScope.launchWithVoidCallback(callback) { incrementAsync(amount.toDouble()) }
5351
}
5452

5553
override fun decrementAsync(amount: Number, callback: Callback<Void>) {
56-
TODO("Not yet implemented")
54+
asyncScope.launchWithVoidCallback(callback) { incrementAsync(-amount.toDouble()) }
5755
}
5856

5957
override fun value(): Double {
@@ -72,6 +70,28 @@ internal class DefaultLiveCounter private constructor(
7270

7371
override fun validate(state: ObjectState) = liveCounterManager.validate(state)
7472

73+
private suspend fun incrementAsync(amount: Double) {
74+
// Validate write API configuration
75+
adapter.throwIfInvalidWriteApiConfiguration(channelName)
76+
77+
// Validate input parameter
78+
if (amount.isNaN() || amount.isInfinite()) {
79+
throw objectError("Counter value increment should be a valid number")
80+
}
81+
82+
// Create ObjectMessage with the COUNTER_INC operation
83+
val msg = ObjectMessage(
84+
operation = ObjectOperation(
85+
action = ObjectOperationAction.CounterInc,
86+
objectId = objectId,
87+
counterOp = ObjectCounterOp(amount = amount)
88+
)
89+
)
90+
91+
// Publish the message
92+
liveObjects.publish(arrayOf(msg))
93+
}
94+
7595
override fun applyObjectState(objectState: ObjectState, message: ObjectMessage): LiveCounterUpdate {
7696
return liveCounterManager.applyState(objectState, message.serialTimestamp)
7797
}

live-objects/src/main/kotlin/io/ably/lib/objects/type/livemap/DefaultLiveMap.kt

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import io.ably.lib.objects.type.map.LiveMapValue
1515
import io.ably.lib.objects.type.noOp
1616
import io.ably.lib.types.Callback
1717
import io.ably.lib.util.Log
18+
import kotlinx.coroutines.runBlocking
1819
import java.util.concurrent.ConcurrentHashMap
1920
import java.util.AbstractMap
2021

@@ -44,6 +45,7 @@ internal class DefaultLiveMap private constructor(
4445
private val channelName = liveObjects.channelName
4546
private val adapter: LiveObjectsAdapter get() = liveObjects.adapter
4647
internal val objectsPool: ObjectsPool get() = liveObjects.objectsPool
48+
private val asyncScope get() = liveObjects.asyncScope
4749

4850
override fun get(keyName: String): LiveMapValue? {
4951
adapter.throwIfInvalidAccessApiConfiguration(channelName) // RTLM5b, RTLM5c
@@ -92,20 +94,16 @@ internal class DefaultLiveMap private constructor(
9294
return data.values.count { !it.isEntryOrRefTombstoned(objectsPool) }.toLong() // RTLM10d
9395
}
9496

95-
override fun set(keyName: String, value: LiveMapValue) {
96-
TODO("Not yet implemented")
97-
}
97+
override fun set(keyName: String, value: LiveMapValue) = runBlocking { setAsync(keyName, value) }
9898

99-
override fun remove(keyName: String) {
100-
TODO("Not yet implemented")
101-
}
99+
override fun remove(keyName: String) = runBlocking { removeAsync(keyName) }
102100

103-
override fun setAsync(keyName: String, value: Any, callback: Callback<Void>) {
104-
TODO("Not yet implemented")
101+
override fun setAsync(keyName: String, value: LiveMapValue, callback: Callback<Void>) {
102+
asyncScope.launchWithVoidCallback(callback) { setAsync(keyName, value) }
105103
}
106104

107105
override fun removeAsync(keyName: String, callback: Callback<Void>) {
108-
TODO("Not yet implemented")
106+
asyncScope.launchWithVoidCallback(callback) { removeAsync(keyName) }
109107
}
110108

111109
override fun validate(state: ObjectState) = liveMapManager.validate(state)
@@ -119,6 +117,53 @@ internal class DefaultLiveMap private constructor(
119117

120118
override fun unsubscribeAll() = liveMapManager.unsubscribeAll()
121119

120+
private suspend fun setAsync(keyName: String, value: LiveMapValue) {
121+
// Validate write API configuration
122+
adapter.throwIfInvalidWriteApiConfiguration(channelName)
123+
124+
// Validate input parameters
125+
if (keyName.isEmpty()) {
126+
throw objectError("Map key should not be empty")
127+
}
128+
129+
// Create ObjectMessage with the MAP_SET operation
130+
val msg = ObjectMessage(
131+
operation = ObjectOperation(
132+
action = ObjectOperationAction.MapSet,
133+
objectId = objectId,
134+
mapOp = ObjectMapOp(
135+
key = keyName,
136+
data = fromLiveMapValue(value)
137+
)
138+
)
139+
)
140+
141+
// Publish the message
142+
liveObjects.publish(arrayOf(msg))
143+
}
144+
145+
private suspend fun removeAsync(keyName: String) {
146+
// Validate write API configuration
147+
adapter.throwIfInvalidWriteApiConfiguration(channelName)
148+
149+
// Validate input parameter
150+
if (keyName.isEmpty()) {
151+
throw objectError("Map key should not be empty")
152+
}
153+
154+
// Create ObjectMessage with the MAP_REMOVE operation
155+
val msg = ObjectMessage(
156+
operation = ObjectOperation(
157+
action = ObjectOperationAction.MapRemove,
158+
objectId = objectId,
159+
mapOp = ObjectMapOp(key = keyName)
160+
)
161+
)
162+
163+
// Publish the message
164+
liveObjects.publish(arrayOf(msg))
165+
}
166+
122167
override fun applyObjectState(objectState: ObjectState, message: ObjectMessage): LiveMapUpdate {
123168
return liveMapManager.applyState(objectState, message.serialTimestamp)
124169
}
@@ -169,5 +214,12 @@ internal class DefaultLiveMap private constructor(
169214
)
170215
)
171216
}
217+
218+
private fun fromLiveMapValue(value: LiveMapValue) : ObjectData {
219+
return when {
220+
value.isLiveMap || value.isLiveCounter -> ObjectData(objectId = (value.value as BaseLiveObject).objectId)
221+
else -> ObjectData(value = ObjectValue(value.value))
222+
}
223+
}
172224
}
173225
}

live-objects/src/test/kotlin/io/ably/lib/objects/integration/DefaultLiveCounterTest.kt

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.ably.lib.objects.integration.helpers.ObjectId
55
import io.ably.lib.objects.integration.helpers.fixtures.createUserEngagementMatrixMap
66
import io.ably.lib.objects.integration.helpers.fixtures.createUserMapWithCountersObject
77
import io.ably.lib.objects.integration.setup.IntegrationTest
8+
import io.ably.lib.objects.type.map.LiveMapValue
89
import kotlinx.coroutines.test.runTest
910
import org.junit.Test
1011
import kotlin.test.assertEquals
@@ -212,8 +213,8 @@ class DefaultLiveCounterTest: IntegrationTest() {
212213
val rootMap = channel.objects.root
213214

214215
// Step 1: Create a new counter with initial value of 10
215-
val testCounterObjectId = objects.createCounter( 10.0)
216-
restObjects.setMapRef(channelName, "root", "testCounter", testCounterObjectId.ObjectId)
216+
val testCounterObject = objects.createCounter( 10.0)
217+
rootMap.set("testCounter", LiveMapValue.of(testCounterObject))
217218

218219
// Wait for updated testCounter to be available in the root map
219220
assertWaiter { rootMap.get("testCounter") != null }
@@ -222,6 +223,77 @@ class DefaultLiveCounterTest: IntegrationTest() {
222223
val testCounter = rootMap.get("testCounter")?.asLiveCounter
223224
assertNotNull(testCounter, "Test counter should be created and accessible")
224225
assertEquals(10.0, testCounter.value(), "Counter should have initial value of 10")
226+
227+
// Step 2: Increment counter by 5 (10 + 5 = 15)
228+
testCounter.increment(5.0)
229+
// Wait for the counter to be updated
230+
assertWaiter { testCounter.value() == 15.0 }
231+
232+
// Assert after first increment
233+
assertEquals(15.0, testCounter.value(), "Counter should be incremented to 15")
234+
235+
// Step 3: Increment counter by 3 (15 + 3 = 18)
236+
testCounter.increment(3.0)
237+
// Wait for the counter to be updated
238+
assertWaiter { testCounter.value() == 18.0 }
239+
240+
// Assert after second increment
241+
assertEquals(18.0, testCounter.value(), "Counter should be incremented to 18")
242+
243+
// Step 4: Increment counter by a larger amount: 12 (18 + 12 = 30)
244+
testCounter.increment(12.0)
245+
// Wait for the counter to be updated
246+
assertWaiter { testCounter.value() == 30.0 }
247+
248+
// Assert after third increment
249+
assertEquals(30.0, testCounter.value(), "Counter should be incremented to 30")
250+
251+
// Step 5: Decrement counter by 7 (30 - 7 = 23)
252+
testCounter.decrement(7.0)
253+
// Wait for the counter to be updated
254+
assertWaiter { testCounter.value() == 23.0 }
255+
256+
// Assert after first decrement
257+
assertEquals(23.0, testCounter.value(), "Counter should be decremented to 23")
258+
259+
// Step 6: Decrement counter by 4 (23 - 4 = 19)
260+
testCounter.decrement(4.0)
261+
// Wait for the counter to be updated
262+
assertWaiter { testCounter.value() == 19.0 }
263+
264+
// Assert after second decrement
265+
assertEquals(19.0, testCounter.value(), "Counter should be decremented to 19")
266+
267+
// Step 7: Increment counter by 1 (19 + 1 = 20)
268+
testCounter.increment(1.0)
269+
// Wait for the counter to be updated
270+
assertWaiter { testCounter.value() == 20.0 }
271+
272+
// Assert after final increment
273+
assertEquals(20.0, testCounter.value(), "Counter should be incremented to 20")
274+
275+
// Step 8: Decrement counter by a larger amount: 15 (20 - 15 = 5)
276+
testCounter.decrement(15.0)
277+
// Wait for the counter to be updated
278+
assertWaiter { testCounter.value() == 5.0 }
279+
280+
// Assert after large decrement
281+
assertEquals(5.0, testCounter.value(), "Counter should be decremented to 5")
282+
283+
// Final verification - test final increment to ensure counter still works
284+
testCounter.increment(25.0)
285+
assertWaiter { testCounter.value() == 30.0 }
286+
287+
// Assert final state
288+
assertEquals(30.0, testCounter.value(), "Counter should have final value of 30")
289+
290+
// Verify the counter object is still accessible and functioning
291+
assertNotNull(testCounter, "Counter should still be accessible at the end")
292+
293+
// Verify we can still access it from the root map
294+
val finalCounterCheck = rootMap.get("testCounter")?.asLiveCounter
295+
assertNotNull(finalCounterCheck, "Counter should still be accessible from root map")
296+
assertEquals(30.0, finalCounterCheck.value(), "Final counter value should be 30 when accessed from root map")
225297
}
226298

227299
@Test

0 commit comments

Comments
 (0)