Skip to content

[LiveObjects] Implement path-based public API end-to-end; port all 15 UTS unit specs - #2234

Open
sacOO7 wants to merge 3 commits into
integration/liveobjectsfrom
feature/liveobjects-implementation
Open

[LiveObjects] Implement path-based public API end-to-end; port all 15 UTS unit specs#2234
sacOO7 wants to merge 3 commits into
integration/liveobjectsfrom
feature/liveobjects-implementation

Conversation

@sacOO7

@sacOO7 sacOO7 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR completes the LiveObjects implementation behind the previously shipped path-based public API, using ably-java's Kotlin implementation as the reference, and ports all 15 UTS objects/unit specs as its acceptance tests.

The public API surface is byte-identical to what was already shipped — every change is additive behind the frozen protocols. Zero changes to the core SDK (Source/), Cartfile, or Ably.podspec: the plugin remains SPM-only.

What's implemented

Public surface (previously trapping via notImplemented() — now zero trap sites)

Area Details
Instance layer LiveMapInstance / LiveCounterInstance / PrimitiveInstance — identity-addressed, compactJson() with cycle markers (RTPO14b2), mutations via the RTO20 publish path, enriched subscription events
Path layer PathObject base + typed path objects — stored dot-delimited path with per-call resolution (RTPO3), PathSegments escaping, exists() / type() / as* casts, reads and writes with 92005/92007 semantics
Path subscriptions PathObjectSubscriptionRegister with RTO24 depth-windowed coverage ranking, driven by getFullPaths(); both operation-driven and sync-originated updates fan out (nil message on sync, RTO4b2a)
RealtimeObject get() (guards → active-channel check → lost-wakeup-safe sync wait → root LiveMapPathObject, RTO23), on()/off() status events (RTO17/18), dispose() lifecycle

Engine additions

  • Parent-reference graph (RTLO3f/4f/4g/4h): reverse references on every live object, cycle-safe getFullPaths(), wired into all map mutation paths, RTO5c10 post-sync rebuild.
  • Update enrichment: update payloads carry the source public ObjectMessage? and a tombstone flag; PAOM3 wire→public conversion; tombstone updates tear down instance subscriptions (RTLO4b4c3c).
  • RTO27 channel-state data lifecycle (clear on DETACHED/FAILED, retain on SUSPENDED).
  • OM3 message-size computation + RTO15d publish pre-flight gate (40009).
  • Channel-config guards (throwIf*, RTO25/26 — implementable subset; core-accessor gaps carry TODO(core accessor) markers) and the plugin-local error model (92005/92007/40024/40019/40003 as raw codes).

Spec-compliance fixes (found by the ported spec tests)

14 pre-existing bugs fixed, including: RTLO4e10 root-tombstone protection; six Swift exclusive-access crash guards (self-referencing entries arriving over the wire could crash the client); RTLM24c equal-serial tie-break; RTLM22-consistent tombstone teardown diffs; empty/zero diffs collapsed to no-op (no spurious subscriber events); RTO9a2b/RTO9a3 checks hoisted before zero-value creation; RTO5a malformed sync-cursor handling (previously aborted the whole sync; now falls back to the RTO5a5 self-contained path).

