Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0410432
Implemented path-based liveobjects public API for PathObject and Inst…
sacOO7 Jun 8, 2026
e226ba4
Updated PathObject and Instance classes/sub-classes as per finalized …
sacOO7 Jun 9, 2026
99d9dd9
Refactored/Updated public API types as per spec
sacOO7 Jun 10, 2026
59a5ecc
Moved subscribe methods to the bottom in `PathObject` interface
sacOO7 Jun 10, 2026
11e87a7
Addressed PR review comments on liveobjects public API
sacOO7 Jun 11, 2026
4f35df8
Merge pull request #1213 from ably/feature/path-based-liveobjects-fin…
sacOO7 Jun 11, 2026
bfb6de1
feat(liveobjects): add path-based RealtimeObject and channel.object a…
sacOO7 Jun 15, 2026
f053c49
- Marked `channel.object.get` method non-blocking using completablefu…
sacOO7 Jun 16, 2026
18485d6
Merge pull request #1216 from ably/chore/liveobjects-basic-implementa…
sacOO7 Jun 16, 2026
6227b75
Added basic impl. for PathObject and Instance liveobjects interfaces
sacOO7 Jun 16, 2026
68edd4b
Merge remote-tracking branch 'origin/feature/path-based-liveobjects-i…
sacOO7 Jun 17, 2026
548c0b5
Added impl. for DefaultObjectMessage and WireObjectMessage along with…
sacOO7 Jun 17, 2026
af0b39e
Added default skeleton implementation for LiveCounter and LiveMap
sacOO7 Jun 17, 2026
f74ae1d
Updated instance types to return specific JsonPrimitive/JsonObject/Js…
sacOO7 Jun 17, 2026
54ae53f
- Implemented ResolvedValue class for resolving value at given path
sacOO7 Jun 17, 2026
0a9ea02
Implemented resolveValueAtPath guards for terminal operations similar…
sacOO7 Jun 17, 2026
3c25c13
Added liveobjects read/write operation validation
sacOO7 Jun 17, 2026
94b96a4
Refactored javadoc for Instance interface, fixed other spec doc comments
sacOO7 Jun 18, 2026
c8a283d
Updated PathObject#value checks for primitives as per spec
sacOO7 Jun 18, 2026
3e8f70c
- Updated Adapter#getChannel with readonly way to retrieve channel
sacOO7 Jun 22, 2026
3471b02
Merge pull request #1217 from ably/chore/liveobjects-add-basic-implem…
sacOO7 Jun 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions lib/src/main/java/io/ably/lib/object/RealtimeObject.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package io.ably.lib.object;

import io.ably.lib.object.path.types.LiveMapPathObject;
import io.ably.lib.object.state.ObjectStateChange;
import io.ably.lib.object.state.ObjectStateEvent;
import io.ably.lib.types.AblyException;
import io.ably.lib.types.ErrorInfo;
import org.jetbrains.annotations.NotNull;

import java.util.concurrent.CompletableFuture;

