Skip to content

docs: add path-based LiveObjects API proposal#1190

Open
ttypic wants to merge 1 commit intomainfrom
draft-new-lo-api
Open

docs: add path-based LiveObjects API proposal#1190
ttypic wants to merge 1 commit intomainfrom
draft-new-lo-api

Conversation

@ttypic
Copy link
Contributor

@ttypic ttypic commented Feb 11, 2026

New API for LO

Summary by CodeRabbit

  • Documentation
    • Added comprehensive documentation for a new path-based LiveObjects API for Java/Kotlin and Python, covering core concepts (path-based access, deferred resolution, atomic creation), a typed PathObject model, type-safe usage patterns, subscription semantics, collection accessors, migration guidance from the current API, usage examples, performance considerations, and a roadmap with open questions.

@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

Walkthrough

Adds a new comprehensive specification document describing a path-based LiveObjects API design for Java/Kotlin and Python, detailing typed PathObject models, core behaviors (deferred resolution, atomic deep creation), subscriptions, usage examples, migration guidance, and open questions.

Changes

Cohort / File(s) Summary
Path-Based API Specification
liveobjects/PATH_BASED_API_JAVA_PYTHON.md
Added a large (≈987-line) specification documenting a path-based LiveObjects API: typed PathObject interfaces/classes for Java and Python, primitives and live collections, subscription model, usage examples, migration guide, design decisions, performance notes, and open questions.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 I hopped along the code-lined trail,
Paths unfolding like a leafy veil,
Java, Python, side by side, I sing,
Deferred roots and counters on the wing,
A tiny rabbit cheers this API spring! 🎉

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding documentation for a new path-based LiveObjects API proposal, which aligns with the single large documentation file added.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch draft-new-lo-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot temporarily deployed to staging/pull/1190/features February 11, 2026 11:46 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@liveobjects/PATH_BASED_API_JAVA_PYTHON.md`:
- Around line 88-99: The fenced code block that begins with "Value (all types
that can be stored)" is missing a language tag (MD040); update the opening
triple-backtick to include a language identifier such as "text" or "console"
(e.g., ```text) so the block is language-tagged and markdownlint passes, leaving
the block content unchanged.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new design/proposal document describing a planned path-based LiveObjects API for Java/Kotlin and Python, including core concepts, proposed interfaces, examples, and migration notes.

Changes:

  • Introduces a comprehensive implementation plan + proposed type system and interfaces for a path-based LO API.
  • Adds end-to-end usage examples (creation, mutation, subscriptions, compact snapshots, instance API).
  • Documents migration guidance and open design questions/next steps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


**Java:**
```java
LiveMapPathObject myObject = channel.getObject().get(); // The singular object
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Java examples use both channel.getObject().get() and channel.object().get() to retrieve the root object; the document should pick one canonical API shape and use it consistently (and align Python/Java naming where intended).

Suggested change
LiveMapPathObject myObject = channel.getObject().get(); // The singular object
LiveMapPathObject myObject = channel.object().get(); // The singular object

Copilot uses AI. Check for mistakes.
Comment on lines +141 to +143
PathObject get(String key);
PathObject at(String path);
LiveMap instance() throws AblyException;
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LiveMapPathObject.instance() is declared as returning a non-null LiveMap, but the later example checks if (playerInstance != null). Clarify whether instance() can return null (and reflect that in the signature, e.g., @Nullable/Optional) or remove the null-check and describe the error behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +420 to +427
// Update primitive value
myObject.at("user.name").asLiveMap().set(
"name",
Primitive.create("Bob")
);

// Or navigate to parent and set
LiveMapPathObject user = myObject.get("user").asLiveMap();
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mutation example navigates to "user.name" (a primitive path) and then casts to LiveMap before calling set("name", ...), which is internally inconsistent. To update a primitive, the example should operate on the parent map path ("user") or the API should define a direct primitive setter for PathObject/StringPathObject.

Suggested change
// Update primitive value
myObject.at("user.name").asLiveMap().set(
"name",
Primitive.create("Bob")
);
// Or navigate to parent and set
LiveMapPathObject user = myObject.get("user").asLiveMap();
// Update primitive value via parent map path
myObject.at("user").asLiveMap().set(
"name",
Primitive.create("Bob")
);
// Or fetch parent map object and then set
LiveMapPathObject user = myObject.at("user").asLiveMap();

Copilot uses AI. Check for mistakes.
Comment on lines +827 to +831
# Update player position
move_options = MessageOptions(
id=f"move-{time.time()}",
extras={"action": "move", "timestamp": time.time()}
)
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Python complete example uses time.time() when building move_options, but time is not imported in the snippet. Add import time (or avoid time usage) so the example runs as written.

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +23
```java
// Path to nested value
StringPathObject colour = myObject.at("shape.colour").asStringPrimitive();
String value = colour.value(); // "red"
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The document uses both colour and color as field/key names across examples (shape.colour vs later "color"). Pick one spelling for keys/paths and use it consistently to avoid implying different schema fields.

