Conversation
WalkthroughAdds 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
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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).
| LiveMapPathObject myObject = channel.getObject().get(); // The singular object | |
| LiveMapPathObject myObject = channel.object().get(); // The singular object |
| PathObject get(String key); | ||
| PathObject at(String path); | ||
| LiveMap instance() throws AblyException; |
There was a problem hiding this comment.
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.
| // 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(); |
There was a problem hiding this comment.
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.
| // 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(); |
| # Update player position | ||
| move_options = MessageOptions( | ||
| id=f"move-{time.time()}", | ||
| extras={"action": "move", "timestamp": time.time()} | ||
| ) |
There was a problem hiding this comment.
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.
| ```java | ||
| // Path to nested value | ||
| StringPathObject colour = myObject.at("shape.colour").asStringPrimitive(); | ||
| String value = colour.value(); // "red" |
There was a problem hiding this comment.
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.
| PathObject get(String key); | ||
| PathObject at(String path); | ||
| LiveMap instance() throws AblyException; | ||
|
|
There was a problem hiding this comment.
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.
| // 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 |
| Double value() throws AblyException; | ||
| void increment(Number amount, MessageOptions options) throws AblyException; |
There was a problem hiding this comment.
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.
| 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; |
| }, SubscriptionOptions.depth(2)); | ||
|
|
||
| // Update game state | ||
| game.at("state").asLiveMap().set("state", Primitive.create("started")); |
There was a problem hiding this comment.
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.
| game.at("state").asLiveMap().set("state", Primitive.create("started")); | |
| game.set("state", Primitive.create("started")); |
| ) | ||
|
|
||
| # Update game state | ||
| game.at("state").as_live_map().set("state", Primitive.create("started")) |
There was a problem hiding this comment.
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.
| game.at("state").as_live_map().set("state", Primitive.create("started")) | |
| game.at("state").as_primitive().set(Primitive.create("started")) |
cf49ba5 to
be13cdc
Compare
There was a problem hiding this comment.
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: UseDoubleinstead ofNumberfor counter operations.The
incrementanddecrementmethods acceptNumber, which is overly broad (includesInteger,Long,BigInteger, etc.). Sincevalue()returnsDouble(line 160), and the Python equivalent usesfloat(lines 300-313), consider usingDoublefor 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; |
There was a problem hiding this comment.
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.
| 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.
| void set(String key, Value value, MessageOptions options) throws AblyException; | ||
| void remove(String key, MessageOptions options) throws AblyException; |
There was a problem hiding this comment.
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:
- Add overloaded methods without the
optionsparameter, or - Update all examples to explicitly pass
nullor a defaultMessageOptions
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.
| public interface LiveListPathObject extends PathObject { | ||
| PathObject get(int index); | ||
| long size(); | ||
| } |
There was a problem hiding this comment.
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.
| def subscribe( | ||
| self, | ||
| listener: ObjectChangeListener, | ||
| options: Optional[SubscriptionOptions] = None | ||
| ) -> Subscription: | ||
| pass |
There was a problem hiding this comment.
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.
| ## 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()) | ||
| ``` | ||
|
|
There was a problem hiding this comment.
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.
| // Update primitive value | ||
| myObject.at("user.name").asLiveMap().set( | ||
| "name", | ||
| Primitive.create("Bob") | ||
| ); |
There was a problem hiding this comment.
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.
| // 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")); |
There was a problem hiding this comment.
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, orgame.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.
| 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.
New API for LO
Summary by CodeRabbit