From 94daf80dd5940ad485237858d84204626e1f8c6c Mon Sep 17 00:00:00 2001 From: sacOO7 Date: Wed, 8 Jul 2026 17:05:04 +0530 Subject: [PATCH 1/5] docs(liveobjects): migrate Java snippets and prose to the path-based API Rewrite the Java LiveObjects documentation from the deprecated callback-based API (io.ably.lib.objects, channel.getObjects()/getRoot()) to the new path-based typed API (channel.object.get() -> LiveMapPathObject, PathObject/ Instance typed views), mirroring the existing ably-js path-based content. Pages: - quickstart/java.mdx: full rewrite converging on the JS quickstart structure (channel.object.get().join(), runtime-classpath plugin discovery, path-based create/subscribe/update flow); drop the ObjectsCallback / blocking-vs-async section. - concepts/objects.mdx, operations.mdx: open Channel object / PathObject / Instance sections to Java; retag the old Root-object, aliasing/cycles, reachability and stale-reference regions to swift-only and delete their Java fences; extend the ObjectMessage/ObjectOperation/ObjectData tables to Java (ObjectOperationAction, ObjectsMapSemantics, byte[]). - concepts/path-object.mdx, instance.mdx: add Java to every code block; add Java "Typed views" sections (never-throwing path casts vs throwing Instance casts, getType()/ValueType, exists()); preserve the #typing and #get-a-compact-object anchors under the Java toggle. - map.mdx, counter.mdx: retag the old swift,java regions to swift-only with all old Java fences removed; add path-based Java fences beside the JS ones across every section. - lifecycle.mdx, typing.mdx, synchronization.mdx, storage.mdx, batch.mdx, inband-objects.mdx, index.mdx: sync events via ObjectStateEvent, Java typed views, channel-object wording, batch not-supported note, inband Pub/Sub Java siblings. - api/realtime-sdk/channels.mdx: document channel.object for Java (public field); move the removed #objects entry point to swift-only. - platform/errors/codes.mdx: repoint #root-object links to #channel-object. - nav/liveobjects.ts: language overrides for batch/storage; update the Java SDK javadoc link to the new package. Swift content is unchanged except for gate retags. Several pre-existing JS snippet bugs found during the migration are fixed in place. --- src/data/nav/liveobjects.ts | 5 +- src/pages/docs/api/realtime-sdk/channels.mdx | 6 +- src/pages/docs/liveobjects/batch.mdx | 14 +- .../docs/liveobjects/concepts/instance.mdx | 298 ++++++++++++++- .../docs/liveobjects/concepts/objects.mdx | 200 +++++----- .../docs/liveobjects/concepts/operations.mdx | 99 +++-- .../docs/liveobjects/concepts/path-object.mdx | 359 +++++++++++++++++- .../liveobjects/concepts/synchronization.mdx | 9 +- src/pages/docs/liveobjects/counter.mdx | 241 +++++++----- src/pages/docs/liveobjects/inband-objects.mdx | 67 +++- src/pages/docs/liveobjects/index.mdx | 2 +- src/pages/docs/liveobjects/lifecycle.mdx | 40 +- src/pages/docs/liveobjects/map.mdx | 358 +++++++++-------- .../docs/liveobjects/quickstart/java.mdx | 160 ++++---- src/pages/docs/liveobjects/storage.mdx | 2 +- src/pages/docs/liveobjects/typing.mdx | 38 ++ src/pages/docs/platform/errors/codes.mdx | 4 +- 17 files changed, 1372 insertions(+), 530 deletions(-) diff --git a/src/data/nav/liveobjects.ts b/src/data/nav/liveobjects.ts index 1b5adcb423..aed0809e2e 100644 --- a/src/data/nav/liveobjects.ts +++ b/src/data/nav/liveobjects.ts @@ -81,6 +81,7 @@ export default { { name: 'Batch operations', link: '/docs/liveobjects/batch', + languages: ['javascript', 'swift', 'java'], }, { name: 'Lifecycle events', @@ -97,6 +98,7 @@ export default { { name: 'Object storage', link: '/docs/liveobjects/storage', + languages: ['javascript', 'swift', 'java'], }, { name: 'Using the REST SDK', @@ -128,7 +130,8 @@ export default { external: true, }, { - link: 'https://sdk.ably.com/builds/ably/ably-java/main/javadoc/io/ably/lib/objects/RealtimeObjects.html', + // TODO: verify this URL resolves once the path-based API javadoc is published + link: 'https://sdk.ably.com/builds/ably/ably-java/main/javadoc/io/ably/lib/liveobjects/RealtimeObject.html', name: 'Java SDK', external: true, }, diff --git a/src/pages/docs/api/realtime-sdk/channels.mdx b/src/pages/docs/api/realtime-sdk/channels.mdx index ca348025ce..61aa899377 100644 --- a/src/pages/docs/api/realtime-sdk/channels.mdx +++ b/src/pages/docs/api/realtime-sdk/channels.mdx @@ -114,13 +114,13 @@ Provides access to the [`RealtimeAnnotations`](#realtime-annotations) object for - + #### object -Provides access to the [RealtimeObject](/docs/liveobjects) for this channel which can be used to read, modify and subscribe to LiveObjects on a channel. +Provides access to the [RealtimeObject](/docs/liveobjects) for this channel which can be used to read, modify and subscribe to LiveObjects on a channel.A public field providing access to the [RealtimeObject](/docs/liveobjects) for this channel, which can be used to read, modify and subscribe to LiveObjects on a channel. - + #### objects Provides access to the [Objects](/docs/liveobjects) object for this channel which can be used to read, modify and subscribe to LiveObjects on a channel. diff --git a/src/pages/docs/liveobjects/batch.mdx b/src/pages/docs/liveobjects/batch.mdx index e187720dd2..81b0dbee12 100644 --- a/src/pages/docs/liveobjects/batch.mdx +++ b/src/pages/docs/liveobjects/batch.mdx @@ -23,9 +23,19 @@ meta_description: "Group multiple objects operations into a single channel messa + + + + + + + + Batch operations allow multiple updates to be grouped into a single channel message and applied atomically. It ensures that all operations in a batch either succeed together or are discarded entirely. Batching is essential when multiple related updates to channel objects must be applied as a single atomic unit, for example, when application logic depends on multiple objects being updated simultaneously. Batching ensures that all operations in the batch either succeed or fail together. @@ -269,3 +279,5 @@ await myObject.batch((ctx) => { // Update: map.set qux ``` + + diff --git a/src/pages/docs/liveobjects/concepts/instance.mdx b/src/pages/docs/liveobjects/concepts/instance.mdx index dac86a6610..3da3a093c8 100644 --- a/src/pages/docs/liveobjects/concepts/instance.mdx +++ b/src/pages/docs/liveobjects/concepts/instance.mdx @@ -27,11 +27,17 @@ meta_description: "Learn about Instance, a reference to a specific LiveObject in An `Instance` represents a specific object instance within the channel object. When you call methods on an `Instance`, they operate on that specific instance regardless of where it exists in the structure or whether it has been moved. + + + + +In the Java SDK the base `Instance` type is type-agnostic; capabilities (`getId()`, `get()`, `value()`, `set()`, `subscribe()`) live on the typed instances — `LiveMapInstance`, `LiveCounterInstance` and the primitive instances — reached via throwing `as*` casts. + ## Get an Instance @@ -44,12 +50,23 @@ const myObject = await channel.object.get(); // Get the specific Instance of a LiveCounter located at the 'visits' key const visits = myObject.get('visits').instance(); -console.log(counterInstance?.id); // e.g. counter:abc123@1234567890 -console.log(counterInstance?.value()); // e.g. 5 +console.log(visits?.id); // e.g. counter:abc123@1234567890 +console.log(visits?.value()); // e.g. 5 +``` + +```java +LiveMapPathObject myObject = channel.object.get().join(); + +Instance instance = myObject.get("visits").instance(); // null if nothing at the path +if (instance != null) { + LiveCounterInstance visits = instance.asLiveCounter(); // throws IllegalStateException on mismatch + System.out.println(visits.getId()); // e.g. "counter:abc123@1234567890" + System.out.println(visits.value()); // e.g. 5.0 +} ``` -The `instance()` method returns `undefined` if no object exists at that path. +The `instance()` method returns `undefined` if no object exists at that path.The `instance()` method returns `null` if no object exists at that path. Once you have an `Instance`, it references a specific object by its ID. This means operations target that exact object: @@ -61,10 +78,21 @@ const visits = myObject.get('visits').instance(); // Increment the specific LiveCounter instance await visits?.increment(5); ``` + +```java +// Obtain an Instance for a LiveCounter +Instance visits = myObject.get("visits").instance(); + +// Increment the specific LiveCounter instance +if (visits != null) { + visits.asLiveCounter().increment(5).join(); +} +``` An `Instance` references a specific object by its ID rather than by location. + ## Type inference When using TypeScript, you can provide type parameters to `instance()` to get rich type inference: @@ -101,14 +129,39 @@ if (settings) { See the [Typing documentation](/docs/liveobjects/typing) for more details on type safety. + + + +## Typed views + +An `Instance` wraps an already-resolved value of known type, so its `as*` casts **throw** an `IllegalStateException` on mismatch — the opposite contract to the never-throwing `PathObject` casts. Check `getType()` first when the type isn't known; `getType()` on an `Instance` is non-null. + + +```java +Instance instance = myObject.get("visits").instance(); +if (instance != null && instance.getType() == ValueType.LIVE_COUNTER) { + instance.asLiveCounter().increment(1).join(); +} +``` + + +See the [Typing documentation](/docs/liveobjects/typing) for more details on type safety. + ## Navigate an Instance For `LiveMap` instances, use the `get(key)` method to navigate to a child value: + + + + + ```javascript @@ -124,9 +177,27 @@ console.log(settings?.id); // e.g. "map:abc123@1234567890" const color = settings?.get('theme')?.get('color'); console.log(color?.value()); // e.g. blue ``` + +```java +Instance instance = myObject.get("settings").instance(); +if (instance != null) { + LiveMapInstance settings = instance.asLiveMap(); + System.out.println(settings.getId()); // e.g. "map:abc123@1234567890" + + // Chain get() calls for deeper navigation + // get() may return null at any point if the accessed entry does not exist + Instance theme = settings.get("theme"); + if (theme != null && theme.getType() == ValueType.LIVE_MAP) { + Instance color = theme.asLiveMap().get("color"); + if (color != null) { + System.out.println(color.asString().value()); // e.g. "blue" + } + } +} +``` -The `id` getter returns the [object ID](/docs/liveobjects/concepts/objects#object-ids) of the instance, which can be used with the [REST API](/docs/liveobjects/rest-api-usage). +The `id` getter returns the [object ID](/docs/liveobjects/concepts/objects#object-ids) of the instance, which can be used with the [REST API](/docs/liveobjects/rest-api-usage).The `getId()` method returns the [object ID](/docs/liveobjects/concepts/objects#object-ids) of the instance, which can be used with the [REST API](/docs/liveobjects/rest-api-usage). ## Access data @@ -149,9 +220,29 @@ console.log('Username:', username?.value()); // e.g. "alice" const visits = user?.get('visits'); console.log(visits?.value()); // e.g. 5 ``` + +```java +// Get an instance +Instance userInstance = myObject.get("user").instance(); +if (userInstance != null) { + LiveMapInstance user = userInstance.asLiveMap(); + + // Get the value of an entry containing a primitive + Instance username = user.get("username"); + if (username != null) { + System.out.println("Username: " + username.asString().value()); // e.g. "alice" + } + + // Get the value of the LiveCounter stored in 'visits' + Instance visits = user.get("visits"); + if (visits != null) { + System.out.println(visits.asLiveCounter().value()); // e.g. 5.0 + } +} +``` -If the entry doesn't exist, `get()` returns undefined: +If the entry doesn't exist, `get()` returns undefined:If the entry doesn't exist, `get()` returns `null`: ```javascript @@ -161,8 +252,17 @@ if (user) { console.log(user.get('nonexistent')); // undefined } ``` + +```java +Instance user = myObject.get("user").instance(); +if (user != null) { + // Get an entry that doesn't exist on the instance + System.out.println(user.asLiveMap().get("nonexistent")); // null +} +``` + If the entry exists, but does not contain a primitive or a `LiveCounter` instance, `value()` returns `undefined`: @@ -177,6 +277,24 @@ const settings = myObject.get('settings').instance(); console.log(settings?.value()); // undefined - it's a LiveMap, not a primitive or LiveCounter ``` + + + +Where the JavaScript `Instance` degrades gracefully (an `undefined` value, empty iterators, an `undefined` size), Java prevents the call at compile time or fails fast in the cast — check `getType()` when the type isn't known: + + +```java +Instance settings = myObject.get("settings").instance(); // a LiveMap +if (settings != null) { + System.out.println(settings.getType()); // LIVE_MAP + + // Maps have no value() and counters have no entries()/size() - those methods + // simply don't exist on the wrong type. Casting to the wrong view throws: + settings.asLiveCounter(); // throws IllegalStateException +} +``` + + ### Enumerate collections @@ -204,8 +322,28 @@ if (settings) { } } ``` + +```java +Instance settingsInstance = myObject.get("settings").instance(); +if (settingsInstance != null) { + LiveMapInstance settings = settingsInstance.asLiveMap(); + + // Iterate over key-value pairs. + // Each key is a string, and the value is an Instance for the entry. + for (Map.Entry entry : settings.entries()) { + System.out.println(entry.getKey() + ": " + entry.getValue().compactJson()); + } + + // Iterate over keys only + for (String key : settings.keys()) { System.out.println("Key: " + key); } + + // Iterate over values + for (Instance value : settings.values()) { System.out.println("Value: " + value.compactJson()); } +} +``` + Collection methods return empty iterators if the instance is not a `LiveMap`: @@ -219,6 +357,7 @@ if (visits) { } ``` + ### Get the size of a collection @@ -231,8 +370,17 @@ const settings = myObject.get('settings').instance(); // Get the number of entries console.log(settings?.size()); ``` + +```java +Instance settings = myObject.get("settings").instance(); +if (settings != null) { + // Get the number of entries - non-null Long on an Instance + System.out.println(settings.asLiveMap().size()); +} +``` + The `size()` method returns `undefined` if the instance is not a `LiveMap`: @@ -241,7 +389,15 @@ const visits = myObject.get('visits').instance(); // This is a LiveCounter, not console.log(visits?.size()); // undefined ``` + + +### Get a compact object + +The Java SDK exposes `compactJson()` as the supported snapshot of an instance — there is no `compact()` equivalent. On an `Instance` the result is non-null; cyclic references are broken via `{"objectId": …}` markers and binary data is base64-encoded. + + + ### Get a compact object The `compact()` method returns a JavaScript object representation of the instance: @@ -293,8 +449,9 @@ if (userInstance) { It is possible for the value returned from `compact()` to contain cyclic references, so it is not safe to serialize this value with `JSON.stringify()`. + -Use the `compactJson()` method to obtain a value that can be safely passed to `JSON.stringify()`: +Use the `compactJson()` method to obtain a value that can be safely passed to `JSON.stringify()`:Use the `compactJson()` method to obtain a JSON representation of the instance: ```javascript @@ -318,6 +475,14 @@ if (userInstance) { // } } ``` + +```java +Instance userInstance = myObject.get("user").instance(); +if (userInstance != null) { + System.out.println(userInstance.compactJson()); // non-null on an Instance + // {"profile":{"objectId":"map:abc123@1234567890"},"name":"Alice"} +} +``` Binary data in the channel object are serialized as base64-encoded strings by `compactJson()`: @@ -336,6 +501,19 @@ if (mapInstance) { console.log(compactJson.hello); // "d29ybGQ=" (base64 encoded) } ``` + +```java +// The root LiveMap always exists, so instance() is non-null here +LiveMapInstance root = myObject.instance().asLiveMap(); + +// Store binary data in the LiveMap +root.set("hello", LiveMapValue.of("world".getBytes(StandardCharsets.UTF_8))).join(); + +// compactJson() converts binary data to base64 strings; +// LiveMapInstance narrows the return type to JsonObject +JsonObject json = root.compactJson(); +System.out.println(json.get("hello").getAsString()); // "d29ybGQ=" +``` ## Update data @@ -359,12 +537,38 @@ const visits = myObject.get('visits').instance(); await visits?.increment(5); await visits?.decrement(2); ``` + +```java +// Update a LiveMap instance +Instance settingsInstance = myObject.get("settings").instance(); +if (settingsInstance != null) { + LiveMapInstance settings = settingsInstance.asLiveMap(); + settings.set("theme", LiveMapValue.of("dark")).join(); + settings.remove("oldSetting").join(); +} + +// Update a LiveCounter instance +Instance visitsInstance = myObject.get("visits").instance(); +if (visitsInstance != null) { + LiveCounterInstance visits = visitsInstance.asLiveCounter(); + visits.increment(5).join(); + visits.decrement(2).join(); +} +``` + + + + + + ### Batch multiple updates The `batch(callback)` method groups multiple mutations into a single message. The `ctx` parameter is the batch context for the specific instance: @@ -381,6 +585,7 @@ await settings?.batch((ctx) => { See the [Batch operations documentation](/docs/liveobjects/batch) for more details on batching. + ## Subscribe to changes @@ -399,8 +604,20 @@ if (visits) { unsubscribe(); } ``` + +```java +Instance visitsInstance = myObject.get("visits").instance(); +if (visitsInstance != null) { + Subscription subscription = visitsInstance.asLiveCounter().subscribe(event -> + System.out.println("Visits updated")); + + // Later, stop listening to changes + subscription.unsubscribe(); +} +``` + Alternatively, use the `subscribeIterator()` method for an async iterator syntax: @@ -418,10 +635,18 @@ if (visits) { } ``` + + + + + + `Instance` subscriptions observe a specific object instance rather than a location. If the instance is moved to a different location within the channel object, the subscription continues to observe the same instance. @@ -444,14 +669,37 @@ if (visits) { await myObject.remove('visits'); } ``` + +```java +Instance visitsInstance = myObject.get("visits").instance(); +if (visitsInstance != null) { + visitsInstance.asLiveCounter().subscribe(event -> { + ObjectMessage message = event.getMessage(); + if (message != null && message.getOperation().getAction() == ObjectOperationAction.OBJECT_DELETE) { + System.out.println("visits counter was deleted"); + // This listener is automatically unsubscribed after this event + } + }); + + // Make the LiveCounter stored in visits unreachable. + // It will eventually be deleted. + myObject.remove("visits").join(); +} +``` ### Determine what changed The subscription receives an argument with information about the update: + - The `object` field contains an `Instance` representing the same object instance you called `subscribe()` on - The `message` field contains the [`ObjectMessage`](/docs/liveobjects/concepts/operations#properties) which details the operation that caused the change, including information about the client that performed the operation and the specific changes made. + + +- `getObject()` returns an `Instance` representing the same object instance you called `subscribe()` on — it is the base `Instance` type, so cast it with the matching `as*` view +- `getMessage()` returns the [`ObjectMessage`](/docs/liveobjects/concepts/operations#properties) which details the operation that caused the change, including information about the client that performed the operation and the specific changes made. It is nullable. + ```javascript @@ -466,6 +714,23 @@ if (visits) { }); } ``` + +```java +Instance visitsInstance = myObject.get("visits").instance(); +if (visitsInstance != null) { + LiveCounterInstance visits = visitsInstance.asLiveCounter(); + visits.subscribe(event -> { + LiveCounterInstance object = event.getObject().asLiveCounter(); + System.out.println("New value: " + object.value()); + ObjectMessage message = event.getMessage(); + if (message != null) { + System.out.println("Updated by: " + message.getClientId()); + System.out.println("Operation: " + message.getOperation().getAction()); // e.g. COUNTER_INC + } + System.out.println(object.getId().equals(visits.getId())); // true - same instance + }); +} +``` ### Updates to nested objects @@ -489,6 +754,25 @@ if (settings) { await settings.get('preferences')?.set('language', 'en'); } ``` + +```java +Instance instance = myObject.get("settings").instance(); +if (instance != null) { + LiveMapInstance settings = instance.asLiveMap(); + + // Subscribe to the settings LiveMap instance + settings.subscribe(event -> System.out.println("Settings LiveMap updated")); + + // This triggers the subscription (direct change to settings) + settings.set("theme", LiveMapValue.of("dark")).join(); + + // This does NOT trigger the subscription (change to nested object) + Instance preferences = settings.get("preferences"); + if (preferences != null) { + preferences.asLiveMap().set("language", LiveMapValue.of("en")).join(); + } +} +``` - + + + + diff --git a/src/pages/docs/liveobjects/counter.mdx b/src/pages/docs/liveobjects/counter.mdx index aaa15da41f..898c412071 100644 --- a/src/pages/docs/liveobjects/counter.mdx +++ b/src/pages/docs/liveobjects/counter.mdx @@ -25,7 +25,7 @@ meta_description: "Create, update and receive updates for a numerical counter th `LiveCounter` is a synchronized numerical counter that supports increment and decrement operations. It ensures that all updates are correctly applied and synchronized across clients in realtime, preventing inconsistencies when multiple clients modify the counter value simultaneously. - + You interact with `LiveCounter` through a [PathObject](/docs/liveobjects/concepts/path-object) or by obtaining a specific [Instance](/docs/liveobjects/concepts/instance). @@ -46,6 +46,19 @@ await myObject.set('score', LiveCounter.create(100)); // Create a counter without specifying initial value (defaults to 0) await myObject.set('clicks', LiveCounter.create()); ``` + +```java +// myObject obtained from channel.object.get() + +// Create a counter with initial value 0 +myObject.set("visits", LiveMapValue.of(LiveCounter.create(0))).join(); + +// Create a counter with initial value 100 +myObject.set("score", LiveMapValue.of(LiveCounter.create(100))).join(); + +// Create a counter without specifying initial value (defaults to 0) +myObject.set("clicks", LiveMapValue.of(LiveCounter.create())).join(); +``` `LiveCounter.create()` returns a value type that describes the initial value for a new counter. The actual object is created when assigned to a path. Each assignment creates a distinct object with its own unique ID: @@ -63,11 +76,27 @@ const id1 = myObject.get('counter1').instance()?.id(); const id2 = myObject.get('counter2').instance()?.id(); console.log(id1 === id2); // false ``` + +```java +LiveCounter counterValue = LiveCounter.create(0); + +// Each assignment creates a different object +myObject.set("counter1", LiveMapValue.of(counterValue)).join(); +myObject.set("counter2", LiveMapValue.of(counterValue)).join(); + +// counter1 and counter2 are different objects with different IDs +Instance counter1 = myObject.get("counter1").instance(); +Instance counter2 = myObject.get("counter2").instance(); +if (counter1 != null && counter2 != null) { + System.out.println(counter1.asLiveCounter().getId() + .equals(counter2.asLiveCounter().getId())); // false +} +``` ## Get counter value -Access a `LiveCounter` through a [PathObject](/docs/liveobjects/concepts/path-object) for path-based operations, or obtain a specific [Instance](/docs/liveobjects/concepts/instance) to work with the underlying object directly. Use the `value()` method to get the value of the `LiveCounter`: +Access a `LiveCounter` through a [PathObject](/docs/liveobjects/concepts/path-object) for path-based operations, or obtain a specific [Instance](/docs/liveobjects/concepts/instance) to work with the underlying object directly. Use the `value()` method to get the value of the `LiveCounter`:Use the `asLiveCounter()` typed view's `value()` method to get the value of the `LiveCounter`: ```javascript @@ -79,17 +108,38 @@ console.log(visits.value()); // e.g. 5 // Instance access: reference to a specific counter object const visitsInstance = myObject.get('visits').instance(); -console.log(visitsInstance?.value()); e.g. 5 +console.log(visitsInstance?.value()); // e.g. 5 +``` + +```java +LiveMapPathObject myObject = channel.object.get().join(); + +// PathObject access - Double, or null when the path doesn't resolve to a counter +System.out.println(myObject.get("visits").asLiveCounter().value()); // e.g. 5.0 + +// Instance access - non-null Double (the throwing cast catches mismatches earlier) +Instance visitsInstance = myObject.get("visits").instance(); +if (visitsInstance != null) { + System.out.println(visitsInstance.asLiveCounter().value()); // e.g. 5.0 +} ``` + + + + + ## Get compact object + Get a numeric representation of the counter using the `compact()` or `compactJson()` methods. These methods return the same result as `value()`: @@ -114,9 +164,42 @@ console.log(stats.compact()); // e.g. { visits: 5 } // Get the Instance of a LiveMap stored in 'stats' const visitsInstance = myObject.get('stats').instance(); -console.log(visitsInstance?.compact()); // e.g. 5 +console.log(visitsInstance?.compact()); // e.g. { visits: 5 } ``` + + + +Get a numeric representation of the counter using the `compactJson()` method (the Java SDK has no `compact()` equivalent). It returns the same result as `value()`: + + +```java +// Get a PathObject to a LiveCounter stored in 'visits' +System.out.println(myObject.get("visits").compactJson()); // e.g. 5.0 (JsonPrimitive) + +// Get the Instance of a LiveCounter stored in 'visits' +Instance visitsInstance = myObject.get("visits").instance(); +if (visitsInstance != null) { + System.out.println(visitsInstance.asLiveCounter().compactJson()); // non-null JsonPrimitive, e.g. 5.0 +} +``` + + +When calling `compactJson()` on a `LiveMap`, nested `LiveCounter` objects are included as a number: + + +```java +// Get a PathObject to a LiveMap stored in 'stats' +System.out.println(myObject.get("stats").compactJson()); // e.g. {"visits":5.0} + +// Get the Instance of a LiveMap stored in 'stats' +Instance statsInstance = myObject.get("stats").instance(); +if (statsInstance != null) { + System.out.println(statsInstance.asLiveMap().compactJson()); // non-null JsonObject, e.g. {"visits":5.0} +} +``` + +