Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions lib/src/main/java/io/ably/lib/objects/LiveObjectsPlugin.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.ably.lib.objects;

import io.ably.lib.objects.path.ChannelObject;
import io.ably.lib.realtime.ChannelState;
import io.ably.lib.types.ProtocolMessage;
import org.jetbrains.annotations.NotNull;
Expand All @@ -22,6 +23,17 @@ public interface LiveObjectsPlugin {
@NotNull
RealtimeObjects getInstance(@NotNull String channelName);

/**
* Retrieves the path-based LiveObjects accessor for the specified channel.
* The returned {@link ChannelObject} is the entry point for the
* path-based public API ({@code channel.object().get()}).
*
* @param channelName the name of the channel for which the ChannelObject is to be retrieved.
* @return the ChannelObject associated with the specified channel name.
*/
@NotNull
ChannelObject getChannelObject(@NotNull String channelName);

/**
* Handles a protocol message.
* This method is invoked whenever a protocol message is received, allowing the implementation
Expand Down
33 changes: 33 additions & 0 deletions lib/src/main/java/io/ably/lib/objects/path/ChannelObject.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.ably.lib.objects.path;

import io.ably.lib.objects.ObjectsCallback;
import io.ably.lib.types.AblyException;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NonBlocking;
import org.jetbrains.annotations.NotNull;

/**
* Path-based LiveObjects accessor for a channel — obtained via
* {@code channel.object()}. Provides a single entry point to the channel's
* root {@link RootPathObject}.
*/
@ApiStatus.NonExtendable
public interface ChannelObject {

/**
* Blocking: waits for the initial Objects sync to complete and returns
* the channel's root path object. Mirrors {@code RealtimeObjects#getRoot()}
* which is also blocking.
*/
@Blocking
@NotNull
RootPathObject get() throws AblyException;

/**
* Non-blocking variant. The callback is invoked once the initial sync
* completes (success) or fails.
*/
@NonBlocking
void getAsync(@NotNull ObjectsCallback<RootPathObject> callback);
}
26 changes: 26 additions & 0 deletions lib/src/main/java/io/ably/lib/objects/path/LiveCounter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.ably.lib.objects.path;

import org.jetbrains.annotations.NotNull;

/**
* Public factory for LiveCounter creation tokens used by atomic deep-create
* {@code PathObject#set(key, LiveCounter.create(...))}.
* <p>
* The runtime instance type is {@link LiveCounterInstance}.
*/
public final class LiveCounter {

private LiveCounter() { /* factory only */ }

/** Create a LiveCounter initialised to zero. */
@NotNull
public static LiveValue create() {
return create(0);
}

/** Create a LiveCounter initialised to {@code initialValue}. */
@NotNull
public static LiveValue create(@NotNull Number initialValue) {
return LiveCreate.counter(initialValue);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.ably.lib.objects.path;

import io.ably.lib.objects.type.counter.LiveCounter;
import org.jetbrains.annotations.ApiStatus;

/**
* Public path-API name for a LiveCounter instance. Extends both the path-API
* {@link LiveInstance} marker and the existing internal {@link LiveCounter}
* so that existing instance-level consumers can use it interchangeably.
* <p>
* Logically sealed.
*/
@ApiStatus.NonExtendable
public interface LiveCounterInstance extends LiveInstance, LiveCounter {
}
65 changes: 65 additions & 0 deletions lib/src/main/java/io/ably/lib/objects/path/LiveCreate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.ably.lib.objects.path;

import io.ably.lib.objects.type.map.LiveMapValue;
import org.jetbrains.annotations.NotNull;

import java.util.Map;

/**
* Package-private factory for the two creation-token kinds — kept here so
* the public {@link LiveMap} / {@link LiveCounter} factories don't have to
* expose any implementation classes.
* <p>
* Both tokens implement {@link LiveValue} so they can be passed straight to
* {@code PathObject#set(...)}. They have no object identity until the
* enclosing wire operation lands.
*/
final class LiveCreate {

private LiveCreate() { /* no instances */ }

@NotNull
static LiveValue map(@NotNull Map<String, LiveValue> entries) {
return new MapCreateToken(entries);
}

@NotNull
static LiveValue counter(@NotNull Number initialValue) {
return new CounterCreateToken(initialValue);
}

/** Tag interface for the two token kinds — read by the impl module. */
interface CreationToken extends LiveValue {}

static final class MapCreateToken implements CreationToken {
final Map<String, LiveValue> entries;

MapCreateToken(@NotNull Map<String, LiveValue> entries) {
this.entries = entries;
}

@Override
public @NotNull LiveMapValue toMapValue() {
throw new UnsupportedOperationException(
"MapCreateToken is a creation token; it has no LiveMapValue until the operation lands");
}

@Override public String toString() { return "MapCreate" + entries.keySet(); }
}

static final class CounterCreateToken implements CreationToken {
final Number initialValue;

CounterCreateToken(@NotNull Number initialValue) {
this.initialValue = initialValue;
}

@Override
public @NotNull LiveMapValue toMapValue() {
throw new UnsupportedOperationException(
"CounterCreateToken is a creation token; it has no LiveMapValue until the operation lands");
}

@Override public String toString() { return "CounterCreate(" + initialValue + ")"; }
}
}
33 changes: 33 additions & 0 deletions lib/src/main/java/io/ably/lib/objects/path/LiveInstance.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.ably.lib.objects.path;

import io.ably.lib.objects.ObjectsSubscription;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NonBlocking;
import org.jetbrains.annotations.NotNull;

/**
* Marker for live (collaborative) instances sitting at a path: either a
* {@link LiveMapInstance} or a {@link LiveCounterInstance}.
* <p>
* Logically sealed.
*/
@ApiStatus.NonExtendable
public interface LiveInstance extends LiveValue {

/**
* Object ID assigned by the server when the object was created.
* Example: {@code "counter:J7x6mAF8X5Ha60VBZb6GtXSgnKJQagNLgadUlgICjkk@1734628392000"}.
*/
@NotNull
String id();

/**
* Instance-pinned subscription — survives even if this instance is moved
* or replaced at its old path. Implementations dispatch via the existing
* {@link io.ably.lib.objects.type.map.LiveMapChange} /
* {@link io.ably.lib.objects.type.counter.LiveCounterChange} machinery.
*/
@NonBlocking
@NotNull
ObjectsSubscription subscribe(@NotNull PathChangeListener listener);
}
43 changes: 43 additions & 0 deletions lib/src/main/java/io/ably/lib/objects/path/LiveMap.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.ably.lib.objects.path;

import org.jetbrains.annotations.NotNull;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
* Public factory for LiveMap creation tokens used by atomic deep-create
* {@code PathObject#set(key, LiveMap.create(...))}.
* <p>
* Note: this class is intentionally a factory, NOT the instance type.
* The runtime instance type is {@link LiveMapInstance}, which extends the
* internal {@link io.ably.lib.objects.type.map.LiveMap}. Naming follows the
* Ably docs (which expose {@code LiveMap.create(...)}); see PR #1190
* review for the rationale.
* <p>
* The returned {@link LiveValue} is a creation token — it has no object
* identity until the enclosing {@code set} operation lands.
*/
public final class LiveMap {

private LiveMap() { /* factory only */ }

/** Create an empty LiveMap. */
@NotNull
public static LiveValue create() {
return create(Collections.<String, LiveValue>emptyMap());
}

/**
* Create a LiveMap with the given nested initial contents. Values may be
* {@link LivePrimitive}s, other {@code LiveMap.create(...)} tokens, or
* {@code LiveCounter.create(...)} tokens.
*/
@NotNull
public static LiveValue create(@NotNull Map<String, LiveValue> entries) {
// Defensive copy; downstream impl consumes lazily during the wire op.
Map<String, LiveValue> snapshot = new HashMap<String, LiveValue>(entries);
return LiveCreate.map(snapshot);
}
}
15 changes: 15 additions & 0 deletions lib/src/main/java/io/ably/lib/objects/path/LiveMapInstance.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.ably.lib.objects.path;

import io.ably.lib.objects.type.map.LiveMap;
import org.jetbrains.annotations.ApiStatus;

/**
* Public path-API name for a LiveMap instance. Extends both the path-API
* {@link LiveInstance} marker and the existing internal {@link LiveMap} so
* that existing instance-level consumers can use it interchangeably.
* <p>
* Logically sealed.
*/
@ApiStatus.NonExtendable
public interface LiveMapInstance extends LiveInstance, LiveMap {
}
77 changes: 77 additions & 0 deletions lib/src/main/java/io/ably/lib/objects/path/LivePrimitive.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package io.ably.lib.objects.path;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

/**
* Wrapper for primitive leaf values stored at a path. Mirrors the existing
* {@link io.ably.lib.objects.type.map.LiveMapValue} union, minus the
* live-object variants (those are represented by {@link LiveInstance}).
* <p>
* The supported underlying types are {@code String}, {@code Number},
* {@code Boolean}, {@code byte[]}, {@link JsonArray} and {@link JsonObject}
* (spec RTO11a1).
* <p>
* Logically sealed.
*/
@ApiStatus.NonExtendable
public interface LivePrimitive extends LiveValue {

// ---- Static factories --------------------------------------------------

@NotNull
static LivePrimitive of(@NotNull String value) {
return LiveValues.primitive(value);
}

@NotNull
static LivePrimitive of(@NotNull Number value) {
return LiveValues.primitive(value);
}

@NotNull
static LivePrimitive of(boolean value) {
return LiveValues.primitive(value);
}

@NotNull
static LivePrimitive of(byte @NotNull [] value) {
return LiveValues.primitive(value);
}

@NotNull
static LivePrimitive of(@NotNull JsonArray value) {
return LiveValues.primitive(value);
}

@NotNull
static LivePrimitive of(@NotNull JsonObject value) {
return LiveValues.primitive(value);
}

// ---- Raw access --------------------------------------------------------

/** Boxed underlying value, same shape as {@link io.ably.lib.objects.type.map.LiveMapValue#getValue()}. */
@NotNull
Object raw();

// ---- Type checks (mirror LiveMapValue.isXxx) ---------------------------

boolean isString();
boolean isNumber();
boolean isBoolean();
boolean isBinary();
boolean isJsonArray();
boolean isJsonObject();

// ---- Typed accessors (throw IllegalStateException on mismatch) ---------

@NotNull String asString();
@NotNull Number asNumber();
boolean asBoolean();
byte @NotNull [] asBinary();
@NotNull JsonArray asJsonArray();
@NotNull JsonObject asJsonObject();
}
35 changes: 35 additions & 0 deletions lib/src/main/java/io/ably/lib/objects/path/LiveValue.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.ably.lib.objects.path;

import io.ably.lib.objects.type.map.LiveMapValue;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

/**
* Root marker for anything that can sit at a path in the LiveObjects tree.
* <p>
* Logically sealed: the only valid implementations are bundled with the SDK
* ({@link LivePrimitive}, {@link LiveMapInstance}, {@link LiveCounterInstance},
* and the internal {@code MapCreate} / {@code CounterCreate} creation tokens).
* The interface is annotated {@link ApiStatus.NonExtendable} — third parties
* must not implement it.
* <p>
* Spec: see <a href="https://sdk.ably.com/builds/ably/specification/main/objects-features/">Objects feature spec</a>.
*/
@ApiStatus.NonExtendable
public interface LiveValue {

/**
* Bridge to the spec-aligned {@link LiveMapValue} union used by the
* underlying internal LiveMap representation. Never null.
*/
@NotNull
LiveMapValue toMapValue();

/**
* Inverse bridge — wraps a {@link LiveMapValue} as a path-API {@link LiveValue}.
*/
@NotNull
static LiveValue fromMapValue(@NotNull LiveMapValue value) {
return LiveValues.from(value);
}
}
Loading
Loading