Testing

  • All 15 UTS objects/unit specs ported to LiveObjects/Tests/AblyLiveObjectsTests/UTS/ (16 files, <Name>UTSTests naming, per-case // @UTS traceability tags) — mirroring ably-java's consolidated uts/unit/ layout. A README.md and deviations.md accompany them.
  • 653 unit tests passing (native + UTS ports) under the standard test-library gate; lint clean; availability annotations verified.
  • Native test additions cover the Swift-specific behaviour no cross-SDK spec can (exclusivity guards, queue discipline, publish-gate, seams).
  • Mock-WebSocket-dependent spec cases are skip-marked with reasons (unit scope); the integration tier is a follow-up.

Deviations & upstream notes

Every intentional divergence from ably-java or the spec is recorded — in the per-file test headers, LiveObjects/Tests/AblyLiveObjectsTests/UTS/deviations.md, and the port's deviation register. Highlights for reviewers:

  • Two places where the spec text supports this implementation over js/java (RTLM23b unconditional createOperationIsMerged; RTLM23/RTLC16 derivedFrom precedence) — kept spec-compliant; a spec clarification is recommended.
  • Swift-specific structural choices (pool-hosted getFullPaths, engine-level path fan-out) exist because Kotlin's in-object designs trip Swift's exclusive-access checker — observable behaviour is identical.
  • Four items pend core-SDK accessors (maxMessageSize, channel modes/echo, attach initiation, channel-release dispose SPI) and are marked with TODO(core accessor) in code.

Reviewer notes

  • Review by layer is easiest: Internal/LiveObjectMutableState + InternalLiveObject (graph) → LiveObjectUpdate/ObjectMessage (enrichment) → Path Based API/Default/ (instances, path objects) → PathObjectSubscriptionRegisterPublicDefaultRealtimeObject.
  • testsOnly_ members are test seams (established convention); nosync_ marks on-queue entry points.
  • The Test/UTS target change is a trim only: objects deviations moved next to the consolidated tests; the shared rest/realtime harness is untouched.

Summary by CodeRabbit

  • New Features
    • Added functional Live Objects maps, counters, primitives, path navigation, reads, writes, JSON conversion, and subscriptions.
    • Added realtime object retrieval, status events, synchronization handling, and subscription lifecycle management.
    • Added public object-message conversion and support for nested and cyclic object structures.
    • Added channel configuration validation and message-size limits.
  • Bug Fixes
    • Prevented no-op updates, unsafe self-references, invalid paths, unsupported operations, and oversized publishes.
    • Improved tombstone cleanup, channel-state handling, path rebinding, and garbage collection.
  • Documentation
    • Expanded API and specification-alignment documentation.
  • Tests
    • Added comprehensive coverage across Live Objects functionality and edge cases.

…t specs

Completes the LiveObjects implementation behind the shipped public API
(which is unchanged), ported from ably-java as the reference:

- Instance and PathObject layers (typed instances, per-call path
  resolution, compactJson with cycle markers, as* casts)
- Path subscriptions with RTO24 depth-windowed coverage, wired to
  getFullPaths over the new parent-reference graph (RTLO3f/4f/4g/4h,
  RTO5c10 post-sync rebuild)
- RealtimeObject.get()/on()/dispose (RTO23, RTO17/18, RTO20e waiters)
- Update enrichment: objectMessage + tombstone on update payloads,
  PAOM3 wire-to-public conversion, tombstone subscription teardown
- RTO27 channel-state data lifecycle; OM3 message-size publish gate;
  RTO5a sync-cursor reconciliation; channel-config guards + error model
- 14 spec-compliance bug fixes found by the ported spec tests
  (RTLO4e10 root-tombstone protection, exclusive-access crash guards,
  RTLM24c tie-break, RTLM22 teardown diffs, RTO9a2b check ordering, ...)

All 15 UTS objects/unit specs are ported to
LiveObjects/Tests/AblyLiveObjectsTests/UTS/ (matching ably-java's
uts/unit layout) with @uts traceability tags and a deviations register.

653 unit tests passing; lint clean; public API surface byte-identical
to the previously shipped commit.
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e1692e3-3b63-4c83-84d8-82281b7792c3

📥 Commits

Reviewing files that changed from the base of the PR and between 71b7d20 and 0d2cd22.

📒 Files selected for processing (20)
  • LiveObjects/Sources/AblyLiveObjects/Internal/ChannelConfigGuards.swift
  • LiveObjects/Sources/AblyLiveObjects/Internal/CoreSDK.swift
  • LiveObjects/Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift
  • LiveObjects/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
  • LiveObjects/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObject.swift
  • LiveObjects/Sources/AblyLiveObjects/Utility/Errors.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/DefaultInternalPluginTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/DefaultPathObjectTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/PathObjectSubscriptionTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/PublicRealtimeObjectTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/ObjectsUTSHelpers.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/WireObjectMessageSizeTests.swift
  • Source/ARTPluginAPI.m
  • Source/ARTRealtimeChannels.m
  • Test/AblyTests/Tests/PluginAPITests.swift
  • _AblyPluginSupportPrivate/include/APConnectionDetails.h
  • _AblyPluginSupportPrivate/include/APLiveObjectsPlugin.h
  • _AblyPluginSupportPrivate/include/APPluginAPI.h
🚧 Files skipped from review as they are similar to previous changes (6)
  • LiveObjects/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObject.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/ObjectsUTSHelpers.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift
  • LiveObjects/Sources/AblyLiveObjects/Utility/Errors.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/PathObjectSubscriptionTests.swift
  • LiveObjects/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift

Walkthrough

The LiveObjects implementation now supports path-based reads and writes, public instances, subscriptions, object-message conversion, parent-reference tracking, tombstone handling, synchronization lifecycle behavior, message-size validation, and extensive native and UTS test coverage.

Changes

LiveObjects implementation

Layer / File(s) Summary
Contracts, validation, and path resolution
LiveObjects/Sources/AblyLiveObjects/Internal/*, LiveObjects/Sources/AblyLiveObjects/Protocol/*, LiveObjects/Sources/AblyLiveObjects/Utility/*
Added channel guards, plugin bridge accessors, path parsing, error mappings, update metadata, public object-message conversion, and wire-size calculations.
Object state and runtime flow
LiveObjects/Sources/AblyLiveObjects/Internal/*
Added parent-reference tracking, enriched updates, tombstone teardown, synchronization waiters, lifecycle disposal, channel-state clearing, and path subscription dispatch.
Path-based public API
LiveObjects/Sources/AblyLiveObjects/Path Based API/*, LiveObjects/Sources/AblyLiveObjects/Public/*
Implemented map, counter, primitive, path-object, instance, realtime-object, and subscription behavior.
Validation and conformance tests
LiveObjects/Tests/AblyLiveObjectsTests/*
Added native tests for path resolution, instances, mutations, lifecycle transitions, subscriptions, parent references, sync cursors, and message sizing.
UTS suites and test setup
LiveObjects/Tests/AblyLiveObjectsTests/UTS/*, Package.swift, Test/UTS/deviations.md
Added UTS-derived LiveObjects suites, fixtures, deviations documentation, and test-target configuration.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: maratal

Poem

A rabbit checks each path and map,
Then guards the channel’s state.
Updates carry messages home,
While tombstones close the gate.
Tests hop through every case.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.40% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR's primary work: completing the path-based LiveObjects API and porting the 15 UTS unit specifications.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/liveobjects-implementation

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

❤️ Share

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

@github-actions
github-actions Bot temporarily deployed to staging/pull/2234/features July 31, 2026 10:19 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2234/AblyLiveObjects July 31, 2026 10:20 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2234/features July 31, 2026 10:24 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2234/AblyLiveObjects July 31, 2026 10:24 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2234/jazzydoc July 31, 2026 10:26 Inactive
@github-actions
github-actions Bot temporarily deployed to staging/pull/2234/markdown-api-reference July 31, 2026 10:26 Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
LiveObjects/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (1)

781-796: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the stale docstring on apply.

The docstring at Line 783 says the method "Returns: true if the operation was applied, false if skipped (RTLM15g)." The return type at Line 796 is LiveObjectUpdate<DefaultLiveMapUpdate>?, not Bool. The docstring was not updated when the signature changed.

Compare with the public wrapper nosync_apply (Line 396), whose docstring was correctly updated to describe the new optional/enriched-update contract. Update this docstring the same way to avoid confusing future readers about the RTLM15g contract.

📝 Proposed docstring fix
         /// Attempts to apply an operation from an inbound `ObjectMessage`, per RTLM15.
         ///
-        /// - Returns: `true` if the operation was applied, `false` if skipped (RTLM15g).
+        /// - Returns: The update that was emitted if the operation was applied (which may be `.noop`), or `nil` if the operation was skipped (RTLM15g).
         internal mutating func apply(
🤖 Prompt for 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.

In `@LiveObjects/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift`
around lines 781 - 796, Update the documentation for
InternalDefaultLiveMap.apply to describe its
LiveObjectUpdate<DefaultLiveMapUpdate>? result and optional/enriched-update
behavior, matching the contract documented by the public nosync_apply wrapper.
Remove the stale Bool and RTLM15g skipped-operation wording.
🧹 Nitpick comments (20)
LiveObjects/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (2)

566-569: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer explicit cases over default: in the action switch.

The default: clause absorbs both unknown wire actions and every unhandled known action. If a new counter-relevant ObjectOperationAction case is added, this switch silently logs a warning instead of failing the build. Listing the known map-only actions explicitly and keeping case .unknown separate restores compile-time exhaustiveness.

♻️ Proposed change
-            default:
+            case .known(.mapCreate), .known(.mapSet), .known(.mapRemove), .known(.mapClear), .unknown:
                 // RTLC7d3
                 logger.log("Operation \(operation) has unsupported action for LiveCounter; discarding", level: .warn)
                 return nil
🤖 Prompt for 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.

In
`@LiveObjects/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift`
around lines 566 - 569, Update the action switch in the LiveCounter operation
handling to remove the default clause. List each known map-only
ObjectOperationAction explicitly with the existing warning-and-discard behavior,
and keep case .unknown separate, so newly added actions require explicit
handling at compile time.

320-328: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Remove the default value from sourceObjectMessage.

sourceObjectMessage defaults to nil. RTLO4b4d requires the emitted update to carry the source public message for channel-originated operations. A production caller that omits the argument compiles cleanly and emits updates with objectMessage == nil. The default exists only so existing tests can omit it.

Make the parameter required, and pass nil explicitly from the sync-originated and test call sites.

♻️ Proposed change
         objectMessageSerialTimestamp: Date?,
-        sourceObjectMessage: ObjectMessage? = nil,
+        sourceObjectMessage: ObjectMessage?,
         objectsPool: inout ObjectsPool,
🤖 Prompt for 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.

In
`@LiveObjects/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift`
around lines 320 - 328, Remove the default value from sourceObjectMessage in
nosync_apply so callers must provide it. Update all sync-originated and test
call sites to pass nil explicitly, while ensuring channel-originated callers
pass the source public message required for emitted updates.
LiveObjects/Sources/AblyLiveObjects/Internal/ChannelConfigGuards.swift (1)

40-43: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The stub guard silently passes.

throwIfMissingObjectSubscribeMode accepts both parameters and discards them, and its body never throws. Every call site therefore performs no check while reading as if it enforces RTO2a2. The doc comment explains the plugin-API gap, so this is intentional. Consider tracking the gap in an issue so the stub does not outlive the missing accessor.

Do you want me to open a tracking issue for the object_subscribe / object_publish mode accessors?

🤖 Prompt for 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.

In `@LiveObjects/Sources/AblyLiveObjects/Internal/ChannelConfigGuards.swift`
around lines 40 - 43, Track the missing channel-modes accessor gap in an issue
covering both object_subscribe and object_publish accessors, and reference that
issue from the TODO associated with throwIfMissingObjectSubscribeMode so the
intentional no-op stub is not left without follow-up.
LiveObjects/Sources/AblyLiveObjects/Path Based API/Default/DefaultPrimitiveInstance.swift (1)

51-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename nosync_checkAccessPreconditionsOnQueue.

The nosync_ prefix marks methods in this codebase that must already run on the internal queue and that perform no synchronization. This method does the opposite: it enters the queue with ably_syncNoDeadlock. Rename it to something like checkAccessPreconditions() so the prefix keeps one meaning.

♻️ Proposed change
-    private func nosync_checkAccessPreconditionsOnQueue() throws(ARTErrorInfo) {
+    private func checkAccessPreconditions() throws(ARTErrorInfo) {

Update both call sites (Lines 31 and 44).

🤖 Prompt for 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.

In `@LiveObjects/Sources/AblyLiveObjects/Path` Based
API/Default/DefaultPrimitiveInstance.swift around lines 51 - 64, Rename
nosync_checkAccessPreconditionsOnQueue to checkAccessPreconditions, reflecting
that it synchronizes onto internalQueue via ably_syncNoDeadlock. Update both
call sites in DefaultPrimitiveInstance while preserving the existing
channel-state validation behavior.
LiveObjects/Sources/AblyLiveObjects/Internal/ObjectsPool.swift (1)

558-575: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Skip the path DFS when no path subscription exists.

nosync_getFullPaths walks the parent-reference graph and enumerates every simple path. Path enumeration grows exponentially with graph density. This runs for every applied operation (nosync_applyObjectProtocolMessageObjectMessage) and for every non-noop deferred sync update in the loop at Lines 450-465. When an application registers no path subscription, the whole walk is discarded.

Add an emptiness check to PathObjectSubscriptionRegister and return before the DFS.

♻️ Proposed early return

In PathObjectSubscriptionRegister:

/// `true` when no subscription is registered. Callers use this to skip path resolution.
internal var nosync_isEmpty: Bool {
    dispatchPrecondition(condition: .onQueue(internalQueue))
    return subscriptions.isEmpty
}

In ObjectsPool.nosync_notifyPathSubscriptions:

     ) {
+        if register.nosync_isEmpty {
+            return // nothing to dispatch; avoid the getFullPaths walk
+        }
         let pathsToThis = nosync_getFullPaths(forObjectID: objectID) // RTO24b1
🤖 Prompt for 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.

In `@LiveObjects/Sources/AblyLiveObjects/Internal/ObjectsPool.swift` around lines
558 - 575, Add the queue-validated `nosync_isEmpty` property to
`PathObjectSubscriptionRegister`, returning whether its subscriptions collection
is empty. In `ObjectsPool.nosync_notifyPathSubscriptions`, check this property
and return before calling `nosync_getFullPaths`, while preserving the existing
path notification behavior when subscriptions exist.
LiveObjects/Sources/AblyLiveObjects/Path Based API/Default/DefaultLiveMapInstance.swift (1)

233-238: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Throw instead of trapping when the delegate cannot create objects.

set(key:value:) reaches objectCreator for any LiveMap/LiveCounter blueprint value. preconditionFailure then terminates the process. Any conformer of InternalRealtimeObjectsProtocol other than InternalDefaultRealtimeObjects (for example a test double, or a future conformer) turns a normal write into a crash. internalValue(from:...) is already throws(ARTErrorInfo), so an error is the cheaper failure mode.

♻️ Proposed change
-    private static func objectCreator(_ realtimeObjects: any InternalRealtimeObjectsProtocol) -> InternalDefaultRealtimeObjects {
+    private static func objectCreator(_ realtimeObjects: any InternalRealtimeObjectsProtocol) throws(ARTErrorInfo) -> InternalDefaultRealtimeObjects {
         guard let creator = realtimeObjects as? InternalDefaultRealtimeObjects else {
-            preconditionFailure("Setting a LiveMap/LiveCounter blueprint value requires the concrete InternalDefaultRealtimeObjects")
+            throw LiveObjectsError.other(
+                message: "Setting a LiveMap/LiveCounter blueprint value requires the concrete InternalDefaultRealtimeObjects",
+            ).toARTErrorInfo()
         }
         return creator
     }

Adjust the two call sites to try Self.objectCreator(realtimeObjects), and use the error case that matches LiveObjectsError.

🤖 Prompt for 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.

In `@LiveObjects/Sources/AblyLiveObjects/Path` Based
API/Default/DefaultLiveMapInstance.swift around lines 233 - 238, Update
objectCreator and both call sites in internalValue(from:...) to propagate an
ARTErrorInfo-compatible LiveObjectsError instead of invoking preconditionFailure
when the delegate is not InternalDefaultRealtimeObjects. Mark objectCreator as
throwing, adjust callers to use try, and preserve the existing successful
creator path.
LiveObjects/Sources/AblyLiveObjects/Path Based API/Default/DefaultLiveCounterInstance.swift (1)

55-72: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Document or fix the listener lifetime contract.

The listener captures self weakly. DefaultSubscription stores only the SubscribeResponse, so it does not retain this instance. If a caller keeps only the returned Subscription and releases the LiveCounterInstance (for example when the instance comes from a temporary Instance.from(...) result), the listener stops firing and no error is reported.

The weak capture does break the node → listener → selfnode cycle, so it is defensible. Make the requirement explicit in the doc comment, or let the returned subscription retain the instance so delivery does not depend on caller-side retention.

♻️ Option: document the retention requirement
     `@discardableResult`
     internal func subscribe(listener: `@escaping` InstanceSubscriptionCallback) throws(ARTErrorInfo) -> any Subscription {
+        // Lifetime: the listener holds `self` weakly to avoid a `node` -> listener -> `self` -> `node`
+        // cycle. The caller must keep this instance alive for as long as it expects events; retaining
+        // only the returned Subscription is not sufficient.
         // RTINS16b (the node's subscribe runs the RTO25b check), RTINS16d -> RTLO4b
🤖 Prompt for 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.

In `@LiveObjects/Sources/AblyLiveObjects/Path` Based
API/Default/DefaultLiveCounterInstance.swift around lines 55 - 72, Document the
retention contract for subscribe in DefaultLiveCounterInstance: state that
callers must retain the LiveCounterInstance while relying on the returned
Subscription because the listener captures self weakly and delivery stops once
the instance is released. Keep the existing weak capture and subscription
behavior unchanged.
LiveObjects/Sources/AblyLiveObjects/Path Based API/Default/DefaultPathObject.swift (1)

143-160: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Document or make path resolution atomic.

resolveValueAtCurrentPath() reads the root on one internal-queue block, then each mapNode.get(key:coreSDK:delegate:) locks independently via InternalDefaultLiveMap.get. Multi-segment reads can be built from different graph states between the root read, key lookup, and any intermediate object states. If this best-effort behavior is intentional, document it near resolveValueAtCurrentPath(); otherwise perform the entire walk in one internal-queue block using a queue-confined map accessor.

🤖 Prompt for 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.

In `@LiveObjects/Sources/AblyLiveObjects/Path` Based
API/Default/DefaultPathObject.swift around lines 143 - 160, Make path resolution
in resolveValueAtCurrentPath atomic by executing the root read and entire
PathSegments.parseStored(path) traversal within one internalQueue block, using a
queue-confined map accessor instead of independently locking through
mapNode.get. Preserve the existing empty-path, non-map, missing-key, and
successful-value behaviors; if atomic traversal is not intended, document the
best-effort consistency semantics near the method instead.
LiveObjects/Tests/AblyLiveObjectsTests/UTS/ObjectsUTSHelpers.swift (1)

165-196: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider deduping the publish holder and node factories.

The header comment (lines 12-17) states that the UTS ports now live in the AblyLiveObjectsTests target, and that the trivial duplicates were folded into TestLogger / MockSimpleClock. Two duplicates remain against DefaultInstanceTests.swift:

  • ObjectsUTSPublished (lines 169-180) duplicates the private Published holder (DefaultInstanceTests.swift lines 470-480).
  • ObjectsUTS.makeCounter / ObjectsUTS.makeMap (lines 190-196) duplicate DefaultInstanceTests.makeCounter / makeMap (lines 16-22).

Since the import barrier no longer applies, you can promote one copy of each to an internal helper and delete the other. This is optional; the current duplication is small and harmless.

🤖 Prompt for 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.

In `@LiveObjects/Tests/AblyLiveObjectsTests/UTS/ObjectsUTSHelpers.swift` around
lines 165 - 196, Deduplicate the shared publish holder and node factories across
the UTS helpers and DefaultInstanceTests. Promote one copy of
ObjectsUTSPublished/Published and ObjectsUTS.makeCounter/makeMap to an internal
shared helper, update both call sites to use it, and remove the redundant
private definitions while preserving their current behavior.
LiveObjects/Tests/AblyLiveObjectsTests/DefaultInstanceTests.swift (1)

267-270: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Update the stale self-reference note.

The comment states that a direct self-reference re-enters the map mutex and is "a pre-existing engine limitation". This PR fixes that path. InternalDefaultLiveMapTests.selfReferencingEntryIsTreatedAsNormalEntry (lines 528-579) asserts that entries(), keys(), values() and get() now treat a direct self-reference as a normal entry. Keep the indirect A→B→A fixture if you prefer it, but remove the claim that a direct self-reference crashes.

📝 Proposed comment update
-        // An indirect cycle A -> B -> A. (A direct self-reference is not used: the internal engine's
-        // `entries()` -> `nosync_isEntryTombstoned` re-enters the same map's mutex for a self-referencing
-        // entry, an exclusive-access conflict — a pre-existing engine limitation, unrelated to the
-        // compactJson cycle handling exercised here.)
+        // An indirect cycle A -> B -> A. (A direct self-reference is covered separately by
+        // `InternalDefaultLiveMapTests.selfReferencingEntryIsTreatedAsNormalEntry`; this case targets
+        // the multi-hop revisit branch of the compactJson walk.)
🤖 Prompt for 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.

In `@LiveObjects/Tests/AblyLiveObjectsTests/DefaultInstanceTests.swift` around
lines 267 - 270, Update the cycle-handling comment near the indirect A → B → A
fixture to remove the claim that direct self-reference re-enters the mutex or
remains a pre-existing engine limitation. Reflect that direct self-referencing
entries are now supported and treated as normal entries, while retaining the
indirect-cycle explanation if still relevant.
LiveObjects/Tests/AblyLiveObjectsTests/UTS/RealtimeObjectUTSTests.swift (1)

239-239: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The lint directives are inert.

Both lines carry swiftformat:disable:this and swiftlint:disable:this directives. The repository does not run SwiftLint or SwiftFormat, so neither directive has an effect. You can drop them and keep the plain #expect(counter.count == 0). This is optional.

As per coding guidelines: "Maintain EditorConfig compliance only (no SwiftLint/SwiftFormat)".

Also applies to: 363-363

🤖 Prompt for 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.

In `@LiveObjects/Tests/AblyLiveObjectsTests/UTS/RealtimeObjectUTSTests.swift` at
line 239, Remove the inert swiftformat and swiftlint directives from the `#expect`
assertions in the affected test cases, leaving plain `#expect`(counter.count == 0)
assertions at both locations.

Source: Coding guidelines

LiveObjects/Tests/AblyLiveObjectsTests/PublicRealtimeObjectTests.swift (1)

176-180: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the fixed sleep with a deterministic queue drain.

The test waits 100 ms and then asserts that no callback ran. The engine dispatches status callbacks to userCallbackQueue, which is .main here. One awaited hop on .main guarantees that every already-scheduled callback has run. That removes the timing dependence and the fixed delay. RealtimeObjectUTSTests already uses this pattern in drainMain() (lines 109-113) for the same assertion in RTO19_off_deregisters.

♻️ Proposed change
         Self.driveToSynced(proxied, on: internalQueue)
 
-        // Allow any (erroneously) scheduled callback to be delivered on the callback queue.
-        try await Task.sleep(nanoseconds: 100_000_000)
+        // Drain the callback queue so any (erroneously) scheduled callback has been delivered.
+        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
+            DispatchQueue.main.async { continuation.resume() }
+        }
         `#expect`(counter.isEmpty)
🤖 Prompt for 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.

In `@LiveObjects/Tests/AblyLiveObjectsTests/PublicRealtimeObjectTests.swift`
around lines 176 - 180, In the test around Self.driveToSynced, replace the fixed
Task.sleep delay with the existing deterministic main-queue drain pattern used
by RealtimeObjectUTSTests.drainMain(). Await one hop on the main callback queue
before asserting counter.isEmpty, preserving the assertion that no callback was
delivered.
LiveObjects/Tests/AblyLiveObjectsTests/UTS/PathObjectSubscribeUTSTests.swift (1)

296-311: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider exact event counts where dispatch is deterministic.

Three assertions use >=:

  • Line 310: events.events.count >= 3 after three drained applies.
  • Line 438: events.events.count >= 1 after one MAP_CLEAR.
  • Line 454: events.events.count >= 2 after two drained applies.

Each stimulus is applied through applyAndDrain, so delivery is complete before the assertion. An exact count would catch a duplicate-dispatch or extra-candidate regression that >= accepts. If the MAP_CLEAR fan-out count is intentionally unpinned, keep >= there and add a short note stating why.

Note: the >= assertions on the control collectors (lines 362, 383, 292, 343) are quiescence barriers for negative assertions. Those should stay as they are.

Also applies to: 426-455

🤖 Prompt for 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.

In `@LiveObjects/Tests/AblyLiveObjectsTests/UTS/PathObjectSubscribeUTSTests.swift`
around lines 296 - 311, Replace the >= event-count assertions in
RTPO19c1_unlimited_depth and the corresponding MAP_CLEAR and two-apply tests
with exact expected counts, since applyAndDrain completes delivery
deterministically. Preserve the control-collector >= assertions used as
quiescence barriers; if MAP_CLEAR fan-out is intentionally variable, retain its
lower-bound assertion and add a brief explanation.
LiveObjects/Tests/AblyLiveObjectsTests/DefaultPathObjectTests.swift (2)

7-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Update the stale doc comment about path subscriptions.

The comment states that path subscriptions (RTPO19/RTO24) "still trap". This PR implements them, and PathObjectSubscriptionRegister plus dedicated subscription tests now exist. Remove or reword that clause so the file header matches the shipped behaviour.

♻️ Proposed doc update
-/// subclasses, the ``ChannelConfigGuards`` state checks, and the extended error model. Path
-/// *subscriptions* (RTPO19/RTO24) are part 2 and are not exercised here (they still trap).
+/// subclasses, the ``ChannelConfigGuards`` state checks, and the extended error model. Path
+/// *subscriptions* (RTPO19/RTO24) are covered separately by the path-subscription tests.
🤖 Prompt for 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.

In `@LiveObjects/Tests/AblyLiveObjectsTests/DefaultPathObjectTests.swift` around
lines 7 - 12, Update the file-level documentation near the PathSegments and
DefaultPathObject description to remove or reword the claim that path
subscriptions RTPO19/RTO24 still trap, reflecting that
PathObjectSubscriptionRegister and their tests are now implemented.

388-395: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider accumulating published messages in the double.

nosync_publishAndApply replaces _captured on every call. A future test that performs two writes will silently observe only the last batch. Appending keeps the double usable for multi-write cases.

♻️ Proposed change
-        mutex.withLock { _captured = objectMessages }
+        mutex.withLock { _captured = (_captured ?? []) + objectMessages }
🤖 Prompt for 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.

In `@LiveObjects/Tests/AblyLiveObjectsTests/DefaultPathObjectTests.swift` around
lines 388 - 395, Update nosync_publishAndApply to append objectMessages to the
existing _captured collection rather than replacing it, while preserving the
current mutex protection and success callback behavior.
LiveObjects/Tests/AblyLiveObjectsTests/ParentReferencesTests.swift (1)

5-8: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reconcile this file with the UTS port that landed in the same PR.

The header states that the full UTS port of objects/unit/parent_references.md is "a separate follow-up". That port is included in this PR as LiveObjects/Tests/AblyLiveObjectsTests/UTS/ParentReferencesUTSTests.swift, and it covers add, remove, diamond, cycle, orphan and the RTO5c10 rebuild with stricter assertions. Update this header, and consider keeping only the cases the UTS port does not cover (for example clearParentReferences), to avoid maintaining two copies of the same behaviour.

🤖 Prompt for 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.

In `@LiveObjects/Tests/AblyLiveObjectsTests/ParentReferencesTests.swift` around
lines 5 - 8, Reconcile the ParentReferencesTests header with the existing
ParentReferencesUTSTests coverage: remove the claim that the UTS port is a
separate follow-up, and retain only native smoke cases not covered by the UTS
tests, such as clearParentReferences, to avoid duplicate behavior coverage.
LiveObjects/Tests/AblyLiveObjectsTests/Mocks/MockRealtimeObjects.swift (1)

12-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider failing fast if the register is used.

The register is created on queues that no test object shares. If a test ever routes path-subscription dispatch through this mock, the queue mismatch produces an internal-queue assertion rather than a clear message. nosync_objectsPool already uses preconditionFailure for unsupported use. Applying the same pattern here keeps misuse obvious.

🤖 Prompt for 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.

In `@LiveObjects/Tests/AblyLiveObjectsTests/Mocks/MockRealtimeObjects.swift`
around lines 12 - 25, The mock’s nosync_pathObjectSubscriptionRegister currently
exposes an unusable register instead of failing clearly when accessed. Replace
the getter implementation with preconditionFailure, matching the existing
nosync_objectsPool unsupported-use pattern, and remove the unused
_pathObjectSubscriptionRegister initialization and stored property.
LiveObjects/Tests/AblyLiveObjectsTests/UTS/ObjectIdUTSTests.swift (1)

41-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the objectId parsing and align the padding assertion.

objectIdFormatCounter and hashIsBase64urlEncoded split the same objectId with two different idioms, and they assert padding differently: line 51 uses !contains("="), line 121 uses !hasSuffix("="). A single helper that returns (type, hash, timestamp) removes the duplication and lets both tests use the stricter !contains("=") check.

♻️ Proposed helper
/// Splits `{type}:{hash}@{timestamp}` into its three parts.
private static func parts(of objectId: String) throws -> (type: String, hash: String, timestamp: String) {
    let type = String(objectId.prefix { $0 != ":" })
    let rest = String(objectId.dropFirst(type.count + 1))
    let hash = String(rest.prefix { $0 != "@" })
    return (type, hash, String(rest.dropFirst(hash.count + 1)))
}

Also applies to: 116-121

🤖 Prompt for 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.

In `@LiveObjects/Tests/AblyLiveObjectsTests/UTS/ObjectIdUTSTests.swift` around
lines 41 - 51, Extract the duplicated `{type}:{hash}@{timestamp}` parsing from
`objectIdFormatCounter` and `hashIsBase64urlEncoded` into a shared `parts(of:)`
helper returning type, hash, and timestamp. Update both tests to use the helper
and change the padding assertion in `hashIsBase64urlEncoded` from
`hasSuffix("=")` to rejecting any `=` with `contains`.
LiveObjects/Tests/AblyLiveObjectsTests/UTS/ValueTypesUTSTests.swift (1)

59-59: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the inert lint directives.

The repository uses EditorConfig only and does not run SwiftLint or SwiftFormat. The swiftformat:disable/swiftlint:disable comments on Line 59 and Line 114 have no effect. Delete them to keep the assertions readable.

As per coding guidelines: "Maintain EditorConfig compliance only (no SwiftLint/SwiftFormat)".

🤖 Prompt for 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.

In `@LiveObjects/Tests/AblyLiveObjectsTests/UTS/ValueTypesUTSTests.swift` at line
59, Remove the inert swiftformat:disable and swiftlint:disable comments from the
count assertions at the referenced test locations, including the assertion near
vt.count and the corresponding one near line 114. Keep the assertions themselves
unchanged and maintain EditorConfig-only formatting.

Source: Coding guidelines

Package.swift (1)

117-117: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the unused _AblyPluginSupportPrivate dependency from the UTS target.

No Swift file under Test/UTS imports _AblyPluginSupportPrivate; the LiveObjects UTS imports are under LiveObjects/Tests/AblyLiveObjectsTests, so this dependency is unnecessary for the standalone UTS target.

🤖 Prompt for 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.

In `@Package.swift` at line 117, Remove the `.target(name:
"_AblyPluginSupportPrivate")` dependency from the `UTS` target definition in
`Package.swift`, leaving the remaining UTS dependencies unchanged.
🤖 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/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift`:
- Around line 1183-1217: Update the public message conversion in the
action-handling flow around objectMessage.toPublicObjectMessage so emitted
ObjectMessage values never receive an empty channel. Ensure the real channel
name is supplied when constructing or converting the message, or omit the
channel field until a valid channel accessor is available; preserve the existing
path-subscription notification behavior.

In `@LiveObjects/Sources/AblyLiveObjects/Protocol/ObjectMessage.swift`:
- Around line 649-650: The extras size calculation in ObjectMessage must use the
UTF-8 byte count of the serialized JSON instead of utf16.count. Update the
relevant ObjectsMap entry-key size calculation as well if it participates in the
same published limit, ensuring RTO15d validates actual wire JSON size for
non-ASCII content.

In
`@LiveObjects/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift`:
- Around line 2189-2219: Update clearOnDetachedEmitsNoUpdateEvents to drain the
main callback queue before checking subscriber invocations: after triggering
nosync_onChannelStateChanged, add the same main-queue synchronization/barrier
pattern used by emitsCorrectSyncEvents, then retain the existing empty
assertions for rootSubscriber, mapSubscriber, and counterSubscriber.

In `@LiveObjects/Tests/AblyLiveObjectsTests/ParentReferencesTests.swift`:
- Around line 147-155: Correct the cycle-suppression comment to match the parent
relationship created by nosync_addParentReference, then strengthen the mapL
cycle test around testsOnly_getFullPaths by asserting the exact expected path
count, following the corresponding UTS/ParentReferencesUTSTests behavior. Remove
the redundant non-empty assertion while retaining the expected ["left"] path
check.

In `@LiveObjects/Tests/AblyLiveObjectsTests/UTS/deviations.md`:
- Around line 262-263: Remove the leaked tool-artifact closing tags from all
affected documentation files: delete both closing tags at
LiveObjects/Tests/AblyLiveObjectsTests/UTS/deviations.md lines 262-263, and
delete the closing content tag at
LiveObjects/Tests/AblyLiveObjectsTests/UTS/README.md line 32 and
Test/UTS/deviations.md line 35.
- Around line 56-65: Update the test references in deviations.md to match the
actual Swift Testing method names: remove the test_ prefix from
DEV5_object_operation_action_seven_distinct_cases and
objectData_holds_typed_values, and correct the similarly prefixed
InstanceUTSTests.test_RTINS14a... reference on line 126. Preserve the
surrounding DEV-5 and DEV-6 descriptions.

---

Outside diff comments:
In `@LiveObjects/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift`:
- Around line 781-796: Update the documentation for InternalDefaultLiveMap.apply
to describe its LiveObjectUpdate<DefaultLiveMapUpdate>? result and
optional/enriched-update behavior, matching the contract documented by the
public nosync_apply wrapper. Remove the stale Bool and RTLM15g skipped-operation
wording.

---

Nitpick comments:
In `@LiveObjects/Sources/AblyLiveObjects/Internal/ChannelConfigGuards.swift`:
- Around line 40-43: Track the missing channel-modes accessor gap in an issue
covering both object_subscribe and object_publish accessors, and reference that
issue from the TODO associated with throwIfMissingObjectSubscribeMode so the
intentional no-op stub is not left without follow-up.

In
`@LiveObjects/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift`:
- Around line 566-569: Update the action switch in the LiveCounter operation
handling to remove the default clause. List each known map-only
ObjectOperationAction explicitly with the existing warning-and-discard behavior,
and keep case .unknown separate, so newly added actions require explicit
handling at compile time.
- Around line 320-328: Remove the default value from sourceObjectMessage in
nosync_apply so callers must provide it. Update all sync-originated and test
call sites to pass nil explicitly, while ensuring channel-originated callers
pass the source public message required for emitted updates.

In `@LiveObjects/Sources/AblyLiveObjects/Internal/ObjectsPool.swift`:
- Around line 558-575: Add the queue-validated `nosync_isEmpty` property to
`PathObjectSubscriptionRegister`, returning whether its subscriptions collection
is empty. In `ObjectsPool.nosync_notifyPathSubscriptions`, check this property
and return before calling `nosync_getFullPaths`, while preserving the existing
path notification behavior when subscriptions exist.

In `@LiveObjects/Sources/AblyLiveObjects/Path` Based
API/Default/DefaultLiveCounterInstance.swift:
- Around line 55-72: Document the retention contract for subscribe in
DefaultLiveCounterInstance: state that callers must retain the
LiveCounterInstance while relying on the returned Subscription because the
listener captures self weakly and delivery stops once the instance is released.
Keep the existing weak capture and subscription behavior unchanged.

In `@LiveObjects/Sources/AblyLiveObjects/Path` Based
API/Default/DefaultLiveMapInstance.swift:
- Around line 233-238: Update objectCreator and both call sites in
internalValue(from:...) to propagate an ARTErrorInfo-compatible LiveObjectsError
instead of invoking preconditionFailure when the delegate is not
InternalDefaultRealtimeObjects. Mark objectCreator as throwing, adjust callers
to use try, and preserve the existing successful creator path.

In `@LiveObjects/Sources/AblyLiveObjects/Path` Based
API/Default/DefaultPathObject.swift:
- Around line 143-160: Make path resolution in resolveValueAtCurrentPath atomic
by executing the root read and entire PathSegments.parseStored(path) traversal
within one internalQueue block, using a queue-confined map accessor instead of
independently locking through mapNode.get. Preserve the existing empty-path,
non-map, missing-key, and successful-value behaviors; if atomic traversal is not
intended, document the best-effort consistency semantics near the method
instead.

In `@LiveObjects/Sources/AblyLiveObjects/Path` Based
API/Default/DefaultPrimitiveInstance.swift:
- Around line 51-64: Rename nosync_checkAccessPreconditionsOnQueue to
checkAccessPreconditions, reflecting that it synchronizes onto internalQueue via
ably_syncNoDeadlock. Update both call sites in DefaultPrimitiveInstance while
preserving the existing channel-state validation behavior.

In `@LiveObjects/Tests/AblyLiveObjectsTests/DefaultInstanceTests.swift`:
- Around line 267-270: Update the cycle-handling comment near the indirect A → B
→ A fixture to remove the claim that direct self-reference re-enters the mutex
or remains a pre-existing engine limitation. Reflect that direct
self-referencing entries are now supported and treated as normal entries, while
retaining the indirect-cycle explanation if still relevant.

In `@LiveObjects/Tests/AblyLiveObjectsTests/DefaultPathObjectTests.swift`:
- Around line 7-12: Update the file-level documentation near the PathSegments
and DefaultPathObject description to remove or reword the claim that path
subscriptions RTPO19/RTO24 still trap, reflecting that
PathObjectSubscriptionRegister and their tests are now implemented.
- Around line 388-395: Update nosync_publishAndApply to append objectMessages to
the existing _captured collection rather than replacing it, while preserving the
current mutex protection and success callback behavior.

In `@LiveObjects/Tests/AblyLiveObjectsTests/Mocks/MockRealtimeObjects.swift`:
- Around line 12-25: The mock’s nosync_pathObjectSubscriptionRegister currently
exposes an unusable register instead of failing clearly when accessed. Replace
the getter implementation with preconditionFailure, matching the existing
nosync_objectsPool unsupported-use pattern, and remove the unused
_pathObjectSubscriptionRegister initialization and stored property.

In `@LiveObjects/Tests/AblyLiveObjectsTests/ParentReferencesTests.swift`:
- Around line 5-8: Reconcile the ParentReferencesTests header with the existing
ParentReferencesUTSTests coverage: remove the claim that the UTS port is a
separate follow-up, and retain only native smoke cases not covered by the UTS
tests, such as clearParentReferences, to avoid duplicate behavior coverage.

In `@LiveObjects/Tests/AblyLiveObjectsTests/PublicRealtimeObjectTests.swift`:
- Around line 176-180: In the test around Self.driveToSynced, replace the fixed
Task.sleep delay with the existing deterministic main-queue drain pattern used
by RealtimeObjectUTSTests.drainMain(). Await one hop on the main callback queue
before asserting counter.isEmpty, preserving the assertion that no callback was
delivered.

In `@LiveObjects/Tests/AblyLiveObjectsTests/UTS/ObjectIdUTSTests.swift`:
- Around line 41-51: Extract the duplicated `{type}:{hash}@{timestamp}` parsing
from `objectIdFormatCounter` and `hashIsBase64urlEncoded` into a shared
`parts(of:)` helper returning type, hash, and timestamp. Update both tests to
use the helper and change the padding assertion in `hashIsBase64urlEncoded` from
`hasSuffix("=")` to rejecting any `=` with `contains`.

In `@LiveObjects/Tests/AblyLiveObjectsTests/UTS/ObjectsUTSHelpers.swift`:
- Around line 165-196: Deduplicate the shared publish holder and node factories
across the UTS helpers and DefaultInstanceTests. Promote one copy of
ObjectsUTSPublished/Published and ObjectsUTS.makeCounter/makeMap to an internal
shared helper, update both call sites to use it, and remove the redundant
private definitions while preserving their current behavior.

In
`@LiveObjects/Tests/AblyLiveObjectsTests/UTS/PathObjectSubscribeUTSTests.swift`:
- Around line 296-311: Replace the >= event-count assertions in
RTPO19c1_unlimited_depth and the corresponding MAP_CLEAR and two-apply tests
with exact expected counts, since applyAndDrain completes delivery
deterministically. Preserve the control-collector >= assertions used as
quiescence barriers; if MAP_CLEAR fan-out is intentionally variable, retain its
lower-bound assertion and add a brief explanation.

In `@LiveObjects/Tests/AblyLiveObjectsTests/UTS/RealtimeObjectUTSTests.swift`:
- Line 239: Remove the inert swiftformat and swiftlint directives from the
`#expect` assertions in the affected test cases, leaving plain
`#expect`(counter.count == 0) assertions at both locations.

In `@LiveObjects/Tests/AblyLiveObjectsTests/UTS/ValueTypesUTSTests.swift`:
- Line 59: Remove the inert swiftformat:disable and swiftlint:disable comments
from the count assertions at the referenced test locations, including the
assertion near vt.count and the corresponding one near line 114. Keep the
assertions themselves unchanged and maintain EditorConfig-only formatting.

In `@Package.swift`:
- Line 117: Remove the `.target(name: "_AblyPluginSupportPrivate")` dependency
from the `UTS` target definition in `Package.swift`, leaving the remaining UTS
dependencies unchanged.
🪄 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 Plus

Run ID: d96b3b49-2c17-436c-b200-e9b8c10610b3

📥 Commits

Reviewing files that changed from the base of the PR and between 1da1100 and 71b7d20.

📒 Files selected for processing (65)
  • LiveObjects/Sources/AblyLiveObjects/Internal/ChannelConfigGuards.swift
  • LiveObjects/Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift
  • LiveObjects/Sources/AblyLiveObjects/Internal/DefaultLiveCounterUpdate.swift
  • LiveObjects/Sources/AblyLiveObjects/Internal/DefaultLiveMapUpdate.swift
  • LiveObjects/Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift
  • LiveObjects/Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
  • LiveObjects/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
  • LiveObjects/Sources/AblyLiveObjects/Internal/InternalLiveObject.swift
  • LiveObjects/Sources/AblyLiveObjects/Internal/InternalTypes.swift
  • LiveObjects/Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift
  • LiveObjects/Sources/AblyLiveObjects/Internal/LiveObjectUpdate.swift
  • LiveObjects/Sources/AblyLiveObjects/Internal/ObjectDiffHelpers.swift
  • LiveObjects/Sources/AblyLiveObjects/Internal/ObjectsPool.swift
  • LiveObjects/Sources/AblyLiveObjects/Internal/PathObjectSubscriptionRegister.swift
  • LiveObjects/Sources/AblyLiveObjects/Path Based API/Default/DefaultLiveCounterInstance.swift
  • LiveObjects/Sources/AblyLiveObjects/Path Based API/Default/DefaultLiveCounterPathObject.swift
  • LiveObjects/Sources/AblyLiveObjects/Path Based API/Default/DefaultLiveMapInstance.swift
  • LiveObjects/Sources/AblyLiveObjects/Path Based API/Default/DefaultLiveMapPathObject.swift
  • LiveObjects/Sources/AblyLiveObjects/Path Based API/Default/DefaultPathObject.swift
  • LiveObjects/Sources/AblyLiveObjects/Path Based API/Default/DefaultPrimitiveInstance.swift
  • LiveObjects/Sources/AblyLiveObjects/Path Based API/Default/DefaultPrimitivePathObject.swift
  • LiveObjects/Sources/AblyLiveObjects/Path Based API/Default/DefaultStatusSubscription.swift
  • LiveObjects/Sources/AblyLiveObjects/Path Based API/Default/DefaultSubscription.swift
  • LiveObjects/Sources/AblyLiveObjects/Path Based API/Public/PathObject.swift
  • LiveObjects/Sources/AblyLiveObjects/Protocol/ObjectMessage.swift
  • LiveObjects/Sources/AblyLiveObjects/Protocol/SyncCursor.swift
  • LiveObjects/Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObject.swift
  • LiveObjects/Sources/AblyLiveObjects/Utility/Errors.swift
  • LiveObjects/Sources/AblyLiveObjects/Utility/PathSegments.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/DefaultInstanceTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/DefaultPathObjectTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/Helpers/UTSTestPoolFactories.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/Mocks/MockRealtimeObjects.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/ObjectDiffHelpersTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/ParentReferencesTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/PathObjectSubscriptionTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/PublicRealtimeObjectTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/SyncCursorTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/TestsOnlySeamsTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/InstanceUTSTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/InternalLiveCounterApiUTSTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/InternalLiveCounterUTSTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/InternalLiveMapApiUTSTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/InternalLiveMapParentReferencesUTSTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/InternalLiveMapUTSTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/LiveObjectSubscribeUTSTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/ObjectIdUTSTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/ObjectsPoolUTSTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/ObjectsUTSHelpers.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/ParentReferencesUTSTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/PathObjectMutationsUTSTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/PathObjectSubscribeUTSTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/PathObjectUTSTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/PublicObjectMessageUTSTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/README.md
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/RealtimeObjectUTSTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/ValueTypesUTSTests.swift
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/deviations.md
  • LiveObjects/Tests/AblyLiveObjectsTests/WireObjectMessageSizeTests.swift
  • Package.swift
  • Test/UTS/deviations.md

Comment on lines +649 to +650
// OM3d: the string length of the JSON representation of `extras` (ably-java: UTF-16 length).
let extrasSize = extras.map { JSONObjectOrArray.object($0).toJSONString.utf16.count } ?? 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

ably-java WireObjectMessage size() extras String.length byteSize

💡 Result:

In the Ably Java SDK, the WireObjectMessage.size() method is used to estimate the size of a wire-formatted message for protocol compliance, such as adhering to maximum message size limits [1][2]. Historically, internal implementations (including WireObjectMessage.kt) have faced challenges with accurate sizing when using .length on JSON-serialized strings, as this counts characters rather than bytes [1]. This approach can undercount payloads containing non-ASCII (multi-byte) characters, potentially leading to violations of byte-based protocol limits [1]. To ensure accurate sizing consistent with Ably's protocol requirements, it is recommended to replace character-based calculations with UTF-8 byte length calculations [1]. For the extras field, specifically, this involves converting the JSON-serialized representation to a UTF-8 byte array and retrieving its size [1]: extrasSize = extras?.let { gson.toJson(it).toByteArray(StandardCharsets.UTF_8).size }?: 0 While the general Ably platform documentation defines the size of extras as the string length of its JSON representation for billing purposes [3], internal SDK message sizing for pre-publish checks requires accurate byte-level calculation to prevent rejection by the server [1][2]. Developers should use byteSize (or equivalent UTF-8 byte length calculations) for fields to maintain coherence and correctly handle multi-byte characters [1][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate ObjectMessage.swift =="
fd -a 'ObjectMessage\.swift$' . | sed 's#^\./##'

echo "== relevant lines =="
file="$(fd 'ObjectMessage\.swift$' . | head -n1)"
if [ -n "${file:-}" ]; then
  wc -l "$file"
  sed -n '620,725p' "$file" | cat -n
fi

echo "== search maxMessageSize / ObjectMessage.size references =="
rg -n "ObjectMessage|extrasSize|ObjectsMap|uuid|byteSize|maxMessageSize|RTO15d|utf16|utf8" -S .

Repository: ably/ably-cocoa

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ObjectMessage.swift size algorithm =="
sed -n '624,785p' LiveObjects/Sources/AblyLiveObjects/Protocol/ObjectMessage.swift | cat -n

echo "== maxMessageSize enforcement references =="
rg -n "maxMessageSize|RTO15|RTO15d|messageSizeLimit|ObjectMessage.*size|client\.publish|publish\(" LiveObjects/Sources LiveObjects/Tests -S --max-count 120

echo "== behavioral comparison for sample strings =="
python3 - <<'PY'
samples = ["abc", "é", "¥", "😀"]
for s in samples:
    utf8 = len(s.encode("utf-8"))
    utf16 = len(s)
    print(f"{s!r}: utf8_bytes={utf8} utf16_code_units={utf16} diff={utf8-utf16}")
PY

Repository: ably/ably-cocoa

Length of output: 16767


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== WireObjectMessageSizeTests relevant non-ASCII cases =="
sed -n '1,260p' LiveObjects/Tests/AblyLiveObjectsTests/WireObjectMessageSizeTests.swift | cat -n

echo "== core/rto enforcement slice =="
sed -n '580,665p' LiveObjects/Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift | cat -n

Repository: ably/ably-cocoa

Length of output: 17126


🌐 Web query:

"ObjectMessageSizeTest" "extras" "utf16" "String.length" ably-java WireObjectMessage size

💡 Result:

In the ably-java SDK, historical implementations of WireObjectMessage.size() incorrectly used the String.length property of JSON-serialized extras [1]. Because String.length in Java/Kotlin returns the number of 16-bit char (UTF-16 code units) rather than the number of bytes in a UTF-8 encoding, this approach undercounted the size of payloads containing non-ASCII characters [1]. To ensure compliance with byte-based protocol limits, the calculation must use the UTF-8 byte count of the serialized JSON string [1]. The corrected approach, as seen in ably-java development, involves converting the GSON-serialized string to a ByteArray using StandardCharsets.UTF_8 and then retrieving the size of that array [1]: // Corrected size calculation approach: val extrasSize = extras?.let { gson.toJson(it).toByteArray(StandardCharsets.UTF_8).size }?: 0 This ensures that the extras component—which is used for metadata and ancillary payloads in messages—is accurately accounted for against the protocol's message size limits [1][2][3]. Relying on String.length remains a known issue or pitfall in legacy or non-compliant implementations because it fails to accurately represent the actual byte-size consumed on the wire [1].

Citations:


Use UTF-8 byte length for the size gate.

extrasSize and ObjectsMap entry key sizes currently use utf16.count, which undercounts non-ASCII text. This can let the RTO15d check pass for a message whose wire JSON is larger than the checked size. Count the JSON-serialized extras UTF-8 bytes here, and use UTF-8 byte length for ObjectsMap entry keys if the published limit still needs parity.

🤖 Prompt for 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.

In `@LiveObjects/Sources/AblyLiveObjects/Protocol/ObjectMessage.swift` around
lines 649 - 650, The extras size calculation in ObjectMessage must use the UTF-8
byte count of the serialized JSON instead of utf16.count. Update the relevant
ObjectsMap entry-key size calculation as well if it participates in the same
published limit, ensuring RTO15d validates actual wire JSON size for non-ASCII
content.

Comment on lines +2189 to +2219
// RTO27a1 requires the clear to emit no LiveObjectUpdate events.
// @specPartial RTO27a1 - asserts the "without emitting any LiveObjectUpdate events" clause
@Test
func clearOnDetachedEmitsNoUpdateEvents() async throws {
let internalQueue = TestFactories.createInternalQueue()
let realtimeObjects = InternalDefaultRealtimeObjectsTests.createDefaultRealtimeObjects(internalQueue: internalQueue)
Self.seedPopulatedPool(realtimeObjects, internalQueue: internalQueue)

let pool = realtimeObjects.testsOnly_objectsPool
let coreSDK = MockCoreSDK(channelState: .attached, internalQueue: internalQueue)

// Subscribe to root, the nested map and the counter.
let rootSubscriber = Subscriber<DefaultLiveMapUpdate, SubscribeResponse>(callbackQueue: .main)
let mapSubscriber = Subscriber<DefaultLiveMapUpdate, SubscribeResponse>(callbackQueue: .main)
let counterSubscriber = Subscriber<DefaultLiveCounterUpdate, SubscribeResponse>(callbackQueue: .main)
let childMap = try #require(pool.entries["map:child@1"]?.mapValue)
let childCounter = try #require(pool.entries["counter:child@2"]?.counterValue)
try pool.root.subscribe(listener: rootSubscriber.createListener(), coreSDK: coreSDK)
try childMap.subscribe(listener: mapSubscriber.createListener(), coreSDK: coreSDK)
try childCounter.subscribe(listener: counterSubscriber.createListener(), coreSDK: coreSDK)

// When the channel enters DETACHED.
internalQueue.ably_syncNoDeadlock {
realtimeObjects.nosync_onChannelStateChanged(toState: .detached, reason: nil)
}

// RTO27a1: no update events are emitted by the clear.
#expect(await rootSubscriber.getInvocations().isEmpty)
#expect(await mapSubscriber.getInvocations().isEmpty)
#expect(await counterSubscriber.getInvocations().isEmpty)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Drain the user callback queue before the negative assertion.

Subscriber delivers on .main, and nosync_onChannelStateChanged is invoked on the internal queue. The assertions at lines 2216-2218 read the invocation lists without any barrier on .main. If the implementation did emit update events, the callbacks could still be pending, so the test would pass for the wrong reason. Add a main-queue barrier, as emitsCorrectSyncEvents does at line 1867.

💚 Proposed fix
             // When the channel enters DETACHED.
             internalQueue.ably_syncNoDeadlock {
                 realtimeObjects.nosync_onChannelStateChanged(toState: .detached, reason: nil)
             }
 
+            // Give any (unwanted) callbacks a chance to run on the user callback queue.
+            await Task { `@MainActor` in }.value
+
             // RTO27a1: no update events are emitted by the clear.
             `#expect`(await rootSubscriber.getInvocations().isEmpty)
📝 Committable suggestion

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

Suggested change
// RTO27a1 requires the clear to emit no LiveObjectUpdate events.
// @specPartial RTO27a1 - asserts the "without emitting any LiveObjectUpdate events" clause
@Test
func clearOnDetachedEmitsNoUpdateEvents() async throws {
let internalQueue = TestFactories.createInternalQueue()
let realtimeObjects = InternalDefaultRealtimeObjectsTests.createDefaultRealtimeObjects(internalQueue: internalQueue)
Self.seedPopulatedPool(realtimeObjects, internalQueue: internalQueue)
let pool = realtimeObjects.testsOnly_objectsPool
let coreSDK = MockCoreSDK(channelState: .attached, internalQueue: internalQueue)
// Subscribe to root, the nested map and the counter.
let rootSubscriber = Subscriber<DefaultLiveMapUpdate, SubscribeResponse>(callbackQueue: .main)
let mapSubscriber = Subscriber<DefaultLiveMapUpdate, SubscribeResponse>(callbackQueue: .main)
let counterSubscriber = Subscriber<DefaultLiveCounterUpdate, SubscribeResponse>(callbackQueue: .main)
let childMap = try #require(pool.entries["map:child@1"]?.mapValue)
let childCounter = try #require(pool.entries["counter:child@2"]?.counterValue)
try pool.root.subscribe(listener: rootSubscriber.createListener(), coreSDK: coreSDK)
try childMap.subscribe(listener: mapSubscriber.createListener(), coreSDK: coreSDK)
try childCounter.subscribe(listener: counterSubscriber.createListener(), coreSDK: coreSDK)
// When the channel enters DETACHED.
internalQueue.ably_syncNoDeadlock {
realtimeObjects.nosync_onChannelStateChanged(toState: .detached, reason: nil)
}
// RTO27a1: no update events are emitted by the clear.
#expect(await rootSubscriber.getInvocations().isEmpty)
#expect(await mapSubscriber.getInvocations().isEmpty)
#expect(await counterSubscriber.getInvocations().isEmpty)
}
// RTO27a1 requires the clear to emit no LiveObjectUpdate events.
// `@specPartial` RTO27a1 - asserts the "without emitting any LiveObjectUpdate events" clause
`@Test`
func clearOnDetachedEmitsNoUpdateEvents() async throws {
let internalQueue = TestFactories.createInternalQueue()
let realtimeObjects = InternalDefaultRealtimeObjectsTests.createDefaultRealtimeObjects(internalQueue: internalQueue)
Self.seedPopulatedPool(realtimeObjects, internalQueue: internalQueue)
let pool = realtimeObjects.testsOnly_objectsPool
let coreSDK = MockCoreSDK(channelState: .attached, internalQueue: internalQueue)
// Subscribe to root, the nested map and the counter.
let rootSubscriber = Subscriber<DefaultLiveMapUpdate, SubscribeResponse>(callbackQueue: .main)
let mapSubscriber = Subscriber<DefaultLiveMapUpdate, SubscribeResponse>(callbackQueue: .main)
let counterSubscriber = Subscriber<DefaultLiveCounterUpdate, SubscribeResponse>(callbackQueue: .main)
let childMap = try `#require`(pool.entries["map:child@1"]?.mapValue)
let childCounter = try `#require`(pool.entries["counter:child@2"]?.counterValue)
try pool.root.subscribe(listener: rootSubscriber.createListener(), coreSDK: coreSDK)
try childMap.subscribe(listener: mapSubscriber.createListener(), coreSDK: coreSDK)
try childCounter.subscribe(listener: counterSubscriber.createListener(), coreSDK: coreSDK)
// When the channel enters DETACHED.
internalQueue.ably_syncNoDeadlock {
realtimeObjects.nosync_onChannelStateChanged(toState: .detached, reason: nil)
}
// Give any (unwanted) callbacks a chance to run on the user callback queue.
await Task { `@MainActor` in }.value
// RTO27a1: no update events are emitted by the clear.
`#expect`(await rootSubscriber.getInvocations().isEmpty)
`#expect`(await mapSubscriber.getInvocations().isEmpty)
`#expect`(await counterSubscriber.getInvocations().isEmpty)
}
🤖 Prompt for 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.

In
`@LiveObjects/Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift`
around lines 2189 - 2219, Update clearOnDetachedEmitsNoUpdateEvents to drain the
main callback queue before checking subscriber invocations: after triggering
nosync_onChannelStateChanged, add the same main-queue synchronization/barrier
pattern used by emitsCorrectSyncEvents, then retain the existing empty
assertions for rootSubscriber, mapSubscriber, and counterSubscriber.

Comment on lines +147 to +155
// Cycle suppression: add map:l as a parent of map:m via map:m, then create a back-edge
// map:l -> map:m so that the graph contains a cycle; getFullPaths must still terminate.
internalQueue.ably_syncNoDeadlock {
mapL.nosync_addParentReference(parentObjectID: "map:m@1000", key: "loop")
}
// map:l is reachable from root at ["left"]; the cycle back through map:m is suppressed.
let mapLPaths = mapL.testsOnly_getFullPaths(objectsPool: pool)
#expect(mapLPaths.contains(["left"]))
#expect(!mapLPaths.isEmpty)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Strengthen the cycle assertions and correct the comment.

The comment describes adding map:l as a parent of map:m, but line 150 adds map:m as a parent of map:l. The assertions also only prove termination: line 155 is implied by line 154, and neither rejects duplicate or extra paths. UTS/ParentReferencesUTSTests.swift (lines 348-354) asserts an exact path count for the same behaviour. Assert the count here too, so a regression that emits repeated paths fails.

💚 Proposed fix
-        // Cycle suppression: add map:l as a parent of map:m via map:m, then create a back-edge
-        // map:l -> map:m so that the graph contains a cycle; getFullPaths must still terminate.
+        // Cycle suppression: give map:l a back-edge parent of map:m, so the graph map:l -> map:m
+        // -> map:l contains a cycle; getFullPaths must terminate and emit each path once.
         internalQueue.ably_syncNoDeadlock {
             mapL.nosync_addParentReference(parentObjectID: "map:m@1000", key: "loop")
         }
         // map:l is reachable from root at ["left"]; the cycle back through map:m is suppressed.
         let mapLPaths = mapL.testsOnly_getFullPaths(objectsPool: pool)
+        `#expect`(mapLPaths.count == 1)
         `#expect`(mapLPaths.contains(["left"]))
-        `#expect`(!mapLPaths.isEmpty)
📝 Committable suggestion

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

Suggested change
// Cycle suppression: add map:l as a parent of map:m via map:m, then create a back-edge
// map:l -> map:m so that the graph contains a cycle; getFullPaths must still terminate.
internalQueue.ably_syncNoDeadlock {
mapL.nosync_addParentReference(parentObjectID: "map:m@1000", key: "loop")
}
// map:l is reachable from root at ["left"]; the cycle back through map:m is suppressed.
let mapLPaths = mapL.testsOnly_getFullPaths(objectsPool: pool)
#expect(mapLPaths.contains(["left"]))
#expect(!mapLPaths.isEmpty)
// Cycle suppression: give map:l a back-edge parent of map:m, so the graph map:l -> map:m
// -> map:l contains a cycle; getFullPaths must terminate and emit each path once.
internalQueue.ably_syncNoDeadlock {
mapL.nosync_addParentReference(parentObjectID: "map:m@1000", key: "loop")
}
// map:l is reachable from root at ["left"]; the cycle back through map:m is suppressed.
let mapLPaths = mapL.testsOnly_getFullPaths(objectsPool: pool)
`#expect`(mapLPaths.count == 1)
`#expect`(mapLPaths.contains(["left"]))
🤖 Prompt for 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.

In `@LiveObjects/Tests/AblyLiveObjectsTests/ParentReferencesTests.swift` around
lines 147 - 155, Correct the cycle-suppression comment to match the parent
relationship created by nosync_addParentReference, then strengthen the mapL
cycle test around testsOnly_getFullPaths by asserting the exact expected path
count, following the corresponding UTS/ParentReferencesUTSTests behavior. Remove
the redundant non-empty assertion while retaining the expected ["left"] path
check.

Comment on lines +56 to +65
- **DEV-5** — Public enums drop `UNKNOWN`. `ObjectOperationAction` has exactly 7 cases (`.mapCreate`,
`.mapSet`, `.mapRemove`, `.counterCreate`, `.counterInc`, `.objectDelete`, `.mapClear`) and
`ObjectsMapSemantics` has only `.lww`. The spec's `action == "MAP_SET"` maps to `.mapSet`, etc.
Unknown wire codes are held internally by `WireEnum.unknown` and never surface publicly. Asserted by
`test_DEV5_object_operation_action_seven_distinct_cases`.
- **DEV-6** — `ObjectData` shape. The public `ObjectData` adds a Swift-only `encoding: String?` (no
wire/Java counterpart), exposes `json` as a raw `String?` (vs Java's parsed `JsonElement`), and uses
non-optional `Double` for `CounterCreate.count` / `CounterInc.number` (`number` on `ObjectData` is
`Double?`). Asserted by `test_ObjectData_holds_typed_values`, `test_PAOOP2_counter_inc_only_relevant_field`
and `test_PAOOP2_counter_create_with_count`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the referenced test-method names.

The document names tests with a test_ prefix, for example test_DEV5_object_operation_action_seven_distinct_cases and test_ObjectData_holds_typed_values. The Swift Testing methods in UTS/PublicObjectMessageUTSTests.swift are DEV5_object_operation_action_seven_distinct_cases and objectData_holds_typed_values. Line 126 has the same problem with InstanceUTSTests.test_RTINS14a.... Update the names so readers can locate the tests.

🤖 Prompt for 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.

In `@LiveObjects/Tests/AblyLiveObjectsTests/UTS/deviations.md` around lines 56 -
65, Update the test references in deviations.md to match the actual Swift
Testing method names: remove the test_ prefix from
DEV5_object_operation_action_seven_distinct_cases and
objectData_holds_typed_values, and correct the similarly prefixed
InstanceUTSTests.test_RTINS14a... reference on line 126. Preserve the
surrounding DEV-5 and DEV-6 descriptions.

Comment on lines +262 to +263
</content>
</invoke>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Generated Markdown files contain leaked closing tags. Three documentation files end with tool artifacts instead of content.

  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/deviations.md#L262-L263: delete the </content> and </invoke> lines.
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/README.md#L32: delete the </content> line.
  • Test/UTS/deviations.md#L35: delete the </content> line.
📍 Affects 3 files
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/deviations.md#L262-L263 (this comment)
  • LiveObjects/Tests/AblyLiveObjectsTests/UTS/README.md#L32-L32
  • Test/UTS/deviations.md#L35-L35
🤖 Prompt for 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.

In `@LiveObjects/Tests/AblyLiveObjectsTests/UTS/deviations.md` around lines 262 -
263, Remove the leaked tool-artifact closing tags from all affected
documentation files: delete both closing tags at
LiveObjects/Tests/AblyLiveObjectsTests/UTS/deviations.md lines 262-263, and
delete the closing content tag at
LiveObjects/Tests/AblyLiveObjectsTests/UTS/README.md line 32 and
Test/UTS/deviations.md line 35.

…integration testing

Implements the remaining core-accessor-gated functionality, ported from
ably-java, after enabling the LiveObjects integration test suites:

- RTL33 implicit attach: new plugin-API attach primitive
  (APPluginAPI nosync_attachChannel:) wired through CoreSDK into
  RealtimeObject.get() - full RTL33a/b/b1/c state handling
- Real maxMessageSize on the RTO15d publish size gate (65536 fallback)
- Real channel name threaded to the public ObjectMessage (PAOM3)
- Channel-config guards fully implemented: object channel modes (40024),
  echoMessages, connection-active checks - no core-accessor stubs remain
- Channel-release SPI (nosync_releaseChannel:) so releasing a channel
  fails in-flight waiters with a release-specific cause
- Fix: engine deinit on the internal queue no longer trips the
  DispatchQueueMutex precondition (SIGTRAP during ARC-driven teardown)
- Fix: siteCode from connectionDetails is now seeded into engines
  created after CONNECTED, restoring RTO20 local echo

All changes are covered by new native tests. Verified end-to-end by the
LiveObjects integration suites (17/17 against the Ably sandbox).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant