Skip to content

Commit e6678b0

Browse files
committed
[ECO-5479] Defined and implemented ObjectLifecycleChange interface
- Updated LiveMap and LiveCounter to extend ObjectLifecycleChange interface - Implemented ObjectLifecycleCoordinator by extending ObjectLifecycleChange - Updated testObjectDelete with relevant assertions for ObjectLifecycleEvent.DELETED
1 parent 0775acd commit e6678b0

11 files changed

Lines changed: 209 additions & 15 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package io.ably.lib.objects.type;
2+
3+
import io.ably.lib.objects.ObjectsSubscription;
4+
import org.jetbrains.annotations.NonBlocking;
5+
import org.jetbrains.annotations.NotNull;
6+
7+
/**
8+
* Interface for managing subscriptions to Object lifecycle events.
9+
* <p>
10+
* This interface provides methods to subscribe to and manage notifications about significant lifecycle
11+
* changes that occur to Object, such as deletion. More events can be added in the future.
12+
* Multiple listeners can be registered independently, and each can be managed separately.
13+
* <p>
14+
* Lifecycle events are different from data update events - they represent changes
15+
* to the object's existence state rather than changes to the object's data content.
16+
*
17+
* @see ObjectLifecycleEvent for the available lifecycle events
18+
*/
19+
public interface ObjectLifecycleChange {
20+
/**
21+
* Subscribes to a specific Object lifecycle event.
22+
*
23+
* <p>This method registers the provided listener to be notified when the specified
24+
* lifecycle event occurs. The returned subscription can be used to
25+
* unsubscribe later when the notifications are no longer needed.
26+
*
27+
* @param event the lifecycle event to subscribe to
28+
* @param listener the listener that will be called when the event occurs
29+
* @return a subscription object that can be used to unsubscribe from the event
30+
*/
31+
@NonBlocking
32+
ObjectsSubscription on(@NotNull ObjectLifecycleEvent event, @NotNull ObjectLifecycleChange.Listener listener);
33+
34+
/**
35+
* Unsubscribes the specified listener from all lifecycle events.
36+
*
37+
* <p>After calling this method, the provided listener will no longer receive
38+
* any lifecycle event notifications.
39+
*
40+
* @param listener the listener to unregister from all events
41+
*/
42+
@NonBlocking
43+
void off(@NotNull ObjectLifecycleChange.Listener listener);
44+
45+
/**
46+
* Unsubscribes all listeners from all lifecycle events.
47+
*
48+
* <p>After calling this method, no listeners will receive any lifecycle
49+
* event notifications until new listeners are registered.
50+
*/
51+
@NonBlocking
52+
void offAll();
53+
54+
/**
55+
* Interface for receiving notifications about Object lifecycle changes.
56+
* <p>
57+
* Implement this interface and register it with an ObjectLifecycleChange provider
58+
* to be notified when lifecycle events occur, such as object creation or deletion.
59+
*/
60+
@FunctionalInterface
61+
interface Listener {
62+
/**
63+
* Called when a lifecycle event occurs.
64+
*
65+
* @param lifecycleEvent The lifecycle event that occurred
66+
*/
67+
void onLifecycleEvent(@NotNull ObjectLifecycleEvent lifecycleEvent);
68+
}
69+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.ably.lib.objects.type;
2+
3+
/**
4+
* Represents lifecycle events a Object in the Ably system.
5+
* <p>
6+
* This enum notifies listeners about significant lifecycle changes
7+
* that occur to an Object during its lifetime. Clients can register a
8+
* {@link ObjectLifecycleChange.Listener} to receive these events.
9+
*/
10+
public enum ObjectLifecycleEvent {
11+
/**
12+
* Indicates that an Object has been deleted (tombstoned).
13+
* Emitted once when the object is tombstoned server-side (i.e., deleted and no longer addressable).
14+
* Not re-emitted during client-side garbage collection of tombstones.
15+
*/
16+
DELETED
17+
}

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

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

33
import io.ably.lib.objects.ObjectsCallback;
4+
import io.ably.lib.objects.type.ObjectLifecycleChange;
45
import org.jetbrains.annotations.Blocking;
56
import org.jetbrains.annotations.NonBlocking;
67
import org.jetbrains.annotations.NotNull;
@@ -11,7 +12,7 @@
1112
* It allows incrementing, decrementing, and retrieving the current value of the counter,
1213
* both synchronously and asynchronously.
1314
*/
14-
public interface LiveCounter extends LiveCounterChange {
15+
public interface LiveCounter extends LiveCounterChange, ObjectLifecycleChange {
1516

1617
/**
1718
* Increments the value of the counter by the specified amount.

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

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

33
import io.ably.lib.objects.ObjectsCallback;
4+
import io.ably.lib.objects.type.ObjectLifecycleChange;
45
import org.jetbrains.annotations.Blocking;
56
import org.jetbrains.annotations.NonBlocking;
67
import org.jetbrains.annotations.Contract;
@@ -14,7 +15,7 @@
1415
* The LiveMap interface provides methods to interact with a live, real-time map structure.
1516
* It supports both synchronous and asynchronous operations for managing key-value pairs.
1617
*/
17-
public interface LiveMap extends LiveMapChange {
18+
public interface LiveMap extends LiveMapChange, ObjectLifecycleChange {
1819

1920
/**
2021
* Retrieves the value associated with the specified key.

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ private class ObjectsStateEmitter : EventEmitter<ObjectsStateEvent, ObjectsState
9999
private val tag = "ObjectsStateEmitter"
100100
override fun apply(listener: ObjectsStateChange.Listener?, event: ObjectsStateEvent?, vararg args: Any?) {
101101
try {
102-
listener?.onStateChanged(event!!)
102+
event?.let { listener?.onStateChanged(it) }
103+
?: Log.w(tag, "Null event passed to ObjectsStateChange Listener callback")
103104
} catch (t: Throwable) {
104105
Log.e(tag, "Error occurred while executing listener callback for event: $event", t)
105106
}

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import io.ably.lib.types.AblyException
44
import io.ably.lib.types.ErrorInfo
55
import io.ably.lib.util.Log
66
import kotlinx.coroutines.*
7-
import org.jetbrains.annotations.NotNull
87
import java.nio.charset.StandardCharsets
98
import java.util.concurrent.CancellationException
109

@@ -66,7 +65,6 @@ internal class ObjectsAsyncScope(channelName: String) {
6665
private val scope =
6766
CoroutineScope(Dispatchers.Default + CoroutineName(tag) + SupervisorJob())
6867

69-
@NotNull
7068
internal fun <T> launchWithCallback(callback: ObjectsCallback<T>, block: suspend () -> T) {
7169
scope.launch {
7270
try {

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ internal val ObjectUpdate.noOp get() = this.update == null
2727
internal abstract class BaseRealtimeObject(
2828
internal val objectId: String, // // RTLO3a
2929
internal val objectType: ObjectType,
30-
) {
30+
) : ObjectLifecycleCoordinator() {
3131

3232
protected open val tag = "BaseRealtimeObject"
3333

@@ -92,7 +92,7 @@ internal abstract class BaseRealtimeObject(
9292

9393
if (isTombstoned) {
9494
// this object is tombstoned so the operation cannot be applied
95-
return;
95+
return
9696
}
9797
applyObjectOperation(objectOperation, objectMessage) // RTLC7d
9898
}
@@ -115,7 +115,7 @@ internal abstract class BaseRealtimeObject(
115115

116116
internal fun validateObjectId(objectId: String?) {
117117
if (this.objectId != objectId) {
118-
throw objectError("Invalid object: incoming objectId=${objectId}; $objectType objectId=$objectId")
118+
throw objectError("Invalid object: incoming objectId=$objectId; $objectType objectId=${this.objectId}")
119119
}
120120
}
121121

@@ -129,7 +129,8 @@ internal abstract class BaseRealtimeObject(
129129
isTombstoned = true
130130
tombstonedAt = serialTimestamp?: System.currentTimeMillis()
131131
val update = clearData()
132-
// TODO: Emit BaseRealtimeObject lifecycle events
132+
// Emit object lifecycle event for deletion
133+
objectLifecycleChanged(ObjectLifecycle.Deleted)
133134
return update
134135
}
135136

@@ -142,13 +143,13 @@ internal abstract class BaseRealtimeObject(
142143
}
143144

144145
/**
145-
* Validates that the provided object state is compatible with this live object.
146+
* Validates that the provided object state is compatible with this object.
146147
* Checks object ID, type-specific validations, and any included create operations.
147148
*/
148149
abstract fun validate(state: ObjectState)
149150

150151
/**
151-
* Applies an object state received during synchronization to this live object.
152+
* Applies an object state received during synchronization to this object.
152153
* This method should update the internal data structure with the complete state
153154
* received from the server.
154155
*
@@ -159,7 +160,7 @@ internal abstract class BaseRealtimeObject(
159160
abstract fun applyObjectState(objectState: ObjectState, message: ObjectMessage): ObjectUpdate
160161

161162
/**
162-
* Applies an operation to this live object.
163+
* Applies an operation to this object.
163164
* This method handles the specific operation actions (e.g., update, remove)
164165
* by modifying the underlying data structure accordingly.
165166
*
@@ -185,7 +186,7 @@ internal abstract class BaseRealtimeObject(
185186
abstract fun clearData(): ObjectUpdate
186187

187188
/**
188-
* Notifies subscribers about changes made to this live object. Propagates updates through the
189+
* Notifies subscribers about changes made to this object. Propagates updates through the
189190
* appropriate manager after converting the generic update map to type-specific update objects.
190191
* Spec: RTLO4b4c
191192
*/
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package io.ably.lib.objects.type
2+
3+
import io.ably.lib.objects.ObjectsSubscription
4+
import io.ably.lib.util.EventEmitter
5+
import io.ably.lib.util.Log
6+
7+
/**
8+
* Internal enum representing object lifecycle states
9+
*/
10+
internal enum class ObjectLifecycle {
11+
Created,
12+
Active,
13+
Deleted
14+
}
15+
16+
/**
17+
* Maps internal ObjectLifecycle values to their corresponding public ObjectLifecycleEvent values.
18+
* Used to determine which events should be emitted when lifecycle changes occur.
19+
* CREATED and ACTIVE map to null (no public event), while DELETED maps to the public DELETED event.
20+
*/
21+
private val objectLifecycleToEventMap = mapOf(
22+
ObjectLifecycle.Created to null,
23+
ObjectLifecycle.Active to null,
24+
ObjectLifecycle.Deleted to ObjectLifecycleEvent.DELETED
25+
)
26+
27+
/**
28+
* An interface for managing and communicating changes in the lifecycle state of objects.
29+
*
30+
* Implementations should ensure thread-safe event emission and proper lifecycle
31+
* event notifications.
32+
*/
33+
internal interface HandlesObjectLifecycleChange {
34+
/**
35+
* Handles changes in the lifecycle of objects by notifying all registered listeners.
36+
* Implementations should ensure thread-safe event emission to both internal and public listeners.
37+
* Makes sure every event is processed in the order they were received.
38+
* @param newLifecycle The new lifecycle state of the object.
39+
*/
40+
fun objectLifecycleChanged(newLifecycle: ObjectLifecycle)
41+
42+
/**
43+
* Disposes all registered lifecycle change listeners and cancels any pending operations.
44+
* Should be called when the associated object is no longer needed.
45+
*/
46+
fun disposeObjectLifecycleListeners()
47+
}
48+
49+
internal abstract class ObjectLifecycleCoordinator : ObjectLifecycleChange, HandlesObjectLifecycleChange {
50+
private val tag = "ObjectLifecycleCoordinator"
51+
// EventEmitter for users of the library
52+
private val objectLifecycleEmitter = ObjectLifecycleEmitter()
53+
54+
override fun on(event: ObjectLifecycleEvent, listener: ObjectLifecycleChange.Listener): ObjectsSubscription {
55+
objectLifecycleEmitter.on(event, listener)
56+
return ObjectsSubscription {
57+
objectLifecycleEmitter.off(event, listener)
58+
}
59+
}
60+
61+
override fun off(listener: ObjectLifecycleChange.Listener) = objectLifecycleEmitter.off(listener)
62+
63+
override fun offAll() = objectLifecycleEmitter.off()
64+
65+
override fun objectLifecycleChanged(newLifecycle: ObjectLifecycle) {
66+
objectLifecycleToEventMap[newLifecycle]?.let { objectLifecycleEvent ->
67+
objectLifecycleEmitter.emit(objectLifecycleEvent)
68+
}
69+
}
70+
71+
override fun disposeObjectLifecycleListeners() = offAll()
72+
}
73+
74+
private class ObjectLifecycleEmitter : EventEmitter<ObjectLifecycleEvent, ObjectLifecycleChange.Listener>() {
75+
private val tag = "ObjectLifecycleEmitter"
76+
override fun apply(listener: ObjectLifecycleChange.Listener?, event: ObjectLifecycleEvent?, vararg args: Any?) {
77+
try {
78+
event?.let { listener?.onLifecycleEvent(it) }
79+
?: Log.w(tag, "Null event passed to ObjectLifecycleChange listener callback")
80+
} catch (t: Throwable) {
81+
Log.e(tag, "Error occurred while executing listener callback for event: $event", t)
82+
}
83+
}
84+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ private class LiveCounterChangeEmitter : EventEmitter<LiveCounterUpdate, LiveCou
4343
override fun apply(listener: LiveCounterChange.Listener?, event: LiveCounterUpdate?, vararg args: Any?) {
4444
try {
4545
event?.let { listener?.onUpdated(it) }
46-
?: Log.w(tag, "Null event passed to listener callback")
46+
?: Log.w(tag, "Null event passed to LiveCounterChange listener callback")
4747
} catch (t: Throwable) {
4848
Log.e(tag, "Error occurred while executing listener callback for event: $event", t)
4949
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ private class LiveMapChangeEmitter : EventEmitter<LiveMapUpdate, LiveMapChange.L
4343
override fun apply(listener: LiveMapChange.Listener?, event: LiveMapUpdate?, vararg args: Any?) {
4444
try {
4545
event?.let { listener?.onUpdated(it) }
46-
?: Log.w(tag, "Null event passed to listener callback")
46+
?: Log.w(tag, "Null event passed to LiveMapChange listener callback")
4747
} catch (t: Throwable) {
4848
Log.e(tag, "Error occurred while executing listener callback for event: $event", t)
4949
}

0 commit comments

Comments
 (0)