[AIT-1019] feat(liveobjects): implement remaining path-based API — parent references, event bubbling etc#1224
Conversation
…ubscriptions and event bubbling Completes the path-addressed LiveObjects API on top of the sync engine: - Bridge ResolvedValue (MapRef/CounterRef/Leaf) to the internal graph objects so path resolution returns live references - Replace the untyped ObjectUpdate with a sealed model (MapUpdate/CounterUpdate/NoOp) carrying the source objectMessage and tombstone flag (RTLO4b4, RTLM18, RTLC11) - Track parent references on every graph object and derive getFullPaths() for event bubbling (RTLO3f, RTLO4f-RTLO4h) - Implement RTPO3 path resolution and the read APIs: RealtimeObject#get (RTO23), PathObject value/exists/instance/compactJson (RTPO8, RTPO13/RTPO14) - Add creation value-type evaluation and the path write APIs - Add value-bound typed Instances and PathObject#instance (RTINS5, RTTS6/RTTS7) - Add path/instance subscriptions with depth windows and event bubbling via PathObjectSubscriptionRegister (RTO24, RTPO19) - RTO4c fix: always start a new sync sequence on ATTACHED regardless of the HAS_OBJECTS flag, matching current ably-js - Make stored path strings round-trip-safe by escaping backslashes as well as dots in PathSegments#join - Remove dead createMap/createCounter code paths and re-enable the UTS liveobjects test suites
WalkthroughThis PR migrates the LiveObjects module to a path-based object model. It implements value resolution for typed instances and path objects, adds parent-reference tracking, threads WireObjectMessage through update pipelines, introduces a path subscription registry and PathSegments escaping utility, migrates example apps to the new API, and updates UTS test infrastructure and proxy documentation references. ChangesLiveObjects Core Migration
Estimated code review effort: 5 (Critical) | ~120 minutes Example App Migration to Path Objects
Estimated code review effort: 3 (Moderate) | ~25 minutes UTS Test Infra and Proxy Doc Updates
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
PathObject implementation from new API i.e. bubbling events and parentReferenesPath based API i.e. parentReferenes and bubbling events
Path based API i.e. parentReferenes and bubbling eventsThe example app targeted the removed io.ably.lib.objects callback API (getRootAsync, createCounterAsync, LiveMapValue.asLiveCounter). Rewrite its bridge layer (Utils.kt) against the public path-based interfaces in io.ably.lib.liveobjects, entered via channel.object.get() which returns the root LiveMapPathObject once objects are SYNCED. PathObjects reference a location and re-resolve on every call, which simplifies the app considerably: - get-or-create is a single atomic publish: root.set(key, LiveMapValue.of(LiveCounter.create())) replaces the old free-floating create + link two-step - the root-map subscription that watched for "Reset all" replacing a counter (and rebound to the new instance) is gone entirely - a path subscription follows the replacement automatically - mutations call the CompletableFuture API directly, fire-and-forget: the path subscription refreshes Compose state on ack, so the *Coroutine wrapper extensions and their scope.launch call sites are removed UI, test tags, channel setup and the instrumentation tests are unchanged. Verified with :examples:assembleDebug, :examples:compileDebugAndroidTestKotlin and manually on an emulator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review fixes for #1225: - getOrCreateCounter/getOrCreateMap now check the resolved type (getType() != LIVE_COUNTER/LIVE_MAP) instead of exists(), so a wrong-typed value at the key is replaced rather than returned as a view whose reads yield null and writes fail; the LWW resolution of the inherent check-then-set race is documented on both helpers - observeCounter/observeMap effects keyed on (root, key) so a changed key rebinds - Red vote card enable state now tracks redCounter (was greenCounter, a pre-existing copy/paste bug) - Add Task is disabled until the tasks map is bound, so input is never cleared without the task being enqueued - new ObjectsSyncStatusRow on both screens surfaces objects sync progress ("Objects syncing..." -> "Objects synced") via the channel.object.on(SYNCING/SYNCED) state API, with the initial state derived from root readiness since state events fire on transitions only Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rrected spec Proxy session creation failed with HTTP 400 for any rule using a frame-action match: the uts-proxy's MatchConfig.match.action is a Go string (action name or numeric string), but the rule builders serialised messageAction as a JSON number. wsFrameToClientRule/wsFrameToServerRule now send it as a string. Add withRealTimeout to the shared infra: inside runTest, a bare withTimeout measures virtual (kotlinx.coroutines.test) time, which fast-forwards while the test idles - a timeout wrapping a real network await fires instantly. All real-network waits in integration tests must run on a real-thread dispatcher, matching the existing awaitState/awaitChannelState/pollUntil helpers. Rework ObjectsFaultsTest against the corrected source spec (objects/integration/proxy/objects_faults.md, ably/specification#501): - bound every object.get() with withRealTimeout instead of virtual-time withTimeout (all five tests previously failed instantly) - RTO7/RTO8: fetch the channel handle once and reuse it - re-calling Channels.get() with mode options on an attached channel throws - RTO20e: exercise the RTO20e1 sequence (mutation in flight while SYNCING, then the channel enters FAILED -> 92008/400 with the injected 90000 channel error as cause), mirroring ably-js's sync-wait rejection test; the spec's original mutate-after-FAILED steps hit the RTO26b precondition (90001) instead and were fixed at source - implement the spec's Common Cleanup: close clients and await CLOSED (with the connection-state guard) before tearing down the proxy session Update proxy spec path references (ObjectsFaultsTest/AuthReauthTest KDocs, uts-to-kotlin skill) to uts/docs/proxy.md per the relocation in ably/specification#501; uts/README.md's GitHub URLs are left until that PR reaches main. See PROXY_UTS_SPEC_FIX.md for the full evidence chain. All five ObjectsFaultsTest tests pass against the sandbox through uts-proxy.
[AIT-1038] chore(examples): migrate LiveObjects example app to the path-based API
…align ensureAttached to RTL33 Rebuild PathObjectSubscriptionRegister on ably-java's EventEmitter — the SDK's thread-safe registration/dispatch primitive already used by the LiveMap/LiveCounter change coordinators — replacing the hand-rolled ConcurrentHashMap + AtomicLong id counter. PathEvent carries the broadcast payload, PathSubscription the per-subscription state, and PathEventEmitter.apply resolves each listener's own covered path. Dispatch behaviour (coverage, first-covered candidate, per-listener error isolation) is unchanged. Update ensureAttached to the ensure-active-channel procedure (RTL33, specification PR #472), matching the ably-js source: - ATTACHED/SUSPENDED return without attaching (RTL33a) - INITIALIZED/DETACHED/DETACHING/ATTACHING trigger an implicit attach and wait; the attach failure's ErrorInfo is propagated unchanged (RTL33b/b1) - FAILED (and any other state) throws code 90001 / statusCode 400 via objectException (RTL33c) Refresh HelpersTest to cover the full RTL33 state matrix.
…LiveObject Move the ObjectUpdate sealed hierarchy (NoOp/MapUpdate/CounterUpdate) and its noOp extension into a dedicated value/ObjectUpdate.kt, leaving BaseRealtimeLiveObject focused on the base class. ObjectType stays with the base class as the broad object discriminator used across the package. Relocate the LiveMap-specific MapChange enum from the generic value package into value.livemap alongside the other map types; it is a per-key diff type owned by LiveMap, and value already depends on value.livemap (BaseRealtimeLiveObject uses InternalLiveMap), so this introduces no new package coupling. The sealed subclasses remain in the value package, so exhaustive when-handling (e.g. tombstone) still compiles. Pure code move, no behaviour change. Also drop a stale @Suppress(RedundantNullableReturnType) on DefaultLiveMapInstance.get, whose nullable return is genuine.
…s type's file The toCompactJsonElement() extension only reads WireObjectData's own fields and is used across packages (path, livemap), so it belongs with the type it extends rather than in the livemap feature file. Move it into message/WireObjectMessage.kt alongside the other WireObjectData extensions (size, isInvalid); retarget the DefaultPathObject import and drop the now-unused gson imports from InternalLiveMap. Pure move, no behaviour change; message still has no dependency on value/path/livemap.
…lution resolveValueAtPath always received the PathObject's own path property at every call site, so the parameter was redundant. Make it a no-arg method that resolves the instance's stored path directly, and rename it to resolveValueAtCurrentPath to reflect that it only ever resolves this node's current path. This also removes a latent footgun: DefaultLiveMapPathObject.at(path) shadows the path property, so a future resolveValueAtPath(path) call inside it would have resolved the argument instead of the node's path. The no-arg form makes that mistake impossible. Pure refactor, no behaviour change.
…er overloads The parameterless increment()/decrement() on both DefaultLiveCounterPathObject and DefaultLiveCounterInstance duplicated the full body of their Number counterparts with a hardcoded 1. Make them delegate to increment(1)/decrement(1) instead. All spec checks live in the parameterized overload, and the default-of-1 spec points (RTPO17a1/RTPO18a1, RTINS14a1/RTINS15a1) are preserved on the delegating methods. Pure refactor, no behaviour change.
…opagation
Rename the DefaultRealtimeObject async bridges for clarity: asyncApi -> asyncFuture
(generic, value-returning) and asyncVoidApi -> asyncVoidFuture (the Unit->Void adapter
required by the Java write APIs), updating all call sites. Behaviour is unchanged; the
thenApply { null } bridge stays because kotlinx's future builder cannot produce a
CompletableFuture<Void> (its type param is non-null, Void's only value is null).
Add DefaultRealtimeObjectAsyncTest asserting result and exception propagation for both
helpers, and - crucially - that a failed operation does not cancel the shared sequential
scope for subsequent operations, locking in the SupervisorJob failure-isolation guarantee.
getOrPut on a ConcurrentHashMap may invoke its factory on a losing thread during a concurrent first-time getInstance, constructing a duplicate DefaultRealtimeObject whose init-started coroutine would then leak. computeIfAbsent runs the factory at most once per channel, so exactly one instance is created. It needs Java 8 / Android API 24, already required by this module's CompletableFuture-based public API.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@liveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/types/DefaultJsonObjectInstance.kt`:
- Around line 14-31: `DefaultJsonObjectInstance.value()` is exposing the
internal mutable Gson `JsonObject` directly, which can let callers mutate the
instance state; update `DefaultJsonObjectInstance` to return a defensive copy
(for example via `deepCopy()`) the same way this is handled in
`DefaultJsonArrayInstance`, unless JSON values are intentionally exempted by
spec. Keep `compactJson()` and the access checks in
`throwIfInvalidAccessApiConfiguration()` unchanged, and make the fix only in the
`value()` path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 09797bbc-a124-42d3-a2da-ffbf16798f66
📒 Files selected for processing (53)
.claude/skills/uts-to-kotlin/SKILL.mdexamples/src/main/kotlin/com/ably/example/Utils.ktexamples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.ktexamples/src/main/kotlin/com/ably/example/screen/ObjectsSyncStatus.ktexamples/src/main/kotlin/com/ably/example/screen/TaskManagementScreen.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/DefaultLiveObjectsPlugin.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/DefaultRealtimeObject.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/Helpers.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/ObjectsManager.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/ObjectsPool.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/Utils.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/DefaultInstance.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/DefaultInstanceSubscriptionEvent.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/types/DefaultBinaryInstance.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/types/DefaultBooleanInstance.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/types/DefaultJsonArrayInstance.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/types/DefaultJsonObjectInstance.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/types/DefaultLiveCounterInstance.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/types/DefaultLiveMapInstance.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/types/DefaultNumberInstance.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/types/DefaultStringInstance.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/message/WireObjectMessage.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/DefaultPathObject.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/PathObjectSubscriptionRegister.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/PathSegments.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/types/DefaultBinaryPathObject.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/types/DefaultBooleanPathObject.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/types/DefaultJsonArrayPathObject.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/types/DefaultJsonObjectPathObject.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/types/DefaultLiveCounterPathObject.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/types/DefaultLiveMapPathObject.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/types/DefaultNumberPathObject.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/types/DefaultStringPathObject.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/BaseRealtimeLiveObject.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/ObjectUpdate.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/ResolvedValue.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livecounter/DefaultLiveCounter.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livecounter/InternalLiveCounter.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livecounter/LiveCounterChangeCoordinator.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livecounter/LiveCounterManager.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livemap/DefaultLiveMap.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livemap/InternalLiveMap.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livemap/LiveMapChangeCoordinator.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livemap/LiveMapEntry.ktliveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livemap/LiveMapManager.ktliveobjects/src/test/kotlin/io/ably/lib/liveobjects/unit/DefaultRealtimeObjectAsyncTest.ktliveobjects/src/test/kotlin/io/ably/lib/liveobjects/unit/HelpersTest.ktliveobjects/src/test/kotlin/io/ably/lib/liveobjects/unit/PathSegmentsTest.ktuts/build.gradle.ktsuts/src/test/kotlin/io/ably/lib/uts/infra/Utils.ktuts/src/test/kotlin/io/ably/lib/uts/infra/integration/proxy/ProxySession.ktuts/src/test/kotlin/io/ably/lib/uts/integration/proxy/liveobjects/ObjectsFaultsTest.ktuts/src/test/kotlin/io/ably/lib/uts/integration/proxy/realtime/AuthReauthTest.kt
💤 Files with no reviewable changes (2)
- liveobjects/src/main/kotlin/io/ably/lib/liveobjects/Utils.kt
- uts/build.gradle.kts
There was a problem hiding this comment.
Pull request overview
Completes the Kotlin liveobjects module’s public, path-based LiveObjects API by wiring the existing sync/CRDT engine up through path resolution, typed reads/writes, instance binding, parent references, and path subscription event bubbling; also updates UTS proxy tests and example apps to exercise the new behavior.
Changes:
- Implemented path resolution (
PathSegments), typedPathObjectoperations, compact JSON snapshots, and value-type evaluation for nested object creation. - Added typed update model (
ObjectUpdate), parent-reference tracking/rebuild, and event dispatch (instance listeners + bubbling to path subscriptions). - Re-enabled/extended UTS LiveObjects suites, improved proxy integration test timeout behavior, and migrated example apps to the new API (including sync-status UI).
Reviewed changes
Copilot reviewed 53 out of 53 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| uts/src/test/kotlin/io/ably/lib/uts/integration/proxy/realtime/AuthReauthTest.kt | Updates proxy KDoc to point at the new proxy docs path. |
| uts/src/test/kotlin/io/ably/lib/uts/integration/proxy/liveobjects/ObjectsFaultsTest.kt | Improves proxy LiveObjects fault tests (real-time timeouts, cleanup, new FAILED-during-sync-wait scenario). |
| uts/src/test/kotlin/io/ably/lib/uts/infra/Utils.kt | Adds withRealTimeout helper for runTest + real network waits. |
| uts/src/test/kotlin/io/ably/lib/uts/infra/integration/proxy/ProxySession.kt | Ensures proxy match config action is encoded as a string. |
| uts/build.gradle.kts | Re-enables LiveObjects UTS tests by removing exclusions. |
| liveobjects/src/test/kotlin/io/ably/lib/liveobjects/unit/PathSegmentsTest.kt | Adds unit tests for stored vs user-supplied path parsing/joining (incl. backslash cases). |
| liveobjects/src/test/kotlin/io/ably/lib/liveobjects/unit/HelpersTest.kt | Updates/expands ensureAttached tests for additional channel states and error propagation. |
| liveobjects/src/test/kotlin/io/ably/lib/liveobjects/unit/DefaultRealtimeObjectAsyncTest.kt | Adds tests for sequential-scope future bridging and exception propagation behavior. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/ResolvedValue.kt | Switches ResolvedValue to reference internal CRDT objects (map/counter) or leaf wire data. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/ObjectUpdate.kt | Introduces a typed update model carrying source message + tombstone signal. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livemap/LiveMapManager.kt | Threads source messages through apply paths; maintains parent refs on mutation; produces typed diffs. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livemap/LiveMapEntry.kt | Resolves entries to internal refs or wire leaves; filters invalid/tombstoned refs. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livemap/LiveMapChangeCoordinator.kt | Adds MapChange enum and proper NoOp update marker; adds listener teardown helper. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livemap/InternalLiveMap.kt | Implements internal map read APIs, compact JSON snapshots, write evaluation (nested creates), and instance subscription dispatch. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livemap/DefaultLiveMap.kt | Implements value-type evaluation to [nested creates..., MAP_CREATE] plus primitive-to-wire conversion helper. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livecounter/LiveCounterManager.kt | Threads source messages; produces typed counter updates and no-op behavior for diffs. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livecounter/LiveCounterChangeCoordinator.kt | Switches no-op representation to ObjectUpdate.NoOp and adds listener teardown helper. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livecounter/InternalLiveCounter.kt | Implements decrement path, subscription dispatch, and ties into new update model. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/livecounter/DefaultLiveCounter.kt | Implements value-type evaluation to COUNTER_CREATE with validation and objectId derivation. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/value/BaseRealtimeLiveObject.kt | Adds parent reference tracking, getFullPaths, tombstone semantics, and update dispatch (instance + bubbling path events). |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/Utils.kt | Removes invalidInputError from this file (now provided elsewhere). |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/types/DefaultStringPathObject.kt | Implements typed leaf read for strings. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/types/DefaultNumberPathObject.kt | Implements typed leaf read for numbers. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/types/DefaultLiveMapPathObject.kt | Implements map navigation and read/write operations via fresh path resolution. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/types/DefaultLiveCounterPathObject.kt | Implements counter read/write operations via fresh path resolution. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/types/DefaultJsonObjectPathObject.kt | Implements typed leaf read for JSON objects. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/types/DefaultJsonArrayPathObject.kt | Implements typed leaf read for JSON arrays. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/types/DefaultBooleanPathObject.kt | Implements typed leaf read for booleans. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/types/DefaultBinaryPathObject.kt | Implements typed leaf read for binary (base64 decode). |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/PathSegments.kt | Adds stored-path vs user-path parsing/joining utilities with escape handling. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/PathObjectSubscriptionRegister.kt | Adds path subscription registry and coverage-based bubbling dispatch. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/path/DefaultPathObject.kt | Implements core path resolution, compact JSON, existence, and subscription registration. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/ObjectsPool.kt | Exposes all() to support parent-reference rebuild. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/ObjectsManager.kt | Rebuilds parent references after sync before dispatching updates. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/message/WireObjectMessage.kt | Adds WireObjectData.toCompactJsonElement() for compact snapshots. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/types/DefaultStringInstance.kt | Implements value-bound primitive instance behavior. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/types/DefaultNumberInstance.kt | Implements value-bound primitive instance behavior. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/types/DefaultLiveMapInstance.kt | Implements identity-bound map instance operations and subscriptions. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/types/DefaultLiveCounterInstance.kt | Implements identity-bound counter instance operations and subscriptions. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/types/DefaultJsonObjectInstance.kt | Implements value-bound JSON object instance behavior. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/types/DefaultJsonArrayInstance.kt | Implements value-bound JSON array instance behavior. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/types/DefaultBooleanInstance.kt | Implements value-bound boolean instance behavior. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/types/DefaultBinaryInstance.kt | Implements value-bound binary instance behavior with defensive copying. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/DefaultInstanceSubscriptionEvent.kt | Makes subscription event compatible with both map/counter change emitters. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/instance/DefaultInstance.kt | Adds ResolvedValue.toInstance() to construct typed instances (including primitives). |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/Helpers.kt | Simplifies/updates ensureAttached logic across channel states and error semantics. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/DefaultRealtimeObject.kt | Implements get() via sequential scope futures; adds path subscription registry; adjusts attach/sync transitions. |
| liveobjects/src/main/kotlin/io/ably/lib/liveobjects/DefaultLiveObjectsPlugin.kt | Uses computeIfAbsent for channel object caching. |
| examples/src/main/kotlin/com/ably/example/Utils.kt | Migrates example bridge to new PathObject API and adds sync-state observation helpers. |
| examples/src/main/kotlin/com/ably/example/screen/TaskManagementScreen.kt | Migrates task management UI to new LiveObjects API; adds sync status row. |
| examples/src/main/kotlin/com/ably/example/screen/ObjectsSyncStatus.kt | Adds UI component for “syncing/synced” indication. |
| examples/src/main/kotlin/com/ably/example/screen/ColorVotingScreen.kt | Migrates color voting UI to new LiveObjects API; adds sync status row. |
| .claude/skills/uts-to-kotlin/SKILL.md | Updates proxy doc path references in the skill documentation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
Completes the path-based LiveObjects public API in the
liveobjectsKotlin module. The sync/CRDT engine and serialization layers landed earlier on the base branch; this PR fills in everything between them and the public interfaces: value resolution, reads, writes, identity-bound instances, parent references, and subscription event bubbling. ably-jsliveobjectsplugin is the source of truth throughout, with spec points (objects-features.md, plus the unmerged typed-SDK points from ably/specification#491) anchored inline as code comments.What was missing before
The module had a complete sync engine but the path/instance layers were skeletons:
ResolvedValuewrapped the wrong types (public blueprints instead of the internal CRDT objects), every read/write/subscribe method was aTODO(), and update events carried no source message, no tombstone signal and no path information.Changes
Internal graph bridge
ResolvedValueis now a sealed interface over the internal graph:MapRef(InternalLiveMap)/CounterRef(InternalLiveCounter)/Leaf(WireObjectData), resolved per call viaLiveMapEntry.getResolvedValue()(tombstone- and dangling-ref-aware, RTLM5d2).PathSegmentshandles dot-delimited path ↔ segment conversion with escape-aware parsing (RTPO4, RTPO6). Deviation from ably-js:joinescapes backslashes as well as dots, because here the joined string is the storage and gets re-parsed on every resolution (ably-js stores segment arrays and only renders the escaped string). Without this, a key ending in\collides with the escaped-dot separator and breaks lookups/subscriptions.Typed update model
ObjectUpdateis a sealed class (NoOp/MapUpdate/CounterUpdate) carrying the sourceWireObjectMessageand atombstoneflag (RTLO4b4). Both managers thread the message through every apply/merge/diff path (RTLM6h, RTLC6h, RTLM23c).Parent references & event bubbling
parentReferences, keyed by parent objectId per RTLO3f), maintained at all mutation points and rebuilt wholesale after sync (RTO5c10, ordered before the RTO5c7 notifications).getFullPaths()computes all simple paths from root with cycle safety (RTLO4f);notifyUpdatedfans out to instance listeners, then bubbles to path subscribers viaPathObjectSubscriptionRegister(RTO24: prefix + depth coverage, most-preferred candidate path, at-most-once per subscription, per-listener error isolation), then tears down instance listeners on tombstone (RTLO4b4c3).operation == null), and surfaced messages are converted once viatoPublicMessage(PAOM3).Reads, writes and instances
RealtimeObject#get()returns the rootLiveMapPathObjectafter attach + sync (RTO23), bridged from coroutines withsequentialScope.future {}.value()s, map iteration,compactJson()with cycle markers (RTPO13/14).LiveMapValueType/LiveCounterValueType, RTLMV4/RTLCV4): nested create messages first, ownMAP_CREATElast, published together with theMAP_SETin a single protocol message (RTLM20h1).Instancetypes are value-bound (RTINS2a): live map/counter instances delegate to the internal objects; primitives capture the extracted value (binary decoded once, defensively copied out).Fix found during cross-validation against ably-js history
handleStateChange(ATTACHED)now clears buffered operations (RTO4d) and starts a new sync unconditionally (RTO4c). The previous conditional was a port of pre-e280bff1ably-js code that upstream has since removed.Testing & validation
PathSegmentsTestcovers the stored-vs-supplied path invariants and the backslash round-trip regression.runUnitTests,runLiveObjectsUnitTests,checkWithCodenarc,checkstyleMain/Testall green; zeroTODOs remain in the module.objects-features.md/ spec PR README: add a note about the push example/test app #491; behavioral markers (boundary comparisons, error codes, skip predicates, message ordering) were cross-checked against ably-js.(Number)/(Map)) unchanged, existing keep rules remain valid.Notes for reviewers
uts/build.gradle.ktsbut failures there are pending triage in a follow-up — check the spec anchor before assuming an engine bug.entries()skips dangling references (RTLM11d3a) whilekeys()does not, and the backslash escaping described above.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests