diff --git a/lib/src/main/java/io/ably/lib/objects/ObjectsPlugin.java b/lib/src/main/java/io/ably/lib/objects/ObjectsPlugin.java index ef30ab7f8..be149843b 100644 --- a/lib/src/main/java/io/ably/lib/objects/ObjectsPlugin.java +++ b/lib/src/main/java/io/ably/lib/objects/ObjectsPlugin.java @@ -38,7 +38,7 @@ public interface ObjectsPlugin { * * @param channelName the name of the channel whose state has changed. * @param state the new state of the channel. - * @param hasObjects flag indicates whether the channel has any associated live objects. + * @param hasObjects flag indicates whether the channel has any associated objects. */ void handleStateChange(@NotNull String channelName, @NotNull ChannelState state, boolean hasObjects); diff --git a/lib/src/main/java/io/ably/lib/objects/RealtimeObjects.java b/lib/src/main/java/io/ably/lib/objects/RealtimeObjects.java index 950b41bf6..6e111b304 100644 --- a/lib/src/main/java/io/ably/lib/objects/RealtimeObjects.java +++ b/lib/src/main/java/io/ably/lib/objects/RealtimeObjects.java @@ -13,7 +13,7 @@ /** * The RealtimeObjects interface provides methods to interact with live data objects, * such as maps and counters, in a real-time environment. It supports both synchronous - * and asynchronous operations for retrieving and creating live objects. + * and asynchronous operations for retrieving and creating objects. * *

Implementations of this interface must be thread-safe as they may be accessed * from multiple threads concurrently. diff --git a/lib/src/main/java/io/ably/lib/objects/state/ObjectsStateChange.java b/lib/src/main/java/io/ably/lib/objects/state/ObjectsStateChange.java index 7b3a7e1e3..180645f3c 100644 --- a/lib/src/main/java/io/ably/lib/objects/state/ObjectsStateChange.java +++ b/lib/src/main/java/io/ably/lib/objects/state/ObjectsStateChange.java @@ -6,7 +6,7 @@ public interface ObjectsStateChange { /** - * Subscribes to a specific Live Objects synchronization state event. + * Subscribes to a specific Objects synchronization state event. * *

This method registers the provided listener to be notified when the specified * synchronization state event occurs. The returned subscription can be used to @@ -40,7 +40,7 @@ public interface ObjectsStateChange { void offAll(); /** - * Interface for receiving notifications about Live Objects synchronization state changes. + * Interface for receiving notifications about Objects synchronization state changes. *

* Implement this interface and register it with an ObjectsStateEmitter to be notified * when synchronization state transitions occur. diff --git a/lib/src/main/java/io/ably/lib/objects/state/ObjectsStateEvent.java b/lib/src/main/java/io/ably/lib/objects/state/ObjectsStateEvent.java index 4fa01a173..1aa27203a 100644 --- a/lib/src/main/java/io/ably/lib/objects/state/ObjectsStateEvent.java +++ b/lib/src/main/java/io/ably/lib/objects/state/ObjectsStateEvent.java @@ -1,7 +1,7 @@ package io.ably.lib.objects.state; /** - * Represents the synchronization state of Ably Live Objects. + * Represents the synchronization state of Ably Objects. *

* This enum is used to notify listeners about state changes in the synchronization process. * Clients can register an {@link ObjectsStateChange.Listener} to receive these events. diff --git a/lib/src/main/java/io/ably/lib/objects/type/ObjectLifecycleChange.java b/lib/src/main/java/io/ably/lib/objects/type/ObjectLifecycleChange.java new file mode 100644 index 000000000..c8d0f5745 --- /dev/null +++ b/lib/src/main/java/io/ably/lib/objects/type/ObjectLifecycleChange.java @@ -0,0 +1,69 @@ +package io.ably.lib.objects.type; + +import io.ably.lib.objects.ObjectsSubscription; +import org.jetbrains.annotations.NonBlocking; +import org.jetbrains.annotations.NotNull; + +/** + * Interface for managing subscriptions to Object lifecycle events. + *

+ * This interface provides methods to subscribe to and manage notifications about significant lifecycle + * changes that occur to Object, such as deletion. More events can be added in the future. + * Multiple listeners can be registered independently, and each can be managed separately. + *

+ * Lifecycle events are different from data update events - they represent changes + * to the object's existence state rather than changes to the object's data content. + * + * @see ObjectLifecycleEvent for the available lifecycle events + */ +public interface ObjectLifecycleChange { + /** + * Subscribes to a specific Object lifecycle event. + * + *

This method registers the provided listener to be notified when the specified + * lifecycle event occurs. The returned subscription can be used to + * unsubscribe later when the notifications are no longer needed. + * + * @param event the lifecycle event to subscribe to + * @param listener the listener that will be called when the event occurs + * @return a subscription object that can be used to unsubscribe from the event + */ + @NonBlocking + ObjectsSubscription on(@NotNull ObjectLifecycleEvent event, @NotNull ObjectLifecycleChange.Listener listener); + + /** + * Unsubscribes the specified listener from all lifecycle events. + * + *

After calling this method, the provided listener will no longer receive + * any lifecycle event notifications. + * + * @param listener the listener to unregister from all events + */ + @NonBlocking + void off(@NotNull ObjectLifecycleChange.Listener listener); + + /** + * Unsubscribes all listeners from all lifecycle events. + * + *

After calling this method, no listeners will receive any lifecycle + * event notifications until new listeners are registered. + */ + @NonBlocking + void offAll(); + + /** + * Interface for receiving notifications about Object lifecycle changes. + *

+ * Implement this interface and register it with an ObjectLifecycleChange provider + * to be notified when lifecycle events occur, such as object creation or deletion. + */ + @FunctionalInterface + interface Listener { + /** + * Called when a lifecycle event occurs. + * + * @param lifecycleEvent The lifecycle event that occurred + */ + void onLifecycleEvent(@NotNull ObjectLifecycleEvent lifecycleEvent); + } +} diff --git a/lib/src/main/java/io/ably/lib/objects/type/ObjectLifecycleEvent.java b/lib/src/main/java/io/ably/lib/objects/type/ObjectLifecycleEvent.java new file mode 100644 index 000000000..7a2d1aa7d --- /dev/null +++ b/lib/src/main/java/io/ably/lib/objects/type/ObjectLifecycleEvent.java @@ -0,0 +1,16 @@ +package io.ably.lib.objects.type; + +/** + * Represents lifecycle events for an Ably Object. + *

+ * This enum notifies listeners about significant lifecycle changes that occur to an Object during its lifetime. + * Clients can register a {@link ObjectLifecycleChange.Listener} to receive these events. + */ +public enum ObjectLifecycleEvent { + /** + * Indicates that an Object has been deleted (tombstoned). + * Emitted once when the object is tombstoned server-side (i.e., deleted and no longer addressable). + * Not re-emitted during client-side garbage collection of tombstones. + */ + DELETED +} diff --git a/lib/src/main/java/io/ably/lib/objects/type/ObjectUpdate.java b/lib/src/main/java/io/ably/lib/objects/type/ObjectUpdate.java index 6df47cf99..8ee1e1578 100644 --- a/lib/src/main/java/io/ably/lib/objects/type/ObjectUpdate.java +++ b/lib/src/main/java/io/ably/lib/objects/type/ObjectUpdate.java @@ -5,7 +5,7 @@ /** * Abstract base class for all LiveMap/LiveCounter update notifications. * Provides common structure for updates that occur on LiveMap and LiveCounter objects. - * Contains the update data that describes what changed in the live object. + * Contains the update data that describes what changed in the object. * Spec: RTLO4b4 */ public abstract class ObjectUpdate { diff --git a/lib/src/main/java/io/ably/lib/objects/type/counter/LiveCounter.java b/lib/src/main/java/io/ably/lib/objects/type/counter/LiveCounter.java index c23ccc91b..958cf05b1 100644 --- a/lib/src/main/java/io/ably/lib/objects/type/counter/LiveCounter.java +++ b/lib/src/main/java/io/ably/lib/objects/type/counter/LiveCounter.java @@ -1,6 +1,7 @@ package io.ably.lib.objects.type.counter; import io.ably.lib.objects.ObjectsCallback; +import io.ably.lib.objects.type.ObjectLifecycleChange; import org.jetbrains.annotations.Blocking; import org.jetbrains.annotations.NonBlocking; import org.jetbrains.annotations.NotNull; @@ -11,7 +12,7 @@ * It allows incrementing, decrementing, and retrieving the current value of the counter, * both synchronously and asynchronously. */ -public interface LiveCounter extends LiveCounterChange { +public interface LiveCounter extends LiveCounterChange, ObjectLifecycleChange { /** * Increments the value of the counter by the specified amount. diff --git a/lib/src/main/java/io/ably/lib/objects/type/map/LiveMap.java b/lib/src/main/java/io/ably/lib/objects/type/map/LiveMap.java index 46c336360..f180fe168 100644 --- a/lib/src/main/java/io/ably/lib/objects/type/map/LiveMap.java +++ b/lib/src/main/java/io/ably/lib/objects/type/map/LiveMap.java @@ -1,6 +1,7 @@ package io.ably.lib.objects.type.map; import io.ably.lib.objects.ObjectsCallback; +import io.ably.lib.objects.type.ObjectLifecycleChange; import org.jetbrains.annotations.Blocking; import org.jetbrains.annotations.NonBlocking; import org.jetbrains.annotations.Contract; @@ -14,7 +15,7 @@ * The LiveMap interface provides methods to interact with a live, real-time map structure. * It supports both synchronous and asynchronous operations for managing key-value pairs. */ -public interface LiveMap extends LiveMapChange { +public interface LiveMap extends LiveMapChange, ObjectLifecycleChange { /** * Retrieves the value associated with the specified key. diff --git a/live-objects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt b/live-objects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt index 0b00a1680..00401c50e 100644 --- a/live-objects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt +++ b/live-objects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt @@ -20,12 +20,12 @@ import java.util.concurrent.CancellationException /** * Default implementation of RealtimeObjects interface. - * Provides the core functionality for managing live objects on a channel. + * Provides the core functionality for managing objects on a channel. */ internal class DefaultRealtimeObjects(internal val channelName: String, internal val adapter: ObjectsAdapter): RealtimeObjects { private val tag = "DefaultRealtimeObjects" /** - * @spec RTO3 - Objects pool storing all live objects by object ID + * @spec RTO3 - Objects pool storing all objects by object ID */ internal val objectsPool = ObjectsPool(this) diff --git a/live-objects/src/main/kotlin/io/ably/lib/objects/ObjectsPool.kt b/live-objects/src/main/kotlin/io/ably/lib/objects/ObjectsPool.kt index a874d6dd6..28ee839e0 100644 --- a/live-objects/src/main/kotlin/io/ably/lib/objects/ObjectsPool.kt +++ b/live-objects/src/main/kotlin/io/ably/lib/objects/ObjectsPool.kt @@ -28,9 +28,9 @@ internal object ObjectsPoolDefaults { internal const val ROOT_OBJECT_ID = "root" /** - * ObjectsPool manages a pool of live objects for a channel. + * ObjectsPool manages a pool of objects for a channel. * - * @spec RTO3 - Maintains an objects pool for all live objects on the channel + * @spec RTO3 - Maintains an objects pool for all objects on the channel */ internal class ObjectsPool( private val realtimeObjects: DefaultRealtimeObjects @@ -39,7 +39,7 @@ internal class ObjectsPool( /** * ConcurrentHashMap for thread-safe access from public APIs in LiveMap and LiveCounter. - * @spec RTO3a - Pool storing all live objects by object ID + * @spec RTO3a - Pool storing all ably objects by object ID */ private val pool = ConcurrentHashMap() @@ -57,7 +57,7 @@ internal class ObjectsPool( } /** - * Gets a live object from the pool by object ID. + * Gets an object from the pool by object ID. */ internal fun get(objectId: String): BaseRealtimeObject? { return pool[objectId] diff --git a/live-objects/src/main/kotlin/io/ably/lib/objects/ObjectsState.kt b/live-objects/src/main/kotlin/io/ably/lib/objects/ObjectsState.kt index f56782613..cdd742ec0 100644 --- a/live-objects/src/main/kotlin/io/ably/lib/objects/ObjectsState.kt +++ b/live-objects/src/main/kotlin/io/ably/lib/objects/ObjectsState.kt @@ -27,14 +27,14 @@ private val objectsStateToEventMap = mapOf( ) /** - * An interface for managing and communicating changes in the synchronization state of live objects. + * An interface for managing and communicating changes in the synchronization state of objects. * * Implementations should ensure thread-safe event emission and proper synchronization * between state change notifications. */ internal interface HandlesObjectsStateChange { /** - * Handles changes in the state of live objects by notifying all registered listeners. + * Handles changes in the state of objects by notifying all registered listeners. * Implementations should ensure thread-safe event emission to both internal and public listeners. * Makes sure every event is processed in the order they were received. * @param newState The new state of the objects, SYNCING or SYNCED. @@ -99,7 +99,8 @@ private class ObjectsStateEmitter : EventEmitter + objectLifecycleEmitter.emit(objectLifecycleEvent) + } + } + + override fun disposeObjectLifecycleListeners() = offAll() +} + +private class ObjectLifecycleEmitter : EventEmitter() { + private val tag = "ObjectLifecycleEmitter" + override fun apply(listener: ObjectLifecycleChange.Listener?, event: ObjectLifecycleEvent?, vararg args: Any?) { + try { + event?.let { listener?.onLifecycleEvent(it) } + ?: Log.w(tag, "Null event passed to ObjectLifecycleChange listener callback") + } catch (t: Throwable) { + Log.e(tag, "Error occurred while executing listener callback for event: $event", t) + } + } +} diff --git a/live-objects/src/main/kotlin/io/ably/lib/objects/type/livecounter/LiveCounterChangeCoordinator.kt b/live-objects/src/main/kotlin/io/ably/lib/objects/type/livecounter/LiveCounterChangeCoordinator.kt index 0ea58f389..a1940dc04 100644 --- a/live-objects/src/main/kotlin/io/ably/lib/objects/type/livecounter/LiveCounterChangeCoordinator.kt +++ b/live-objects/src/main/kotlin/io/ably/lib/objects/type/livecounter/LiveCounterChangeCoordinator.kt @@ -43,7 +43,7 @@ private class LiveCounterChangeEmitter : EventEmitter() + // Remove the "referencedCounter" from the root map val refCounter = rootMap.get("referencedCounter")?.asLiveCounter assertNotNull(refCounter) @@ -178,6 +182,10 @@ class DefaultRealtimeObjectsTest : IntegrationTest() { refCounter.subscribe { event -> counterUpdates.add(event.update.amount) } + // Subscribe to lifecycle events for this counter + refCounter.on(ObjectLifecycleEvent.DELETED) { event -> + lifecycleEvents.add(event) + } // Simulate the deletion of the referencedCounter object channel.objects.simulateObjectDelete(refCounter as DefaultLiveCounter) @@ -195,6 +203,10 @@ class DefaultRealtimeObjectsTest : IntegrationTest() { referencedMap.subscribe { event -> mapUpdates.add(event.update) } + // Subscribe to lifecycle events for this map + referencedMap.on(ObjectLifecycleEvent.DELETED) { event -> + lifecycleEvents.add(event) + } // Simulate the deletion of the referencedMap object channel.objects.simulateObjectDelete(referencedMap as DefaultLiveMap) @@ -216,6 +228,10 @@ class DefaultRealtimeObjectsTest : IntegrationTest() { valuesMap.subscribe { event -> valuesMapUpdates.add(event.update) } + // Subscribe to lifecycle events for this map + valuesMap.on(ObjectLifecycleEvent.DELETED) { event -> + lifecycleEvents.add(event) + } // Simulate the deletion of the valuesMap object channel.objects.simulateObjectDelete(valuesMap as DefaultLiveMap) @@ -230,5 +246,11 @@ class DefaultRealtimeObjectsTest : IntegrationTest() { updatedValuesMap.values.forEach { change -> assertEquals(LiveMapUpdate.Change.REMOVED, change) } + + // Assert lifecycle events + assertEquals(3, lifecycleEvents.size) // Should have received 3 DELETED lifecycle events + lifecycleEvents.forEach { event -> + assertEquals(ObjectLifecycleEvent.DELETED, event) // All events should be DELETED + } } } diff --git a/live-objects/src/test/kotlin/io/ably/lib/objects/integration/helpers/fixtures/MapFixtures.kt b/live-objects/src/test/kotlin/io/ably/lib/objects/integration/helpers/fixtures/MapFixtures.kt index 8499eefc2..b7979310c 100644 --- a/live-objects/src/test/kotlin/io/ably/lib/objects/integration/helpers/fixtures/MapFixtures.kt +++ b/live-objects/src/test/kotlin/io/ably/lib/objects/integration/helpers/fixtures/MapFixtures.kt @@ -8,7 +8,7 @@ import io.ably.lib.objects.integration.helpers.RestObjects * Initializes a comprehensive test fixture object tree on the specified channel. * * This method creates a predetermined object hierarchy rooted at a "root" map object, - * establishing references between different types of live objects to enable comprehensive testing. + * establishing references between different types of objects to enable comprehensive testing. * * **Object Tree Structure:** * ``` @@ -71,7 +71,7 @@ internal fun RestObjects.initializeRootMap(channelName: String) { /** * Creates a comprehensive test fixture object tree on the specified channel using * - * This method establishes a hierarchical structure of live objects for testing map operations, + * This method establishes a hierarchical structure of objects for testing map operations, * creating various types of objects and establishing references between them. * * **Object Tree Structure:**