Copilot uses AI. Check for mistakes.
PathObject get(String key);
PathObject at(String path);
LiveMap instance() throws AblyException;

Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LiveMapPathObject.set/remove take a required MessageOptions options parameter here, but most Java examples call set(...)/remove(...) without providing options. Either document overloads/default options (e.g., options nullable or a MessageOptions.none()), or update the examples so they compile against the proposed signatures.

Suggested change
// Convenience overloads using default message options
void set(String key, Value value) throws AblyException;
void remove(String key) throws AblyException;
// Full-control overloads with explicit message options

Copilot uses AI. Check for mistakes.
Comment on lines +160 to +161
Double value() throws AblyException;
void increment(Number amount, MessageOptions options) throws AblyException;
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LiveCounterPathObject.increment/decrement require MessageOptions options here, but later examples call increment(1) / decrement(25) with no options. Align the method signatures and examples (overload vs optional/default options) so the sample code is implementable as written.

Suggested change
Double value() throws AblyException;
void increment(Number amount, MessageOptions options) throws AblyException;
Double value() throws AblyException;
void increment(Number amount) throws AblyException;
void increment(Number amount, MessageOptions options) throws AblyException;
void decrement(Number amount) throws AblyException;

Copilot uses AI. Check for mistakes.
}, SubscriptionOptions.depth(2));

// Update game state
game.at("state").asLiveMap().set("state", Primitive.create("started"));
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

game.at("state") refers to a string primitive (see initialization game.set("state", Primitive.create(...))), so casting it to asLiveMap() and setting a "state" key is inconsistent. This should update the root key (e.g., game.set("state", ...)) or operate on the correct parent map path.

Suggested change
game.at("state").asLiveMap().set("state", Primitive.create("started"));
game.set("state", Primitive.create("started"));

Copilot uses AI. Check for mistakes.
)

# Update game state
game.at("state").as_live_map().set("state", Primitive.create("started"))
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python example updates state via game.at("state").as_live_map().set(...), but state was initialized as a primitive string. Adjust the example to set the root key directly or change the data model so state is a map if that’s the intention.

Suggested change
game.at("state").as_live_map().set("state", Primitive.create("started"))
game.at("state").as_primitive().set(Primitive.create("started"))