/**
* The RealtimeObject interface is the entry point to the strongly-typed, path-based
* LiveObjects API on a channel. It exposes the root of the objects graph as a
* {@link LiveMapPathObject} and, via {@link ObjectStateChange}, lets callers observe
* synchronization state transitions for the channel's objects.
*
* <p>Implementations of this interface must be thread-safe as they may be accessed
* from multiple threads concurrently.
*
* <p>Spec: RTO23
*/
public interface RealtimeObject extends ObjectStateChange {

/**
* Retrieves a {@link LiveMapPathObject} rooted at the channel's root {@code LiveMap}.
* The returned object has an empty path and resolves to the root {@code LiveMap}; use
* its navigation methods to address nested values within the objects graph.
*
* <p>When called without a type variable, we return a default root type which is based
* on the globally defined interface for the Objects feature. A user can provide an
* explicit type to set the type structure on this particular channel. This is useful
* when working with multiple channels with different underlying data structures.
*
* <p>This operation requires the {@code OBJECT_SUBSCRIBE} channel mode. It implicitly
* attaches the channel if it is not already attached; the returned future completes once
* the objects synchronization state has transitioned to {@code SYNCED}, and completes
* exceptionally with an {@code AblyException} if synchronization fails.
*
* <p>Spec: RTO23, RTO23f (typed SDKs return a {@link LiveMapPathObject})
*
* @return a future that completes with the root {@link LiveMapPathObject} for this
* channel's objects graph.
*/
@NotNull
CompletableFuture<LiveMapPathObject> get();

/**
* Null-Object guard for {@link RealtimeObject}, used as the value of {@code channel.object}
* when the LiveObjects plugin is not installed.
*
* <p>Because {@code channel.object} is a field, dereferencing it can never throw; instead
* every method here fails fast with the plugin-missing error, so {@code get()}, {@code on()},
* {@code off()} and {@code offAll()} surface a clear, consistent error rather than a
* {@link NullPointerException}.
*
* <p>A stateless singleton ({@link #INSTANCE}) shared across all channels that lack the
* plugin. Adding a method to {@link RealtimeObject} will fail compilation here until it is
* guarded, which is the intended safety net.
*/
final class Unavailable implements RealtimeObject {

public static final Unavailable INSTANCE = new Unavailable();

private Unavailable() {}

@Override
public @NotNull CompletableFuture<LiveMapPathObject> get() {
throw missing();
}

@Override
public Subscription on(@NotNull ObjectStateEvent event, ObjectStateChange.@NotNull Listener listener) {
throw missing();
}

@Override
public void off(ObjectStateChange.@NotNull Listener listener) {
throw missing();
}

@Override
public void offAll() {
throw missing();
}

private static RuntimeException missing() {
return new IllegalStateException("LiveObjects plugin hasn't been installed", AblyException.fromErrorInfo(
new ErrorInfo("add runtimeOnly('io.ably:liveobjects:<ably-version>') to your dependency tree", 400, 40019)
));
}
}
}
30 changes: 30 additions & 0 deletions lib/src/main/java/io/ably/lib/object/Subscription.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.ably.lib.object;

/**
* Represents a registration for receiving events from a subscribe operation.
* Provides a way to clean up and remove a subscription when it is no longer
* needed.
*
* <p>Example usage:
* <pre>
* {@code
* Subscription s = pathObject.subscribe(event -> { ... });
* // Later, when done with the subscription
* s.unsubscribe();
* }
* </pre>
*
* <p>Spec: SUB1
*/
public interface Subscription {

/**
* Deregisters the listener that was registered by the corresponding
* {@code subscribe} call. Once called, the listener will not be invoked for
* any subsequent events and references to it are cleaned up. Calling this
* method more than once is a no-op.
*
* <p>Spec: SUB2a, SUB2b
*/
void unsubscribe();
}
28 changes: 28 additions & 0 deletions lib/src/main/java/io/ably/lib/object/ValueType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.ably.lib.object;

