Skip to content

Commit bfb6de1

Browse files
sacOO7claude
andcommitted
feat(liveobjects): add path-based RealtimeObject and channel.object accessor
Introduce the public, strongly-typed, path-based LiveObjects entry point on a realtime channel, accessed via `channel.object`. - RealtimeObject: exposes `get()` returning the root LiveMapPathObject, and extends ObjectStateChange to subscribe to objects sync-state events (on/off/offAll). - ObjectStateChange / ObjectStateEvent: the SYNCING/SYNCED sync-state subscription API surface. - ChannelBase.object: a public field providing `channel.object` access. When the LiveObjects plugin is not installed, the field is assigned RealtimeObject.Unavailable - a null-object guard whose methods fail fast with a clear plugin-missing error (statusCode 400, code 40019) instead of an NPE, keeping the `channel.object.<method>()` syntax consistent in both cases. The plugin-present branch is intentionally left as a TODO until the LiveObjects plugin exposes the new io.ably.lib.object.RealtimeObject type (getInstance currently returns the legacy io.ably.lib.objects.RealtimeObjects). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4f35df8 commit bfb6de1

4 files changed

Lines changed: 176 additions & 1 deletion

File tree

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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.objects.ObjectsSubscription;
7+
import io.ably.lib.types.AblyException;
8+
import io.ably.lib.types.ErrorInfo;
9+
import org.jetbrains.annotations.Blocking;
10+
import org.jetbrains.annotations.NotNull;
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, and waits for the objects
37+
* synchronization state to transition to {@code SYNCED} before returning.
38+
*
39+
* <p>Spec: RTO23, RTO23f (typed SDKs return a {@link LiveMapPathObject})
40+
*
41+
* @return the root {@link LiveMapPathObject} for this channel's objects graph.
42+
*/
43+
@Blocking
44+
@NotNull
45+
LiveMapPathObject get();
46+
47+
/**
48+
* Null-Object guard for {@link RealtimeObject}, used as the value of {@code channel.object}
49+
* when the LiveObjects plugin is not installed.
50+
*
51+
* <p>Because {@code channel.object} is a field, dereferencing it can never throw; instead
52+
* every method here fails fast with the plugin-missing error, so {@code get()}, {@code on()},
53+
* {@code off()} and {@code offAll()} surface a clear, consistent error rather than a
54+
* {@link NullPointerException}.
55+
*
56+
* <p>A stateless singleton ({@link #INSTANCE}) shared across all channels that lack the
57+
* plugin. Adding a method to {@link RealtimeObject} will fail compilation here until it is
58+
* guarded, which is the intended safety net.
59+
*/
60+
final class Unavailable implements RealtimeObject {
61+
62+
public static final Unavailable INSTANCE = new Unavailable();
63+
64+
private Unavailable() {}
65+
66+
@Override
67+
public @NotNull LiveMapPathObject get() {
68+
throw missing();
69+
}
70+
71+
@Override
72+
public ObjectsSubscription on(@NotNull ObjectStateEvent event, ObjectStateChange.@NotNull Listener listener) {
73+
throw missing();
74+
}
75+
76+
@Override
77+
public void off(ObjectStateChange.@NotNull Listener listener) {
78+
throw missing();
79+
}
80+
81+
@Override
82+
public void offAll() {
83+
throw missing();
84+
}
85+
86+
private static RuntimeException missing() {
87+
return new IllegalStateException("LiveObjects plugin hasn't been installed", AblyException.fromErrorInfo(
88+
new ErrorInfo("add runtimeOnly('io.ably:liveobjects:<ably-version>') to your dependency tree", 400, 40019)
89+
));
90+
}
91+
}
92+
}
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.objects.ObjectsSubscription;
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+
ObjectsSubscription 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)