Copilot uses AI. Check for mistakes.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@liveobjects/PATH_BASED_API_JAVA_PYTHON.md`:
- Around line 154-157: LiveListPathObject currently only exposes read methods
(get, size) making lists immutable in the path-based API; update the
LiveListPathObject interface to include mutation operations analogous to
LiveMapPathObject such as add(PathObject value), add(int index, PathObject
value) or insert, set(int index, PathObject value), remove(int index) and
clear() (optionally addAll(Collection<PathObject>) and remove(Object) if
needed), and ensure method signatures use PathObject for elements and preserve
existing get/size; update any related docs/examples to reflect the new mutating
API and maintain consistency with PathObject semantics.
- Around line 318-868: Add parallel Java and Python LiveList usage examples
mirroring existing sections: show creating a list with LiveList.create(...),
setting it on a path via myObject.set("items", LiveList.create(...)) and
retrieving via myObject.at("items").asLiveList()/as_live_list(); demonstrate
mutation methods (append/insert/remove/pop or add/insert/remove depending on SDK
naming — reference LiveListPathObject methods such as append/add, insert,
remove, and get/get(index) or at(index)), accessing items by index
(myList.get(0)/my_list.get(0) or my_list.at(0)), subscribing to list changes
using subscribe/subscribe_async on a LiveListPathObject, iterating
entries/values and using size()/size, and showing compact() snapshot of the
list; place these examples in the Usage Examples block alongside other
collection examples so readers can find LiveList usage next to LiveMap and
counters.
- Line 143: Change the return type of the interface method instance() from
LiveMap to Optional<LiveMap> to match the Python API and avoid nulls; update the
method signature to Optional<LiveMap> instance() throws AblyException, add the
necessary java.util.Optional import, update all implementations of instance() to
return Optional.empty() or Optional.of(map) as appropriate, and update callers
to unwrap the Optional (or use orElse/orElseThrow) and adjust any
JavaDoc/comments referencing instance() to reflect the Optional return.
- Line 728: The snippet uses game.at("state").asLiveMap().set("state",
Primitive.create("started")) which wrongly treats the "state" field as a
LiveMap; replace this with either game.set("state", Primitive.create("started"))
when the game object is already a LiveMapPathObject, or use
game.asLiveMap().set("state", Primitive.create("started")) if you need explicit
casting; also apply the analogous correction in the Python variant (the similar
game.at("state").as_live_map().set(...) occurrence) so the root game's "state"
field is updated correctly.
- Around line 145-146: The method signatures for set and remove require
MessageOptions but examples call them without options; add Java overloads to
match usage by introducing void set(String key, Value value) throws
AblyException and void remove(String key) throws AblyException (no
MessageOptions) in the same interface, and implement these overloads in the
corresponding classes to delegate to the existing set(String, Value,
MessageOptions) and remove(String, MessageOptions) by passing null (or a default
MessageOptions) so existing implementations and examples work without changing
all call sites.
- Around line 420-424: The path used to mutate the name navigates into the
primitive ("user.name") then casts to LiveMap which is wrong; change the
navigation to the parent map by calling at("user") and then use
asLiveMap().set("name", Primitive.create("Bob")) so you set the "name" key on
the user map (update the invocation of myObject.at(...).asLiveMap().set(...)
accordingly).
- Around line 209-214: Add a Python-specific async variant to the PathObject
interface by defining subscribe_async alongside the existing subscribe method:
declare a method named subscribe_async(self, options:
Optional[SubscriptionOptions] = None) that returns an async iterator (e.g.,
typing.AsyncIterator[ObjectChangeEvent] or AsyncGenerator) so examples using
subscribe_async() work; ensure you import AsyncIterator (or AsyncGenerator) and
reference the ObjectChangeEvent and SubscriptionOptions types consistently with
the existing subscribe() signature.
🧹 Nitpick comments (1)
liveobjects/PATH_BASED_API_JAVA_PYTHON.md (1)

161-162: Use Double instead of Number for counter operations.

The increment and decrement methods accept Number, which is overly broad (includes Integer, Long, BigInteger, etc.). Since value() returns Double (line 160), and the Python equivalent uses float (lines 300-313), consider using Double for consistency and to avoid ambiguity.

♻️ Proposed change
-  void increment(Number amount, MessageOptions options) throws AblyException;
-  void decrement(Number amount, MessageOptions options) throws AblyException;
+  void increment(Double amount, MessageOptions options) throws AblyException;
+  void decrement(Double amount, MessageOptions options) throws AblyException;

public interface LiveMapPathObject extends PathObject {
PathObject get(String key);
PathObject at(String path);
LiveMap instance() throws AblyException;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Return type should be Optional<LiveMap> for null safety.

The instance() method returns LiveMap, but the object might not exist at the path. The Python equivalent (line 251) correctly returns Optional['LiveMap']. For consistency and null safety, this should return Optional<LiveMap>.

🔧 Proposed fix
-  LiveMap instance() throws AblyException;
+  Optional<LiveMap> instance() throws AblyException;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LiveMap instance() throws AblyException;
Optional<LiveMap> instance() throws AblyException;
🤖 Prompt for AI Agents
In `@liveobjects/PATH_BASED_API_JAVA_PYTHON.md` at line 143, Change the return
type of the interface method instance() from LiveMap to Optional<LiveMap> to
match the Python API and avoid nulls; update the method signature to
Optional<LiveMap> instance() throws AblyException, add the necessary
java.util.Optional import, update all implementations of instance() to return
Optional.empty() or Optional.of(map) as appropriate, and update callers to
unwrap the Optional (or use orElse/orElseThrow) and adjust any JavaDoc/comments
referencing instance() to reflect the Optional return.

Comment on lines +145 to +146
void set(String key, Value value, MessageOptions options) throws AblyException;
void remove(String key, MessageOptions options) throws AblyException;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Method signatures don't match usage examples.

The set and remove methods require MessageOptions as a mandatory parameter, but multiple usage examples throughout the document call these methods without providing options (see lines 39, 428, 455, etc.). Java doesn't support optional parameters, so you need to either:

  1. Add overloaded methods without the options parameter, or
  2. Update all examples to explicitly pass null or a default MessageOptions

The Python interface (lines 255-260, 264) correctly uses Optional[MessageOptions] = None.

🔧 Proposed fix (add overloads)
   void set(String key, Value value, MessageOptions options) throws AblyException;
+  void set(String key, Value value) throws AblyException;
   void remove(String key, MessageOptions options) throws AblyException;
+  void remove(String key) throws AblyException;
🤖 Prompt for AI Agents
In `@liveobjects/PATH_BASED_API_JAVA_PYTHON.md` around lines 145 - 146, The method
signatures for set and remove require MessageOptions but examples call them
without options; add Java overloads to match usage by introducing void
set(String key, Value value) throws AblyException and void remove(String key)
throws AblyException (no MessageOptions) in the same interface, and implement
these overloads in the corresponding classes to delegate to the existing
set(String, Value, MessageOptions) and remove(String, MessageOptions) by passing
null (or a default MessageOptions) so existing implementations and examples work
without changing all call sites.

Comment on lines +154 to +157
public interface LiveListPathObject extends PathObject {
PathObject get(int index);
long size();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

LiveListPathObject lacks mutation methods.

The LiveListPathObject interface only provides read operations (get and size), but no mutation methods like add, remove, set, or insert. This makes LiveList effectively read-only in the path-based API, which seems like a significant limitation. Consider adding mutation methods similar to LiveMapPathObject.

Would you like me to propose a complete API surface for LiveListPathObject with mutation operations?

🤖 Prompt for AI Agents
In `@liveobjects/PATH_BASED_API_JAVA_PYTHON.md` around lines 154 - 157,
LiveListPathObject currently only exposes read methods (get, size) making lists
immutable in the path-based API; update the LiveListPathObject interface to
include mutation operations analogous to LiveMapPathObject such as
add(PathObject value), add(int index, PathObject value) or insert, set(int
index, PathObject value), remove(int index) and clear() (optionally
addAll(Collection<PathObject>) and remove(Object) if needed), and ensure method
signatures use PathObject for elements and preserve existing get/size; update
any related docs/examples to reflect the new mutating API and maintain
consistency with PathObject semantics.

Comment on lines +209 to +214
def subscribe(
self,
listener: ObjectChangeListener,
options: Optional[SubscriptionOptions] = None
) -> Subscription:
pass
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing subscribe_async method in Python interface.

The usage examples (lines 541-544, 847) demonstrate subscribe_async() returning an async iterator, but this method isn't defined in the PathObject interface. You should add this Python-specific async variant to the interface definition.

➕ Proposed addition
     def subscribe(
         self,
         listener: ObjectChangeListener,
         options: Optional[SubscriptionOptions] = None
     ) -> Subscription:
         pass
+
+    `@abstractmethod`
+    def subscribe_async(
+        self,
+        options: Optional[SubscriptionOptions] = None
+    ) -> AsyncIterator[ObjectChangeEvent]:
+        pass
🤖 Prompt for AI Agents
In `@liveobjects/PATH_BASED_API_JAVA_PYTHON.md` around lines 209 - 214, Add a
Python-specific async variant to the PathObject interface by defining
subscribe_async alongside the existing subscribe method: declare a method named
subscribe_async(self, options: Optional[SubscriptionOptions] = None) that
returns an async iterator (e.g., typing.AsyncIterator[ObjectChangeEvent] or
AsyncGenerator) so examples using subscribe_async() work; ensure you import
AsyncIterator (or AsyncGenerator) and reference the ObjectChangeEvent and
SubscriptionOptions types consistently with the existing subscribe() signature.

Comment on lines +318 to +868
## Usage Examples

### Getting Started

**Java:**
```java
// Get channel
RealtimeChannel channel = client.channels.get("my-channel");

