Skip to content

Commit 18485d6

Browse files
authored
Merge pull request #1216 from ably/chore/liveobjects-basic-implementation-for-path-based-interfaces
[AIT-928] feat(liveobjects): add path-based RealtimeObject and channel.object accessor
2 parents 4f35df8 + f053c49 commit 18485d6

4 files changed

Lines changed: 177 additions & 1 deletion

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package io.ably.lib.object;
2+
3+
import io.ably.lib.object.path.types.LiveMapPathObject;
4+
import io.ably.lib.object.state.ObjectStateChange;
5+
import io.ably.lib.object.state.ObjectStateEvent;
6+
import io.ably.lib.types.AblyException;
7+
import io.ably.lib.types.ErrorInfo;
8+
import org.jetbrains.annotations.NotNull;
9+
10+
import java.util.concurrent.CompletableFuture;
11+
12+
/**
13+
* The RealtimeObject interface is the entry point to the strongly-typed, path-based
14+
* LiveObjects API on a channel. It exposes the root of the objects graph as a
15+
* {@link LiveMapPathObject} and, via {@link ObjectStateChange}, lets callers observe
16+
* synchronization state transitions for the channel's objects.
17+
*
18+
* <p>Implementations of this interface must be thread-safe as they may be accessed
19+
* from multiple threads concurrently.
20+
*
21+
* <p>Spec: RTO23
22+
*/
23+
public interface RealtimeObject extends ObjectStateChange {
24+
25+
/**
26+
* Retrieves a {@link LiveMapPathObject} rooted at the channel's root {@code LiveMap}.
27+
* The returned object has an empty path and resolves to the root {@code LiveMap}; use
28+
* its navigation methods to address nested values within the objects graph.
29+
*
30+
* <p>When called without a type variable, we return a default root type which is based
31+
* on the globally defined interface for the Objects feature. A user can provide an
32+
* explicit type to set the type structure on this particular channel. This is useful
33+
* when working with multiple channels with different underlying data structures.
34+
*
35+
* <p>This operation requires the {@code OBJECT_SUBSCRIBE} channel mode. It implicitly
36+
* attaches the channel if it is not already attached; the returned future completes once
37+
* the objects synchronization state has transitioned to {@code SYNCED}, and completes
38+
* exceptionally with an {@code AblyException} if synchronization fails.
39+
*
40+
* <p>Spec: RTO23, RTO23f (typed SDKs return a {@link LiveMapPathObject})
41+
*
42+
* @return a future that completes with the root {@link LiveMapPathObject} for this
43+
* channel's objects graph.
44+
*/
45+
@NotNull
46+
CompletableFuture<LiveMapPathObject> get();
47+
48+
/**
49+
* Null-Object guard for {@link RealtimeObject}, used as the value of {@code channel.object}
50+
* when the LiveObjects plugin is not installed.
51+
*
52+
* <p>Because {@code channel.object} is a field, dereferencing it can never throw; instead
53+
* every method here fails fast with the plugin-missing error, so {@code get()}, {@code on()},
54+
* {@code off()} and {@code offAll()} surface a clear, consistent error rather than a
55+
* {@link NullPointerException}.
56+
*
57+
* <p>A stateless singleton ({@link #INSTANCE}) shared across all channels that lack the
58+
* plugin. Adding a method to {@link RealtimeObject} will fail compilation here until it is
59+
* guarded, which is the intended safety net.
60+
*/
61+
final class Unavailable implements RealtimeObject {
62+
63+
public static final Unavailable INSTANCE = new Unavailable();
64+
65+
private Unavailable() {}
66+
67+
@Override
68+
public @NotNull CompletableFuture<LiveMapPathObject> get() {
69+
throw missing();
70+
}
71+
72+
@Override
73+
public Subscription on(@NotNull ObjectStateEvent event, ObjectStateChange.@NotNull Listener listener) {
74+
throw missing();
75+
}
76+
77+
@Override
78+
public void off(ObjectStateChange.@NotNull Listener listener) {
79+
throw missing();
80+
}
81+
82+
@Override
83+
public void offAll() {
84+
throw missing();
85+
}
86+
87+
private static RuntimeException missing() {
88+
return new IllegalStateException("LiveObjects plugin hasn't been installed", AblyException.fromErrorInfo(
89+
new ErrorInfo("add runtimeOnly('io.ably:liveobjects:<ably-version>') to your dependency tree", 400, 40019)
90+
));
91+
}
92+
}
93+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package io.ably.lib.object.state;
2+
3+
import io.ably.lib.object.Subscription;
4+
import org.jetbrains.annotations.NonBlocking;
5+
import org.jetbrains.annotations.NotNull;
6+
7+
public interface ObjectStateChange {
8+
/**
9+
* Subscribes to a specific Objects synchronization state event.
10+
*
11+
* <p>This method registers the provided listener to be notified when the specified
12+
* synchronization state event occurs. The returned subscription can be used to
13+
* unsubscribe later when the notifications are no longer needed.
14+
*
15+
* @param event the synchronization state event to subscribe to (SYNCING or SYNCED)
16+
* @param listener the listener that will be called when the event occurs
17+
* @return a subscription object that can be used to unsubscribe from the event
18+
*/
19+
@NonBlocking
20+
Subscription on(@NotNull ObjectStateEvent event, @NotNull ObjectStateChange.Listener listener);
21+
22+
/**
23+
* Unsubscribes the specified listener from all synchronization state events.
24+
*
25+
* <p>After calling this method, the provided listener will no longer receive
26+
* any synchronization state event notifications.
27+
*
28+
* @param listener the listener to unregister from all events
29+
*/
30+
@NonBlocking
31+
void off(@NotNull ObjectStateChange.Listener listener);
32+
33+
/**
34+
* Unsubscribes all listeners from all synchronization state events.
35+
*
36+
* <p>After calling this method, no listeners will receive any synchronization
37+
* state event notifications until new listeners are registered.
38+
*/
39+
@NonBlocking
40+
void offAll();
41+
42+
/**
43+
* Interface for receiving notifications about Objects synchronization state changes.
44+
* <p>
45+
* Implement this interface and register it with an {@code ObjectStateEmitter} to be notified
46+
* when synchronization state transitions occur.
47+
*/
48+
interface Listener {
49+
/**
50+
* Called when the synchronization state changes.
51+
*
52+
* @param objectStateEvent The new state event (SYNCING or SYNCED)
53+
*/
54+
void onStateChanged(ObjectStateEvent objectStateEvent);
55+
}
56+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.ably.lib.object.state;
2+
3+
/**
4+
* Represents the synchronization state of Ably Objects.
5+
* <p>
6+
* This enum is used to notify listeners about state changes in the synchronization process.
7+
* Clients can register an {@link ObjectStateChange.Listener} to receive these events.
8+
*/
9+
public enum ObjectStateEvent {
10+
/**
11+
* Indicates that synchronization between local and remote objects is in progress.
12+
*/
13+
SYNCING,
14+
15+
/**
16+
* Indicates that synchronization has completed successfully and objects are in sync.
17+
*/
18+
SYNCED
19+
}

lib/src/main/java/io/ably/lib/realtime/ChannelBase.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import io.ably.lib.http.Http;
1414
import io.ably.lib.http.HttpCore;
1515
import io.ably.lib.http.HttpUtils;
16+
import io.ably.lib.object.RealtimeObject;
1617
import io.ably.lib.objects.RealtimeObjects;
1718
import io.ably.lib.objects.LiveObjectsPlugin;
1819
import io.ably.lib.rest.MessageEditsMixin;
@@ -112,6 +113,8 @@ public abstract class ChannelBase extends EventEmitter<ChannelEvent, ChannelStat
112113

113114
private volatile MessageEditsMixin messageEditsMixin;
114115

116+
public RealtimeObject object;
117+
115118
public RealtimeObjects getObjects() throws AblyException {
116119
if (liveObjectsPlugin == null) {
117120
throw AblyException.fromErrorInfo(
@@ -1695,7 +1698,12 @@ else if(stateChange.current.equals(failureState)) {
16951698
this.decodingContext = new DecodingContext();
16961699
this.liveObjectsPlugin = liveObjectsPlugin;
16971700
if (liveObjectsPlugin != null) {
1698-
liveObjectsPlugin.getInstance(name); // Make objects instance ready to process sync messages
1701+
liveObjectsPlugin.getInstance(name);
1702+
// TODO(objects-migration): assign `this.object` to the real RealtimeObject once the
1703+
// LiveObjects plugin exposes io.ably.lib.object.RealtimeObject (getInstance currently
1704+
// returns the legacy io.ably.lib.objects.RealtimeObjects type).
1705+
} else {
1706+
this.object = RealtimeObject.Unavailable.INSTANCE;
16991707
}
17001708
this.annotations = new RealtimeAnnotations(
17011709
this,

0 commit comments

Comments
 (0)