/**
* The type of a value resolved by a {@code PathObject} or wrapped by an
* {@code Instance} in the LiveObjects graph.
*
* <p>Spec: RTTS2
*/
public enum ValueType {
/** Corresponds to the {@code String} primitive. Spec: RTTS2a1 */
STRING,
/** Corresponds to the {@code Number} primitive. Spec: RTTS2a2 */
NUMBER,
/** Corresponds to the {@code Boolean} primitive. Spec: RTTS2a3 */
BOOLEAN,
/** Corresponds to the {@code Binary} primitive. Spec: RTTS2a4 */
BINARY,
/** Corresponds to the {@code JsonObject} primitive. Spec: RTTS2a5 */
JSON_OBJECT,
/** Corresponds to the {@code JsonArray} primitive. Spec: RTTS2a6 */
JSON_ARRAY,
/** Corresponds to a {@code LiveMap} object. Spec: RTTS2a7 */
LIVE_MAP,
/** Corresponds to a {@code LiveCounter} object. Spec: RTTS2a8 */
LIVE_COUNTER,
/** Returned by {@code PathObject#getType()} only when a value is present but matches none of the known types. Never produced by an {@code Instance} in normal operation. Spec: RTTS2a9 */
UNKNOWN,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.ably.lib.object.adapter;

import io.ably.lib.realtime.ChannelBase;
import io.ably.lib.realtime.Connection;
import io.ably.lib.types.AblyException;
import io.ably.lib.types.ClientOptions;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;

/**
* Bridges the path-based LiveObjects implementation to the core Ably client, exposing the
* client configuration, connection and channel state it needs without coupling it to the
* concrete {@link io.ably.lib.realtime.AblyRealtime} type.
*
* <p>This is the adapter for the path-based {@code io.ably.lib.object} API and is intentionally
* kept independent of the legacy {@code io.ably.lib.objects} package.
*/
public interface AblyClientAdapter {
/**
* Retrieves the client options configured for the Ably client.
* Used to access client configuration parameters such as echoMessages setting
* that affect the behavior of Objects operations.
*
* @return the client options containing configuration parameters
*/
@NotNull ClientOptions getClientOptions();

/**
* Retrieves the connection instance for handling connection state and operations.
* Used to check connection status, obtain error information, and manage
* message transmission across the Ably connection.
*
* @return the connection instance
*/
@NotNull Connection getConnection();

/**
* Retrieves the current time in milliseconds from the Ably server.
* Spec: RTO16
*/
@Blocking
long getTime() throws AblyException;

/**
* Retrieves the channel instance for the specified channel name.
* If the channel does not exist, an AblyException is thrown.
*
* @param channelName the name of the channel to retrieve
* @return the ChannelBase instance for the specified channel
* @throws AblyException if the channel is not found or cannot be retrieved
*/
@NotNull ChannelBase getChannel(@NotNull String channelName) throws AblyException;
}
56 changes: 56 additions & 0 deletions lib/src/main/java/io/ably/lib/object/adapter/Adapter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.ably.lib.object.adapter;

import io.ably.lib.realtime.AblyRealtime;
import io.ably.lib.realtime.Channel;
import io.ably.lib.realtime.ChannelBase;
import io.ably.lib.realtime.Connection;
import io.ably.lib.types.AblyException;
import io.ably.lib.types.ClientOptions;
import io.ably.lib.types.ErrorInfo;
import io.ably.lib.types.ReadOnlyMap;
import io.ably.lib.util.Log;
import org.jetbrains.annotations.NotNull;

/**
* Default {@link AblyClientAdapter} implementation backed by an {@link AblyRealtime} client.
* Holding the {@code AblyRealtime} reference gives the path-based LiveObjects implementation
* access to the full client configuration and runtime state it may need.
*/
public class Adapter implements AblyClientAdapter {
private final AblyRealtime ably;
private static final String TAG = AblyClientAdapter.class.getName();

public Adapter(@NotNull AblyRealtime ably) {
this.ably = ably;
}

@Override
public @NotNull ClientOptions getClientOptions() {
return ably.options;
}

@Override
public @NotNull Connection getConnection() {
return ably.connection;
}

@Override
public long getTime() throws AblyException {
return ably.time();
}

@Override
public @NotNull ChannelBase getChannel(@NotNull String channelName) throws AblyException {
// Look up via the read-only map view. Channels#get(String) would create the channel if
// absent; ReadOnlyMap only exposes get(Object), which returns null atomically for an
// unknown channel instead of silently recreating it.
final ReadOnlyMap<String, Channel> channels = ably.channels;
final ChannelBase channel = channels.get(channelName);
if (channel == null) {
Log.e(TAG, "getChannel(): channel not found: " + channelName);
ErrorInfo errorInfo = new ErrorInfo("Channel not found: " + channelName, 404);
throw AblyException.fromErrorInfo(errorInfo);
}
return channel;
}
}
10 changes: 10 additions & 0 deletions lib/src/main/java/io/ably/lib/object/adapter/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Adapter layer bridging the path-based LiveObjects implementation to the core Ably client.
* {@link io.ably.lib.object.adapter.AblyClientAdapter} is the abstraction the implementation
* depends on; {@link io.ably.lib.object.adapter.Adapter} is the default implementation backed
* by an {@link io.ably.lib.realtime.AblyRealtime} client.
*
* <p>This package is intentionally independent of the legacy {@code io.ably.lib.objects}
* package so the path-based API can evolve on its own.
*/
package io.ably.lib.object.adapter;
Loading
Loading