// Get root object (waits for sync)
LiveMapPathObject myObject = channel.getObject().get();

// Access nested primitive value
StringPathObject name = myObject.at("user.name").asStringPrimitive();
String nameValue = name.value(); // "Alice"

// Access nested counter
LiveCounterPathObject visits = myObject.at("stats.visits").asLiveCounter();
Double visitsValue = visits.value(); // 42.0
```

**Python:**
```python
# Get channel
channel = client.channels.get("my-channel")

# Get root object (waits for sync)
my_object = await channel.object.get()

# Access nested primitive value
name = my_object.at("user.name").as_string_primitive()
name_value = name.value() # "Alice"

# Access nested counter
visits = my_object.at("stats.visits").as_live_counter()
visits_value = visits.value() # 42.0
```

### Creating Objects

**Java:**
```java
// Create simple map
myObject.set("user", LiveMap.create(Map.of(
"name", Primitive.create("Alice"),
"age", Primitive.create(30),
"active", Primitive.create(true)
)));

// Create nested structure
myObject.set("game", LiveMap.create(Map.of(
"title", Primitive.create("Chess"),
"players", LiveMap.create(Map.of(
"alice", LiveMap.create(Map.of(
"score", LiveCounter.create(0),
"color", Primitive.create("white")
)),
"bob", LiveMap.create(Map.of(
"score", LiveCounter.create(0),
"color", Primitive.create("black")
))
)),
"status", Primitive.create("ongoing")
)));

