Skip to content

Commit 6ad594d

Browse files
committed
[ECO-5457] Moved callbackScope at global level to be shared amongst all public api methods
1 parent 67c2b62 commit 6ad594d

4 files changed

Lines changed: 35 additions & 36 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,15 @@ public interface LiveObjectsPlugin {
4646
* Disposes of the LiveObjects instance associated with the specified channel name.
4747
* This method removes the LiveObjects instance for the given channel, releasing any
4848
* resources associated with it.
49+
* This is invoked when ablyRealtimeClient.channels.release(channelName) is called
4950
*
5051
* @param channelName the name of the channel whose LiveObjects instance is to be removed.
5152
*/
5253
void dispose(@NotNull String channelName);
5354

5455
/**
5556
* Disposes of the plugin instance and all underlying resources.
57+
* This is invoked when ablyRealtimeClient.close() is called
5658
*/
5759
void dispose();
5860
}

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

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import io.ably.lib.util.Log
99
import kotlinx.coroutines.*
1010
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
1111
import kotlinx.coroutines.flow.MutableSharedFlow
12+
import java.util.concurrent.CancellationException
1213

1314
/**
1415
* Default implementation of LiveObjects interface.
@@ -34,12 +35,6 @@ internal class DefaultLiveObjects(private val channelName: String, internal val
3435
private val sequentialScope =
3536
CoroutineScope(Dispatchers.Default.limitedParallelism(1) + CoroutineName(channelName) + SupervisorJob())
3637

37-
/**
38-
* Coroutine scope for handling callbacks asynchronously.
39-
*/
40-
private val callbackScope =
41-
CoroutineScope(Dispatchers.Default + CoroutineName("LiveObjectsCallback-$channelName") + SupervisorJob())
42-
4338
/**
4439
* Event bus for handling incoming object messages sequentially.
4540
*/
@@ -55,7 +50,7 @@ internal class DefaultLiveObjects(private val channelName: String, internal val
5550
}
5651

5752
override fun getRootAsync(callback: Callback<LiveMap>) {
58-
callbackScope.launchWithCallback(callback) { getRootAsync() }
53+
GlobalCallbackScope.launchWithCallback(callback) { getRootAsync() }
5954
}
6055

6156
override fun createMap(liveMap: LiveMap): LiveMap {
@@ -97,12 +92,10 @@ internal class DefaultLiveObjects(private val channelName: String, internal val
9792

9893
override fun offAll() = objectsManager.offAll()
9994

100-
private suspend fun getRootAsync(): LiveMap {
101-
return sequentialScope.async {
102-
adapter.throwIfInvalidAccessApiConfiguration(channelName)
103-
objectsManager.ensureSynced(state)
104-
objectsPool.get(ROOT_OBJECT_ID) as LiveMap
105-
}.await()
95+
private suspend fun getRootAsync(): LiveMap = withContext(sequentialScope.coroutineContext) {
96+
adapter.throwIfInvalidAccessApiConfiguration(channelName)
97+
objectsManager.ensureSynced(state)
98+
objectsPool.get(ROOT_OBJECT_ID) as LiveMap
10699
}
107100

108101
/**
@@ -188,9 +181,13 @@ internal class DefaultLiveObjects(private val channelName: String, internal val
188181
}
189182

190183
// Dispose of any resources associated with this LiveObjects instance
191-
fun dispose() {
192-
incomingObjectsHandler.cancel() // objectsEventBus automatically garbage collected when collector is cancelled
184+
// Called when either connection is closed, channel is released, so channel goes into detached state.
185+
fun dispose(reason: String) {
186+
val cancellationError = CancellationException("Objects disposed for channel $channelName, reason: $reason")
187+
incomingObjectsHandler.cancel(cancellationError) // objectsEventBus automatically garbage collected when collector is cancelled
193188
objectsPool.dispose()
194189
objectsManager.dispose()
190+
// Don't cancel sequentialScope (needed in public methods), just cancel ongoing coroutines
191+
sequentialScope.coroutineContext.cancelChildren(cancellationError)
195192
}
196193
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ public class DefaultLiveObjectsPlugin(private val adapter: LiveObjectsAdapter) :
2222
}
2323

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

2929
override fun dispose() {
3030
liveObjects.values.forEach {
31-
it.dispose()
31+
it.dispose("AblyClient has been closed using client.close()")
3232
}
3333
liveObjects.clear()
3434
}

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

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -49,26 +49,26 @@ internal val String.byteSize: Int
4949
get() = this.toByteArray(Charsets.UTF_8).size
5050

5151
/**
52-
* Executes a suspend function within a coroutine and handles the result via a callback.
53-
*
54-
* This utility bridges between coroutine-based implementation code and callback-based APIs.
55-
* It launches a coroutine in the current scope to execute the provided suspend block,
56-
* then routes the result or any error to the appropriate callback method.
57-
*
58-
* @param T The type of result expected from the operation
59-
* @param callback The callback to invoke with the operation result or error
60-
* @param block The suspend function to execute that returns a value of type T
52+
* A global coroutine scope for executing callbacks asynchronously.
53+
* Provides safe execution of suspend functions with results delivered via callbacks,
54+
* with proper error handling for both the execution and callback invocation.
6155
*/
62-
internal fun <T> CoroutineScope.launchWithCallback(callback: Callback<T>, block: suspend () -> T) {
63-
launch {
64-
try {
65-
val result = block()
66-
try { callback.onSuccess(result) } catch (t: Throwable) {
67-
Log.e("asyncCallback", "Error occurred while executing callback's onSuccess handler", t)
68-
} // catch and don't rethrow error from callback
69-
} catch (throwable: Throwable) {
70-
val exception = throwable as? AblyException
71-
callback.onError(exception?.errorInfo)
56+
internal object GlobalCallbackScope {
57+
private const val TAG = "GlobalCallbackScope"
58+
private val scope =
59+
CoroutineScope(Dispatchers.Default + CoroutineName("LiveObjects-GlobalCallbackScope") + SupervisorJob())
60+
61+
internal fun <T> launchWithCallback(callback: Callback<T>, block: suspend () -> T) {
62+
scope.launch {
63+
try {
64+
val result = block()
65+
try { callback.onSuccess(result) } catch (t: Throwable) {
66+
Log.e(TAG, "Error occurred while executing callback's onSuccess handler", t)
67+
} // catch and don't rethrow error from callback
68+
} catch (throwable: Throwable) {
69+
val exception = throwable as? AblyException
70+
callback.onError(exception?.errorInfo)
71+
}
7272
}
7373
}
7474
}

0 commit comments

Comments
 (0)