// Create counter
myObject.set("visits", LiveCounter.create(0));
```

**Python:**
```python
# Create simple map
my_object.set("user", LiveMap.create({
"name": Primitive.create("Alice"),
"age": Primitive.create(30),
"active": Primitive.create(True)
}))

# Create nested structure
my_object.set("game", LiveMap.create({
"title": Primitive.create("Chess"),
"players": LiveMap.create({
"alice": LiveMap.create({
"score": LiveCounter.create(0),
"color": Primitive.create("white")
}),
"bob": LiveMap.create({
"score": LiveCounter.create(0),
"color": Primitive.create("black")
})
}),
"status": Primitive.create("ongoing")
}))

# Create counter
my_object.set("visits", LiveCounter.create(0))
```

### Mutations

**Java:**
```java
// Update primitive value
myObject.at("user.name").asLiveMap().set(
"name",
Primitive.create("Bob")
);

// Or navigate to parent and set
LiveMapPathObject user = myObject.get("user").asLiveMap();
user.set("name", Primitive.create("Bob"));

// Increment counter
myObject.at("stats.visits").asLiveCounter().increment(1);

// With message options
MessageOptions options = new MessageOptions.Builder()
.id("msg-123")
.extras(Map.of("source", "mobile"))
.build();

myObject.at("user").asLiveMap().set("status", Primitive.create("online"), options);

// Remove key
myObject.at("user").asLiveMap().remove("oldField");
```

**Python:**
```python
# Update primitive value
my_object.at("user").as_live_map().set(
"name",
Primitive.create("Bob")
)

# Or navigate to parent and set
user = my_object.get("user").as_live_map()
user.set("name", Primitive.create("Bob"))

# Increment counter
my_object.at("stats.visits").as_live_counter().increment(1)

# With message options
options = MessageOptions(id="msg-123", extras={"source": "mobile"})
my_object.at("user").as_live_map().set(
"status",
Primitive.create("online"),
options
)

# Remove key
my_object.at("user").as_live_map().remove("old_field")
```

### Subscriptions

**Java:**
```java
// Subscribe to all changes (unlimited depth)
Subscription sub1 = myObject.subscribe(event -> {
System.out.println("Changed at: " + event.object().path());
System.out.println("By: " + event.message().getClientId());
System.out.println("Operation: " + event.message().getOperation());
}, SubscriptionOptions.unlimited());

// Subscribe to specific path
Subscription sub2 = myObject.at("user.name").subscribe(event -> {
StringPathObject name = event.object().asStringPrimitive();
System.out.println("Name changed to: " + name.value());
}, SubscriptionOptions.unlimited());

// Subscribe with depth limit (only top-level changes)
Subscription sub3 = myObject.subscribe(event -> {
System.out.println("Top-level change: " + event.object().path());
}, SubscriptionOptions.depth(1));

// Subscribe to counter changes
myObject.at("stats.visits").subscribe(event -> {
LiveCounterPathObject visits = event.object().asLiveCounter();
System.out.println("Visits: " + visits.value());
}, SubscriptionOptions.unlimited());

// Unsubscribe
sub1.unsubscribe();
```

**Python:**
```python
# Subscribe to all changes (unlimited depth)
def on_change(event):
print(f"Changed at: {event.object.path()}")
print(f"By: {event.message.client_id}")
print(f"Operation: {event.message.operation}")

sub1 = my_object.subscribe(on_change, SubscriptionOptions.unlimited())

# Subscribe to specific path
def on_name_change(event):
name = event.object.as_string_primitive()
print(f"Name changed to: {name.value()}")

sub2 = my_object.at("user.name").subscribe(
on_name_change,
SubscriptionOptions.unlimited()
)

# Subscribe with depth limit (only top-level changes)
sub3 = my_object.subscribe(
lambda event: print(f"Top-level change: {event.object.path()}"),
SubscriptionOptions.with_depth(1)
)

# Subscribe to counter changes
def on_visits_change(event):
visits = event.object.as_live_counter()
print(f"Visits: {visits.value()}")

my_object.at("stats.visits").subscribe(
on_visits_change,
SubscriptionOptions.unlimited()
)

# Async iterator style (Python-specific)
async for event in my_object.subscribe_async():
print(f"Event: {event.object.path()}")
if some_condition:
break

# Unsubscribe
sub1.unsubscribe()
```

### Collection Accessors

**Java:**
```java
LiveMapPathObject user = myObject.get("user").asLiveMap();

// Iterate entries
for (Map.Entry<String, PathObject> entry : user.entries()) {
System.out.println(entry.getKey() + ": " + entry.getValue().path());
}

// Get keys
for (String key : user.keys()) {
System.out.println("Key: " + key);
}

// Get values
for (PathObject value : user.values()) {
System.out.println("Value at: " + value.path());
}

// Get size
long count = user.size();
System.out.println("User has " + count + " properties");
```

**Python:**
```python
user = my_object.get("user").as_live_map()

# Iterate entries
for key, value in user.entries():
print(f"{key}: {value.path()}")

# Get keys
for key in user.keys():
print(f"Key: {key}")

# Get values
for value in user.values():
print(f"Value at: {value.path()}")

# Get size
count = user.size()
print(f"User has {count} properties")
```

### Compact Representation

**Java:**
```java
// Get JSON snapshot of entire object tree
JsonValue snapshot = myObject.compact();
System.out.println(snapshot.toString());
// {"user":{"name":"Alice","age":30},"stats":{"visits":42}}

// Get snapshot of nested object
JsonValue userSnapshot = myObject.get("user").compact();
System.out.println(userSnapshot.toString());
// {"name":"Alice","age":30}
```

**Python:**
```python
# Get JSON snapshot of entire object tree
snapshot = my_object.compact()
print(snapshot)
# {"user": {"name": "Alice", "age": 30}, "stats": {"visits": 42}}

# Get snapshot of nested object
user_snapshot = my_object.get("user").compact()
print(user_snapshot)
# {"name": "Alice", "age": 30}
```

### Instance API

**Java:**
```java
// Get specific object instance
LiveMapPathObject player = myObject.at("game.players.alice").asLiveMap();
LiveMap playerInstance = player.instance();

if (playerInstance != null) {
// Get object ID
String objectId = playerInstance.id();
System.out.println("Player object ID: " + objectId);

// Subscribe to this specific instance
// (subscription persists even if this object moves to different path)
playerInstance.subscribe(event -> {
System.out.println("Player instance " + objectId + " changed");
}, SubscriptionOptions.unlimited());

// Mutate via instance
playerInstance.set("score", LiveCounter.create(10));
}
```

**Python:**
```python
# Get specific object instance
player = my_object.at("game.players.alice").as_live_map()
player_instance = player.instance()

if player_instance:
# Get object ID
object_id = player_instance.id()
print(f"Player object ID: {object_id}")

# Subscribe to this specific instance
# (subscription persists even if this object moves to different path)
player_instance.subscribe(
lambda event: print(f"Player instance {object_id} changed"),
SubscriptionOptions.unlimited()
)

# Mutate via instance
player_instance.set("score", LiveCounter.create(10))
```

### Complete Application Example

**Java:**
```java
public class GameApp {
public static void main(String[] args) throws AblyException {
// Initialize client
AblyRealtime client = new AblyRealtime(options);
RealtimeChannel channel = client.channels.get("game:123");

// Get root object
LiveMapPathObject game = channel.getObject().get();

// Initialize game state
game.set("state", Primitive.create("waiting"));
game.set("players", LiveMap.create());
game.set("score", LiveMap.create());
game.set("timer", LiveCounter.create(60));

// Add player
game.at("players").asLiveMap().set("alice", LiveMap.create(Map.of(
"name", Primitive.create("Alice"),
"team", Primitive.create("red"),
"health", LiveCounter.create(100),
"position", LiveMap.create(Map.of(
"x", Primitive.create(0),
"y", Primitive.create(0)
))
)));

// Subscribe to game state changes
game.at("state").subscribe(event -> {
StringPathObject state = event.object().asStringPrimitive();
System.out.println("Game state: " + state.value());

if ("started".equals(state.value())) {
startGameTimer(game);
}
}, SubscriptionOptions.unlimited());

// Subscribe to player health
game.at("players.alice.health").subscribe(event -> {
LiveCounterPathObject health = event.object().asLiveCounter();
System.out.println("Alice health: " + health.value());

if (health.value() <= 0) {
handlePlayerDeath("alice");
}
}, SubscriptionOptions.unlimited());

// Subscribe to all player changes (depth 2 = players.*/*)
game.at("players").subscribe(event -> {
System.out.println("Player update at: " + event.object().path());
System.out.println("By: " + event.message().getClientId());
}, SubscriptionOptions.depth(2));

// Update game state
game.at("state").asLiveMap().set("state", Primitive.create("started"));

// Update player position
MessageOptions moveOptions = new MessageOptions.Builder()
.id("move-" + System.currentTimeMillis())
.extras(Map.of("action", "move", "timestamp", System.currentTimeMillis()))
.build();

game.at("players.alice.position").asLiveMap().set(
"x",
Primitive.create(10),
moveOptions
);

// Damage player
game.at("players.alice.health").asLiveCounter().decrement(25);

// Get game snapshot
JsonValue snapshot = game.compact();
System.out.println("Game state: " + snapshot);
}

private static void startGameTimer(LiveMapPathObject game) {
// Timer logic
}

private static void handlePlayerDeath(String playerId) {
// Death handling logic
}
}
```

**Python:**
```python
import asyncio
from ably import AblyRealtime

class GameApp:
def __init__(self, client: AblyRealtime):
self.client = client
self.channel = client.channels.get("game:123")

async def run(self):
# Get root object
game = await self.channel.object.get()

# Initialize game state
game.set("state", Primitive.create("waiting"))
game.set("players", LiveMap.create())
game.set("score", LiveMap.create())
game.set("timer", LiveCounter.create(60))

# Add player
game.at("players").as_live_map().set("alice", LiveMap.create({
"name": Primitive.create("Alice"),
"team": Primitive.create("red"),
"health": LiveCounter.create(100),
"position": LiveMap.create({
"x": Primitive.create(0),
"y": Primitive.create(0)
})
}))

# Subscribe to game state changes
def on_state_change(event):
state = event.object.as_string_primitive()
print(f"Game state: {state.value()}")

if state.value() == "started":
self.start_game_timer(game)

game.at("state").subscribe(on_state_change, SubscriptionOptions.unlimited())

# Subscribe to player health
def on_health_change(event):
health = event.object.as_live_counter()
print(f"Alice health: {health.value()}")

if health.value() <= 0:
self.handle_player_death("alice")

game.at("players.alice.health").subscribe(
on_health_change,
SubscriptionOptions.unlimited()
)

# Subscribe to all player changes (depth 2 = players.*/*)
def on_player_change(event):
print(f"Player update at: {event.object.path()}")
print(f"By: {event.message.client_id}")

game.at("players").subscribe(
on_player_change,
SubscriptionOptions.with_depth(2)
)

# Update game state
game.at("state").as_live_map().set("state", Primitive.create("started"))

# Update player position
move_options = MessageOptions(
id=f"move-{time.time()}",
extras={"action": "move", "timestamp": time.time()}
)

game.at("players.alice.position").as_live_map().set(
"x",
Primitive.create(10),
move_options
)

# Damage player
game.at("players.alice.health").as_live_counter().decrement(25)

# Get game snapshot
snapshot = game.compact()
print(f"Game state: {snapshot}")

# Async iteration over events (Python-specific)
async for event in game.subscribe_async(SubscriptionOptions.unlimited()):
print(f"Event at: {event.object.path()}")
# Handle events...

def start_game_timer(self, game):
# Timer logic
pass

def handle_player_death(self, player_id: str):
# Death handling logic
pass


# Run the app
async def main():
client = AblyRealtime(options)
app = GameApp(client)
await app.run()

asyncio.run(main())
```

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing LiveList usage examples.

The document defines LiveListPathObject interfaces for both Java and Python, but the entire Usage Examples section (lines 318-868) contains no examples demonstrating how to create, manipulate, or subscribe to LiveList objects. Given that LiveList is a core collaborative type, please add usage examples showing:

  • Creating a LiveList
  • Adding/removing items (if mutation methods are added)
  • Accessing items by index
  • Subscribing to list changes
  • Iterating over list items

Would you like me to propose LiveList usage examples based on the existing API patterns?

🤖 Prompt for AI Agents
In `@liveobjects/PATH_BASED_API_JAVA_PYTHON.md` around lines 318 - 868, Add
parallel Java and Python LiveList usage examples mirroring existing sections:
show creating a list with LiveList.create(...), setting it on a path via
myObject.set("items", LiveList.create(...)) and retrieving via
myObject.at("items").asLiveList()/as_live_list(); demonstrate mutation methods
(append/insert/remove/pop or add/insert/remove depending on SDK naming —
reference LiveListPathObject methods such as append/add, insert, remove, and
get/get(index) or at(index)), accessing items by index
(myList.get(0)/my_list.get(0) or my_list.at(0)), subscribing to list changes
using subscribe/subscribe_async on a LiveListPathObject, iterating
entries/values and using size()/size, and showing compact() snapshot of the
list; place these examples in the Usage Examples block alongside other
collection examples so readers can find LiveList usage next to LiveMap and
counters.

Comment on lines +420 to +424
// Update primitive value
myObject.at("user.name").asLiveMap().set(
"name",
Primitive.create("Bob")
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Incorrect path navigation for mutation.

The code navigates to "user.name" (which should be a primitive value) then casts it to LiveMap. This is incorrect. To update the name field, you should navigate to the parent "user" map, not to "user.name". The correct pattern is shown immediately below at lines 427-428.

🐛 Proposed fix
 // Update primitive value
-myObject.at("user.name").asLiveMap().set(
+myObject.at("user").asLiveMap().set(
     "name",
     Primitive.create("Bob")
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Update primitive value
myObject.at("user.name").asLiveMap().set(
"name",
Primitive.create("Bob")
);
// Update primitive value
myObject.at("user").asLiveMap().set(
"name",
Primitive.create("Bob")
);
🤖 Prompt for AI Agents
In `@liveobjects/PATH_BASED_API_JAVA_PYTHON.md` around lines 420 - 424, The path
used to mutate the name navigates into the primitive ("user.name") then casts to
LiveMap which is wrong; change the navigation to the parent map by calling
at("user") and then use asLiveMap().set("name", Primitive.create("Bob")) so you
set the "name" key on the user map (update the invocation of
myObject.at(...).asLiveMap().set(...) accordingly).

}, SubscriptionOptions.depth(2));

// Update game state
game.at("state").asLiveMap().set("state", Primitive.create("started"));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Incorrect path navigation pattern.

The code navigates to "state" then casts it to LiveMap, which doesn't make sense since "state" should be a primitive value. To update the state field on the root game object, use either:

  • game.set("state", Primitive.create("started")) if game is already a LiveMapPathObject, or
  • game.asLiveMap().set("state", Primitive.create("started")) if explicit casting is needed

The same issue appears in the Python version at line 825.

🐛 Proposed fix
 // Update game state
-game.at("state").asLiveMap().set("state", Primitive.create("started"));
+game.set("state", Primitive.create("started"));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
game.at("state").asLiveMap().set("state", Primitive.create("started"));
// Update game state
game.set("state", Primitive.create("started"));
🤖 Prompt for AI Agents
In `@liveobjects/PATH_BASED_API_JAVA_PYTHON.md` at line 728, The snippet uses
game.at("state").asLiveMap().set("state", Primitive.create("started")) which
wrongly treats the "state" field as a LiveMap; replace this with either
game.set("state", Primitive.create("started")) when the game object is already a
LiveMapPathObject, or use game.asLiveMap().set("state",
Primitive.create("started")) if you need explicit casting; also apply the
analogous correction in the Python variant (the similar
game.at("state").as_live_map().set(...) occurrence) so the root game's "state"
field is updated correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant