Skip to content

Conversation

@lawrence-forooghian
Copy link
Collaborator

@lawrence-forooghian lawrence-forooghian commented Sep 2, 2025

Isolate the plugin's mutable state to the ARTRealtime instance's internal queue, for consistency with ably-cocoa.

Will unpin ably-cocoa and ably-cocoa-plugin-support before our next release.

Related:

Resolves #3.

Summary by CodeRabbit

  • Refactor

    • Concurrency rewritten to an explicit internal-queue model with safer mutex primitives and queue-based synchronization; behaviour preserved.
  • Documentation

    • Added Threading guidelines covering thread-safety, callback ordering, and a deadlock-avoidance sync helper (duplicate section noted).
  • Chores

    • Dependency pins and lockfiles updated for deterministic builds.
  • Tests

    • Test harness and helpers updated to use per-test internal queues and new test-only accessors to align with the concurrency changes.

@coderabbitai
Copy link

coderabbitai bot commented Sep 2, 2025

Walkthrough

Switches internal concurrency to a DispatchQueue-based model: adds DispatchQueueMutex and ably_syncNoDeadlock, threads an internalQueue through core/plugin/objects and tests, introduces many nosync_ and testsOnly_ fast-paths/accessors, and pins two dependencies to exact git revisions. CONTRIBUTING.md threading guidance was inserted twice.

Changes

Cohort / File(s) Summary
Dependency pin updates
Package.swift, Package.resolved, AblyLiveObjects.xcworkspace/xcshareddata/swiftpm/Package.resolved
Pin ably-cocoa and ably-cocoa-plugin-support to explicit git revisions; update lockfiles (originHash, remove version fields).
Threading documentation
CONTRIBUTING.md
Add "Threading" subsection describing queue model, nosync conventions, and ably_syncNoDeadlock; the subsection appears twice (duplicate insertion).
DispatchQueue helpers
Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift, Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift
Add ably_syncNoDeadlock DispatchQueue extensions and a generic DispatchQueueMutex<T> with withSync/withoutSync patterns and runtime preconditions.
Core SDK surface
Sources/.../CoreSDK.swift, Tests/.../Mocks/MockCoreSDK.swift
Replace channelStatenosync_channelState; validateChannelStatenosync_validateChannelState; MockCoreSDK now uses DispatchQueueMutex and accepts an internalQueue.
Plugin wiring & send path
Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift, Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift
Add nosync_realtimeObjects/nosync_prepare and other nosync_ handlers; obtain per-client internalQueue from plugin API and run nosync_realtimeObjects on it; switch sendObjectnosync_sendObject with completion precondition on internalQueue.
Realtime core & mutable state
Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift, Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
Replace NSLock with mutableStateMutex: DispatchQueueMutex<MutableState>; require internalQueue in initializer; migrate many flows to nosync_ variants and thread internalQueue through pool, creation, GC, and message handling; add UpdateMutableState alias and update on(event:) signature.
Objects pool & object internals
Sources/AblyLiveObjects/Internal/ObjectsPool.swift, Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift, Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift, Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift, Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift, Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift
Replace NSLock with DispatchQueueMutex patterns, add internalQueue parameters, introduce many nosync_ and testsOnly_ accessors (objectID/isTombstone/tombstonedAt), and propagate internalQueue through create/merge/apply/emit/GC and helper paths.
Public API wiring (runtime path)
Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift
When constructing public objects handle, retrieve per-client internalQueue and synchronously run DefaultInternalPlugin.nosync_realtimeObjects on it.
Tests, factories & mocks
Tests/AblyLiveObjectsTests/*, Tests/.../Mocks/*
Add TestFactories.createInternalQueue; plumb internalQueue into MockCoreSDK, MockLiveMapObjectPoolDelegate, ObjectsPool, InternalDefault* factories; update tests to call nosync_ variants under internalQueue sync and to use testsOnly_ accessors; many test signatures updated to accept internalQueue.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App
  participant Public as ARTRealtimeChannel+Objects
  participant SDK as PluginAPI
  participant Q as internalQueue
  participant Plugin as DefaultInternalPlugin
  participant Core as InternalDefaultRealtimeObjects
  participant Pool as ObjectsPool

  Note over App,Public: Acquire objects handle
  App->>Public: request objects
  Public->>SDK: internalQueue(for: client)
  SDK-->>Public: internalQueue
  Public->>Q: sync { Plugin.nosync_realtimeObjects(channel, pluginAPI) }
  Q-->>Public: InternalDefaultRealtimeObjects
  Public-->>App: Objects handle

  Note over App,Core: Mutate (e.g., increment)
  App->>Core: increment()
  Core->>Q: dispatchPrecondition(.onQueue) / ensure on internalQueue
  Core->>Pool: nosync_buildMessage / nosync_apply(...)
  Pool->>SDK: nosync_sendObject(message, completion)
  SDK-->>Q: completion (runs on internalQueue)
  alt success
    Q-->>Core: success callback
  else failure
    Q-->>Core: error callback
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

"I hop through queues where updates flow,
A serial stream where states now go.
With nosync charms and mutex bright,
I guard my carrots, left and right.
Pins snug in place — the burrow’s set. 🥕✨"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The pull request contains out-of-scope updates to dependency version pins in Package.swift and Package.resolved that are unrelated to selecting or documenting the threading model, and the Threading subsection in CONTRIBUTING.md is duplicated appearing twice, suggesting unintended documentation duplication beyond the linked issue’s objectives. Extract the dependency pinning and lock-file updates into a separate changeset and remove the duplicated Threading subsection from CONTRIBUTING.md so that only the core threading documentation remains.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title succinctly and accurately describes the primary change of finalising and documenting the threading approach, matching the implementation and documentation scope without extraneous detail beyond the ticket reference.
Linked Issues Check ✅ Passed The pull request implements a queue-based threading and concurrency model by synchronizing mutable plugin state on an internal DispatchQueue and invoking callbacks on the ARTRealtime queue #3, renames internal methods to nosync_ variants to enforce isolation, and documents the threading approach in CONTRIBUTING.md, thus fulfilling the linked issue’s requirement to decide and implement a threading strategy.
Docstring Coverage ✅ Passed Docstring coverage is 95.45% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 3-decide-how-threading-will-work

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b86e09a and 5a63313.

📒 Files selected for processing (1)
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift (22 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.swift

📄 CodeRabbit inference engine (.cursor/rules/swift.mdc)

**/*.swift: Specify an explicit access control level (SwiftLint explicit_acl) for all declarations in Swift code (tests are exempt)
When extending a type, put the access level on the extension declaration rather than on each member (tests are exempt)
Prefer implicit .init(...) when the type can be inferred in initializer expressions
Prefer enum case shorthand (.caseName) when the type can be inferred
For JSONValue or WireValue, prefer using literal syntax via ExpressibleBy*Literal where possible
Prefer Swift raw string literals for JSON strings instead of escaping double quotes
When an array literal begins with an initializer expression, place the initializer on the line after the opening bracket

Files:

  • Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift
Tests/**/*.swift

📄 CodeRabbit inference engine (.cursor/rules/testing.mdc)

Tests/**/*.swift: Use the Swift Testing framework (import Testing), not XCTest, in test files
Do not use fatalError for expectation failures; prefer Swift Testing’s #require
Only add labels to test cases or suites when the label differs from the suite struct or test method name
Tag tests per CONTRIBUTING.md’s "Attributing tests to a spec point" with exact comment format; distinguish @spec vs @specPartial; do not repeat @spec for the same spec point
Add comments in tests to clarify when certain test data is irrelevant to the scenario
In tests, import Ably using import Ably
In tests, import AblyLiveObjects using @testable import AblyLiveObjects
In tests, import _AblyPluginSupportPrivate using import _AblyPluginSupportPrivate (do not use internal import)
When passing a logger to internal components in tests, use TestLogger()
When unwrapping optionals in tests, prefer #require over guard let

Files:

  • Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift
🧬 Code graph analysis (1)
Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift (4)
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (6)
  • createInternalQueue (688-691)
  • counterObjectState (229-244)
  • objectOperation (336-356)
  • counterCreateOperation (374-383)
  • counterOp (386-388)
  • mapCreateOperation (359-371)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (5)
  • createZeroValued (66-81)
  • nosync_replaceData (211-224)
  • value (100-104)
  • nosync_mergeInitialValue (227-231)
  • nosync_apply (248-267)
Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift (2)
  • ably_syncNoDeadlock (5-8)
  • ably_syncNoDeadlock (11-14)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (2)
  • nosync_replaceData (82-107)
  • nosync_apply (33-58)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
  • GitHub Check: Xcode, release configuration, macOS (Xcode 16.3)
  • GitHub Check: Example app, macOS (Xcode 16.3)
  • GitHub Check: Example app, tvOS (Xcode 16.3)
  • GitHub Check: Xcode, macOS (Xcode 16.3)
  • GitHub Check: Example app, iOS (Xcode 16.3)
  • GitHub Check: Xcode, release configuration, tvOS (Xcode 16.3)
  • GitHub Check: Xcode, tvOS (Xcode 16.3)
  • GitHub Check: Xcode, iOS (Xcode 16.3)
  • GitHub Check: Xcode, release configuration, iOS (Xcode 16.3)
  • GitHub Check: SPM (Xcode 16.3)
  • GitHub Check: SPM, release configuration (Xcode 16.3)
  • GitHub Check: Generate code coverage
  • GitHub Check: check-documentation
🔇 Additional comments (6)
Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift (6)

14-14: LGTM: Consistent queue creation pattern

The consistent use of TestFactories.createInternalQueue() across all test methods properly supports the new threading model.

Also applies to: 33-33, 52-52, 70-70, 103-103, 119-119, 138-138, 161-161, 188-188, 217-217, 236-236, 263-263, 307-307, 334-334, 377-377, 416-416, 461-461, 492-492, 515-515, 537-537, 566-566, 591-591


15-15: LGTM: Proper parameter threading

All InternalDefaultLiveCounter.createZeroValued calls correctly include the internalQueue parameter, maintaining consistency with the new concurrency model.

Also applies to: 34-34, 53-53, 72-72, 104-104, 120-120, 139-139, 162-162, 189-189, 218-218, 237-237, 264-264, 308-308, 335-335, 378-378, 417-417, 462-462, 493-493, 516-516, 538-538, 567-567, 592-592


16-16: LGTM: Test doubles properly updated

All MockCoreSDK constructor calls correctly include the internalQueue parameter to support the new threading model.

Also applies to: 35-35, 105-105, 121-121, 140-140, 163-163, 190-190, 238-238, 265-265, 309-309, 336-336, 379-379, 418-418, 463-463, 494-494, 517-517, 539-539, 568-568, 593-593


38-40: LGTM: Proper synchronization patterns

All counter state mutations are correctly wrapped in internalQueue.ably_syncNoDeadlock blocks, ensuring proper serialization and deadlock prevention.

Also applies to: 57-59, 79-81, 91-93, 109-111, 122-126, 145-147, 166-168, 173-175, 193-195, 203-205, 222-224, 241-244, 268-270, 312-314, 339-344, 353-361, 388-396, 424-426, 436-444, 470-478


39-39: LGTM: Correct nosync_ method usage

The switch to nosync_ prefixed methods (like nosync_replaceData, nosync_mergeInitialValue, nosync_apply) correctly reflects that synchronization is handled by the caller via the queue wrapper.

Also applies to: 58-58, 80-80, 92-92, 110-110, 123-123, 146-146, 167-167, 174-174, 194-194, 204-204, 223-223, 242-242, 243-243, 269-269, 313-313, 340-340, 354-354, 389-389, 425-425, 437-437, 471-471


350-350: LGTM: ObjectsPool properly updated

All ObjectsPool constructor calls correctly include the internalQueue parameter, maintaining consistency with the new concurrency model.

Also applies to: 385-385, 433-433, 469-469

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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

❤️ Share

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

@lawrence-forooghian lawrence-forooghian changed the title wip of threading on internal queue [ECO-5377] Finalise and document threading approach Sep 2, 2025
@lawrence-forooghian lawrence-forooghian force-pushed the 3-decide-how-threading-will-work branch from 17fbbf3 to b125c6e Compare September 2, 2025 13:59
@github-actions github-actions bot temporarily deployed to staging/pull/85/AblyLiveObjects September 2, 2025 14:01 Inactive
@lawrence-forooghian lawrence-forooghian force-pushed the 3-decide-how-threading-will-work branch from b125c6e to 55b834b Compare September 2, 2025 20:28
@github-actions github-actions bot temporarily deployed to staging/pull/85/AblyLiveObjects September 2, 2025 20:30 Inactive
@lawrence-forooghian lawrence-forooghian marked this pull request as ready for review September 2, 2025 20:37
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (3)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (1)

275-376: Critical: Set subtraction uses unsupported + between Set and Array.

Line 362 subtracting(receivedObjectIds + [Self.rootKey]) likely doesn’t compile unless you have a custom overload. Use union.

Apply:

-        let objectIdsToRemove = Set(entries.keys).subtracting(receivedObjectIds + [Self.rootKey])
+        let objectIdsToRemove = Set(entries.keys).subtracting(receivedObjectIds.union([Self.rootKey]))
Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift (2)

16-22: Data race on _publishHandler read in publish — guard with the lock.

Copy the handler under the lock, then call it outside.

-    func publish(objectMessages: [OutboundObjectMessage]) async throws(InternalError) {
-        if let handler = _publishHandler {
-            try await handler(objectMessages)
-        } else {
-            protocolRequirementNotImplemented()
-        }
-    }
+    func publish(objectMessages: [OutboundObjectMessage]) async throws(InternalError) {
+        let handler = mutex.withLock { _publishHandler }
+        guard let handler else {
+            protocolRequirementNotImplemented()
+            return
+        }
+        try await handler(objectMessages)
+    }

24-26: Keep backwards-compatibility: implement testsOnly_overridePublish via setPublishHandler.

Prevents unexpected protocolRequirementNotImplemented() in tests still using this API.

-    func testsOnly_overridePublish(with _: @escaping ([OutboundObjectMessage]) async throws(InternalError) -> Void) {
-        protocolRequirementNotImplemented()
-    }
+    func testsOnly_overridePublish(with handler: @escaping ([OutboundObjectMessage]) async throws(InternalError) -> Void) {
+        setPublishHandler(handler)
+    }
🧹 Nitpick comments (19)
CONTRIBUTING.md (1)

73-82: Tiny grammar fix in Threading section.

Change “invoked on same queue as used by” → “invoked on the same queue used by”.

-2. Callbacks passed to the public API are invoked on same queue as used by the `ARTRealtime` instance (the `dispatchQueue` client option)
+2. Callbacks passed to the public API are invoked on the same queue used by the `ARTRealtime` instance (the `dispatchQueue` client option)
Sources/AblyLiveObjects/Internal/CoreSDK.swift (1)

99-112: Helpful diagnostics on invalid state.

Consider logging operationDescription and current state before throwing to aid field debugging.

Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (1)

25-37: Optional: rethrows(E) to improve call ergonomics.

If you want non-throwing closures not to force try, switch to rethrows(E).

Apply:

-    internal func withLock<R, E>(_ body: (inout T) throws(E) -> R) throws(E) -> R {
+    internal func withLock<R, E>(_ body: (inout T) throws(E) -> R) rethrows(E) -> R {
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (2)

82-107: Remove unused userCallbackQueue parameter from Entry.nosync_replaceData.

It’s unused in both branches; keeping it adds noise and risks drift.

Apply:

-        fileprivate func nosync_replaceData(
+        fileprivate func nosync_replaceData(
             using state: ObjectState,
             objectMessageSerialTimestamp: Date?,
             objectsPool: inout ObjectsPool,
-            userCallbackQueue: DispatchQueue,
         ) -> DeferredUpdate {
@@
-                let deferredUpdate = existingEntry.nosync_replaceData(
+                let deferredUpdate = existingEntry.nosync_replaceData(
                     using: syncObjectsPoolEntry.state,
                     objectMessageSerialTimestamp: syncObjectsPoolEntry.objectMessageSerialTimestamp,
                     objectsPool: &self,
-                    userCallbackQueue: userCallbackQueue,
                 )

Also applies to: 299-305


128-146: Remove comments on testsOnly_ accessors per repo guidelines.

Guideline: don’t add generic explanatory comments for testsOnly_ properties.

Apply:

-        /// Test-only accessor for isTombstone that handles locking internally.
         internal var testsOnly_isTombstone: Bool {
@@
-        /// Test-only accessor for tombstonedAt that handles locking internally.
         internal var testsOnly_tombstonedAt: Date? {
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (2)

114-119: Remove explanatory comments from testsOnly_ accessors (per repo guideline).

Guideline: “For testsOnly_ property declarations, do not add generic explanatory comments.” Strip the doc comments from these accessors.

-    /// Test-only accessor for objectID that handles locking internally.
     internal var testsOnly_objectID: String {
         mutableStateMutex.withLock { mutableState in
             mutableState.liveObjectMutableState.objectID
         }
     }

Apply similarly to testsOnly_isTombstone and testsOnly_tombstonedAt.

Also applies to: 406-411, 420-425


895-921: Minor: prefer lazy filter/map to avoid intermediate array growth in entries().

Not critical, but you could tighten allocation a bit.

-            var result: [(key: String, value: InternalLiveMapValue)] = []
-
-            for (key, entry) in data where !Self.nosync_isEntryTombstoned(entry, delegate: delegate) {
-                // Convert entry to LiveMapValue using the same logic as get(key:)
-                if let value = nosync_convertEntryToLiveMapValue(entry, delegate: delegate) {
-                    result.append((key: key, value: value))
-                }
-            }
-
-            return result
+            return data.lazy
+                .filter { !Self.nosync_isEntryTombstoned($0.value, delegate: delegate) }
+                .compactMap { key, entry in
+                    nosync_convertEntryToLiveMapValue(entry, delegate: delegate).map { (key: key, value: $0) }
+                }
Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift (1)

123-129: Add explicit ACLs inside the private nested type (explicit_acl).

Even within a private type, declare member ACLs explicitly to satisfy the guideline.

-    private struct LifecycleEventResponse: OnLiveObjectLifecycleEventResponse {
-        let subscription: any SubscribeResponse
+    private struct LifecycleEventResponse: OnLiveObjectLifecycleEventResponse {
+        private let subscription: any SubscribeResponse

-        func off() {
+        internal func off() {
             subscription.unsubscribe()
         }
     }
Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (2)

37-53: Prepare path: consider asserting queue expectations

The method is prefixed nosync_ yet it captures internalQueue and writes via nosync_setPluginDataValue. If the contract requires running on the internalQueue, add a debug precondition; otherwise, a short comment clarifying that nosync here means “no internal locking required” would help future readers.


149-156: Minor: simplify continuation with throwing variant

You can avoid Result plumbing by using withCheckedThrowingContinuation and throwing directly from the completion.

Proposed refactor:

-        try await withCheckedContinuation { (continuation: CheckedContinuation<Result<Void, InternalError>, _>) in
+        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
             let internalQueue = pluginAPI.internalQueue(for: client)

             internalQueue.async {
                 pluginAPI.nosync_sendObject(
                     withObjectMessages: objectMessageBoxes,
                     channel: channel,
                 ) { error in
                     // We don't currently rely on this documented behaviour of `nosync_sendObject` but we may do later, so assert it to be sure it's happening.
                     dispatchPrecondition(condition: .onQueue(internalQueue))

-                    if let error {
-                        continuation.resume(returning: .failure(error.toInternalError()))
-                    } else {
-                        continuation.resume(returning: .success(()))
-                    }
+                    if let error { continuation.resume(throwing: error.toInternalError()) }
+                    else { continuation.resume() }
                 }
             }
-        }.get()
+        }
Tests/AblyLiveObjectsTests/Mocks/MockLiveMapObjectPoolDelegate.swift (1)

12-19: Document access constraints to prevent misuse.

Clarify that objects must not be accessed on internalQueue; use nosync APIs instead.

-    var objects: [String: ObjectsPool.Entry] {
+    /// Access only off `internalQueue`. When already on `internalQueue`, use `nosync_` accessors.
+    var objects: [String: ObjectsPool.Entry] {
Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift (2)

33-54: Wrap nosync operation on internalQueue; consider a small helper to reduce repetition.

Pattern is correct; a tiny helper would keep tests DRY and avoid accidental re-entrancy.

Example helper (in TestFactories):

+extension DispatchQueue {
+    @inlinable
+    func syncReturn<T>(_ body: () -> T) -> T { sync { body() } }
+}

Usage:

-            let creationOperation = internalQueue.sync {
+            let creationOperation = internalQueue.syncReturn {
                 ObjectCreationHelpers.nosync_creationOperationForLiveMap(…
             }

7-16: Duplicate @SPEC tag(s).

Avoid repeating the same @spec. Remove the duplicate RTO11f4c1a.

-        // @spec RTO11f4c1a
-        // @spec RTO11f4c1a
+        // @spec RTO11f4c1a
Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift (1)

352-361: Correct spec comment (will be discarded, not applied) and typo

The comment contradicts the test intent. With ts1 < ts2, canApplyOperation should return nil and the op is discarded. Also fix “RTOL4a” → “RTLO4a”.

-            // Apply operation with serial "ts1" which is lexicographically less than existing "ts2" and thus will be applied per RTLO4a (this is a non-pathological case of RTOL4a, that spec point being fully tested elsewhere)
+            // Apply operation with serial "ts1" which is lexicographically less than existing "ts2" and thus will be discarded per RTLO4a (this is a non-pathological case of RTLO4a, that spec point being fully tested elsewhere)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (2)

451-455: Nit: spec tag typo

Comment says RTL9e; should be RTLC9e.

-                // RTL9e
+                // RTLC9e

351-353: Track TODO with an issue

Unresolved spec detail in RTLC6d. Please link an issue and reference it here to avoid TODO drift.

I can open a tracking issue and propose wording based on the current spec discussion if helpful.

Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift (2)

351-355: Fix tombstone comments to match setup

The comments contradict the data (Date() means tombstoned).

Apply:

-                    // tombstonedAt is false, so not considered tombstoned
-                    "tombstoned": TestFactories.internalMapEntry(tombstonedAt: Date(), data: ObjectData(string: "tombstoned")),
+                    // tombstonedAt is set, so considered tombstoned
+                    "tombstoned": TestFactories.internalMapEntry(tombstonedAt: Date(), data: ObjectData(string: "tombstoned")),
-                    "tombstoned2": TestFactories.internalMapEntry(tombstonedAt: Date(), data: ObjectData(string: "tombstoned2")),
+                    "tombstoned2": TestFactories.internalMapEntry(tombstonedAt: Date(), data: ObjectData(string: "tombstoned2")),

1122-1124: Correct spec tag typos (RTLO4a/RTOL4a → RTO4a)

Keeps spec references consistent.

Apply:

-            // Apply operation with serial "ts1" which is lexicographically less than existing "ts2" and thus will be applied per RTLO4a (this is a non-pathological case of RTOL4a, that spec point being fully tested elsewhere)
+            // Apply operation with serial "ts1" which is lexicographically less than existing "ts2" and thus will be applied per RTO4a (this is a non-pathological case of RTO4a, that spec point being fully tested elsewhere)
Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift (1)

1-3: Import Ably in tests per project guidelines

Keeps test files consistent with the stated testing imports policy.

Apply:

 import _AblyPluginSupportPrivate
+import Ably
 @testable import AblyLiveObjects
 import Testing
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5c6ff74 and 55b834b.

📒 Files selected for processing (25)
  • AblyLiveObjects.xcworkspace/xcshareddata/swiftpm/Package.resolved (1 hunks)
  • CONTRIBUTING.md (1 hunks)
  • Package.resolved (1 hunks)
  • Package.swift (1 hunks)
  • Sources/AblyLiveObjects/Internal/CoreSDK.swift (3 hunks)
  • Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (4 hunks)
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (9 hunks)
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (32 hunks)
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (22 hunks)
  • Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift (2 hunks)
  • Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift (1 hunks)
  • Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift (1 hunks)
  • Sources/AblyLiveObjects/Internal/ObjectsPool.swift (22 hunks)
  • Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift (1 hunks)
  • Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (1 hunks)
  • Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (1 hunks)
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift (22 hunks)
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift (51 hunks)
  • Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift (53 hunks)
  • Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift (3 hunks)
  • Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift (8 hunks)
  • Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift (2 hunks)
  • Tests/AblyLiveObjectsTests/Mocks/MockLiveMapObjectPoolDelegate.swift (1 hunks)
  • Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift (1 hunks)
  • Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift (17 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.swift

📄 CodeRabbit inference engine (.cursor/rules/swift.mdc)

**/*.swift: Specify an explicit access control level (SwiftLint explicit_acl) for all declarations in Swift code (tests are exempt)
When extending a type, put the access level on the extension declaration rather than on each member (tests are exempt)
Prefer implicit .init(...) when the type can be inferred in initializer expressions
Prefer enum case shorthand (.caseName) when the type can be inferred
For JSONValue or WireValue, prefer using literal syntax via ExpressibleBy*Literal where possible
Prefer Swift raw string literals for JSON strings instead of escaping double quotes
When an array literal begins with an initializer expression, place the initializer on the line after the opening bracket

Files:

  • Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift
  • Package.swift
  • Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift
  • Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift
  • Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift
  • Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift
  • Tests/AblyLiveObjectsTests/Mocks/MockLiveMapObjectPoolDelegate.swift
  • Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift
  • Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift
  • Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift
  • Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift
  • Sources/AblyLiveObjects/Internal/CoreSDK.swift
  • Sources/AblyLiveObjects/Internal/ObjectsPool.swift
  • Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift
  • Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift
  • Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
Tests/**/*.swift

📄 CodeRabbit inference engine (.cursor/rules/testing.mdc)

Tests/**/*.swift: Use the Swift Testing framework (import Testing), not XCTest, in test files
Do not use fatalError for expectation failures; prefer Swift Testing’s #require
Only add labels to test cases or suites when the label differs from the suite struct or test method name
Tag tests per CONTRIBUTING.md’s "Attributing tests to a spec point" with exact comment format; distinguish @spec vs @specPartial; do not repeat @spec for the same spec point
Add comments in tests to clarify when certain test data is irrelevant to the scenario
In tests, import Ably using import Ably
In tests, import AblyLiveObjects using @testable import AblyLiveObjects
In tests, import _AblyPluginSupportPrivate using import _AblyPluginSupportPrivate (do not use internal import)
When passing a logger to internal components in tests, use TestLogger()
When unwrapping optionals in tests, prefer #require over guard let

Files:

  • Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift
  • Tests/AblyLiveObjectsTests/Mocks/MockLiveMapObjectPoolDelegate.swift
  • Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift
  • Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift
  • Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift
  • Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift
  • Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift
Sources/AblyLiveObjects/**/*.swift

📄 CodeRabbit inference engine (.cursor/rules/swift.mdc)

In AblyLiveObjects library (non-test) code, import modules as: Ably with import Ably, and _AblyPluginSupportPrivate with internal import _AblyPluginSupportPrivate

Files:

  • Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift
  • Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift
  • Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift
  • Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift
  • Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift
  • Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift
  • Sources/AblyLiveObjects/Internal/CoreSDK.swift
  • Sources/AblyLiveObjects/Internal/ObjectsPool.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
Sources/**/*.swift

📄 CodeRabbit inference engine (.cursor/rules/testing.mdc)

For testsOnly_ property declarations, do not add generic explanatory comments (their meaning is understood)

Files:

  • Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift
  • Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift
  • Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift
  • Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift
  • Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift
  • Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift
  • Sources/AblyLiveObjects/Internal/CoreSDK.swift
  • Sources/AblyLiveObjects/Internal/ObjectsPool.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
🧠 Learnings (5)
📚 Learning: 2025-08-26T11:32:54.222Z
Learnt from: CR
PR: ably/ably-liveobjects-swift-plugin#0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-08-26T11:32:54.222Z
Learning: Applies to Tests/**/*.swift : Only add labels to test cases or suites when the label differs from the suite struct or test method name

Applied to files:

  • Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift
📚 Learning: 2025-08-26T11:32:26.789Z
Learnt from: CR
PR: ably/ably-liveobjects-swift-plugin#0
File: .cursor/rules/swift.mdc:0-0
Timestamp: 2025-08-26T11:32:26.789Z
Learning: Applies to Sources/AblyLiveObjects/**/*.swift : In AblyLiveObjects library (non-test) code, import modules as: Ably with `import Ably`, and _AblyPluginSupportPrivate with `internal import _AblyPluginSupportPrivate`

Applied to files:

  • AblyLiveObjects.xcworkspace/xcshareddata/swiftpm/Package.resolved
  • Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift
  • Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift
  • Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift
📚 Learning: 2025-08-26T11:32:54.222Z
Learnt from: CR
PR: ably/ably-liveobjects-swift-plugin#0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-08-26T11:32:54.222Z
Learning: Applies to Tests/**/*.swift : In tests, import AblyLiveObjects using `testable import AblyLiveObjects`

Applied to files:

  • Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift
  • Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift
  • Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift
📚 Learning: 2025-08-26T11:32:54.222Z
Learnt from: CR
PR: ably/ably-liveobjects-swift-plugin#0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-08-26T11:32:54.222Z
Learning: Applies to Tests/**/*.swift : In tests, import `_AblyPluginSupportPrivate` using `import _AblyPluginSupportPrivate` (do not use `internal import`)

Applied to files:

  • Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift
📚 Learning: 2025-08-22T20:27:49.043Z
Learnt from: lawrence-forooghian
PR: ably/ably-liveobjects-swift-plugin#79
File: Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift:246-258
Timestamp: 2025-08-22T20:27:49.043Z
Learning: In Swift, `mutex.withLock { ... }` closures with single expressions automatically return the result of that expression, so explicit return statements are not needed when the closure contains only a call like `mutableState.on(...)` that returns the expected type.

Applied to files:

  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
🧬 Code graph analysis (17)
Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (2)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (1)
  • value (100-104)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (1)
  • get (124-128)
Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift (1)
Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (2)
  • nosync_realtimeObjects (24-32)
  • nosync_realtimeObjects (56-58)
Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift (1)
Sources/AblyLiveObjects/Internal/CoreSDK.swift (1)
  • nosync_validateChannelState (99-111)
Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift (3)
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (6)
  • createInternalQueue (688-691)
  • counterObjectState (229-244)
  • objectOperation (336-356)
  • counterCreateOperation (374-383)
  • counterOp (386-388)
  • mapCreateOperation (359-371)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (5)
  • createZeroValued (66-81)
  • nosync_replaceData (210-223)
  • value (100-104)
  • nosync_mergeInitialValue (226-230)
  • nosync_apply (247-266)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (2)
  • nosync_replaceData (82-107)
  • nosync_apply (33-58)
Tests/AblyLiveObjectsTests/Mocks/MockLiveMapObjectPoolDelegate.swift (2)
Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (2)
  • withLock (25-37)
  • withoutLock (45-49)
Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (1)
  • nosync_getObjectFromPool (144-148)
Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift (1)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (1)
  • entries (136-140)
Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift (5)
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (1)
  • createInternalQueue (688-691)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (4)
  • createZeroValued (66-81)
  • nosync_replaceData (210-223)
  • nosync_mergeInitialValue (226-230)
  • nosync_apply (247-266)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (6)
  • createZeroValued (87-104)
  • get (124-128)
  • nosync_replaceData (276-292)
  • nosync_mergeInitialValue (295-306)
  • nosync_apply (323-343)
  • set (152-182)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (2)
  • nosync_replaceData (82-107)
  • nosync_apply (33-58)
Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift (1)
  • setPublishHandler (33-37)
Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (1)
Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (6)
  • nosync_onChannelAttached (293-301)
  • nosync_onChannelAttached (450-476)
  • nosync_handleObjectProtocolMessage (308-319)
  • nosync_handleObjectProtocolMessage (582-612)
  • nosync_handleObjectSyncProtocolMessage (326-338)
  • nosync_handleObjectSyncProtocolMessage (479-579)
Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift (5)
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (1)
  • createInternalQueue (688-691)
Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift (4)
  • nosync_subscribe (84-100)
  • emit (138-147)
  • on (102-120)
  • unsubscribeAll (130-132)
Tests/AblyLiveObjectsTests/Helpers/Subscriber.swift (1)
  • createListener (36-58)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (3)
  • subscribe (150-164)
  • on (172-185)
  • unsubscribeAll (166-170)
Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (1)
  • withLock (25-37)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (2)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (5)
  • nosync_apply (247-266)
  • nosync_emit (198-202)
  • nosync_replaceData (210-223)
  • createZeroValued (66-81)
  • nosync_mergeInitialValue (226-230)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (8)
  • nosync_apply (323-343)
  • nosync_emit (263-267)
  • nosync_replaceData (276-292)
  • entries (136-140)
  • createZeroValued (87-104)
  • nosync_mergeInitialValue (295-306)
  • nosync_resetData (384-388)
  • nosync_releaseTombstonedEntries (391-395)
Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift (4)
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (1)
  • createInternalQueue (688-691)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (1)
  • createZeroValued (66-81)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (2)
  • createZeroValued (87-104)
  • entries (136-140)
Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift (1)
  • nosync_creationOperationForLiveMap (103-160)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (5)
Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (2)
  • withLock (25-37)
  • withoutLock (45-49)
Sources/AblyLiveObjects/Internal/CoreSDK.swift (1)
  • nosync_validateChannelState (99-111)
Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift (4)
  • nosync_subscribe (84-100)
  • unsubscribeAll (130-132)
  • on (102-120)
  • offAll (134-136)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (8)
  • unsubscribeAll (231-235)
  • on (237-250)
  • offAll (252-256)
  • nosync_emit (263-267)
  • nosync_replaceData (276-292)
  • nosync_mergeInitialValue (295-306)
  • mergeInitialValue (517-575)
  • nosync_apply (323-343)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (3)
  • nosync_emit (66-73)
  • nosync_replaceData (82-107)
  • nosync_apply (33-58)
Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift (1)
Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (1)
  • withoutLock (45-49)
Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift (5)
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (3)
  • createInternalQueue (688-691)
  • objectState (189-205)
  • counterObjectState (229-244)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (3)
  • createZeroValued (66-81)
  • value (100-104)
  • subscribe (150-164)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (3)
  • createZeroValued (87-104)
  • entries (136-140)
  • subscribe (215-229)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (2)
  • createZeroValueObject (222-272)
  • nosync_applySyncObjectsPool (275-376)
Tests/AblyLiveObjectsTests/Helpers/Subscriber.swift (1)
  • createListener (36-58)
Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift (3)
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (1)
  • createInternalQueue (688-691)
Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (3)
  • nosync_handleObjectSyncProtocolMessage (123-134)
  • nosync_handleObjectProtocolMessage (111-121)
  • nosync_onChannelAttached (107-109)
Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (7)
  • nosync_handleObjectSyncProtocolMessage (326-338)
  • nosync_handleObjectSyncProtocolMessage (479-579)
  • nosync_handleObjectProtocolMessage (308-319)
  • nosync_handleObjectProtocolMessage (582-612)
  • testsOnly_createZeroValueLiveObject (343-353)
  • nosync_onChannelAttached (293-301)
  • nosync_onChannelAttached (450-476)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (5)
Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (3)
  • nosync_getObjectFromPool (144-148)
  • on (264-277)
  • on (669-687)
Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (2)
  • withLock (25-37)
  • withoutLock (45-49)
Sources/AblyLiveObjects/Internal/CoreSDK.swift (1)
  • nosync_validateChannelState (99-111)
Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift (2)
  • nosync_subscribe (84-100)
  • on (102-120)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (1)
  • createZeroValueObject (222-272)
Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (5)
Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (2)
  • withLock (25-37)
  • withoutLock (45-49)
Sources/AblyLiveObjects/Internal/CoreSDK.swift (1)
  • nosync_validateChannelState (99-111)
Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift (1)
  • nosync_creationOperationForLiveMap (103-160)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (7)
  • nosync_getOrCreateMap (434-471)
  • nosync_getOrCreateCounter (387-423)
  • createZeroValueObject (222-272)
  • nosync_performGarbageCollection (486-518)
  • nosync_reset (474-483)
  • nosync_applySyncObjectsPool (275-376)
  • nosync_apply (33-58)
Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (3)
  • nosync_onChannelAttached (107-109)
  • nosync_handleObjectProtocolMessage (111-121)
  • nosync_handleObjectSyncProtocolMessage (123-134)
🪛 LanguageTool
CONTRIBUTING.md

[grammar] ~78-~78: There might be a mistake here.
Context: ...ks passed to the public API are invoked on same queue as used by the ARTRealtime...

(QB_NEW_EN)

🔇 Additional comments (55)
AblyLiveObjects.xcworkspace/xcshareddata/swiftpm/Package.resolved (1)

2-18: Lockfile aligns with pinned revisions.

Matches Package.swift; nothing else to do. Keep this in sync when unpinning.

Use the script in Package.swift comment to re-check after any dependency updates.

Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (1)

683-691: Internal queue helper looks good.

Serial by default, unique label, and .userInitiated QoS are sensible for tests.

Package.resolved (1)

2-18: Root lockfile matches the workspace and manifest pins.

LGTM; consistent with the temporary revision pinning approach.

Sources/AblyLiveObjects/Internal/CoreSDK.swift (2)

84-86: LGTM: direct passthrough to pluginAPI.

Implementation aligns with nosync contract.


17-18: channelState rename validated: no remaining call sites detected.

Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (1)

45-49: LGTM: queue-correctness precondition is appropriate.

The on-queue guard keeps withoutLock safe.

Sources/AblyLiveObjects/Internal/ObjectsPool.swift (9)

33-58: LGTM: nosync_apply forwards correctly to object-specific handlers.


66-73: LGTM: deferred updates now emit via nosync paths.


109-126: LGTM: tombstone accessors expose nosync_isTombstone/tombstonedAt for GC.


162-174: LGTM: internalQueue now threaded through pool initialization and root creation.

Also applies to: 185-193


222-228: LGTM: zero-value object creation propagates internalQueue to maps/counters.

Also applies to: 246-254, 256-264


383-423: LGTM: nosync_getOrCreateCounter adds internalQueue and merges initial value on creation.


430-471: LGTM: nosync_getOrCreateMap mirrors counter flow and passes semantics.


473-483: LGTM: nosync_reset preserves root and clears via nosync_resetData.


486-519: LGTM: GC flow uses nosync_releaseTombstonedEntries and nosync_tombstonedAt; yields once upon completion.

Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (7)

73-80: Good: state isolation via DispatchQueueMutex initialized with internalQueue.

This establishes a clear single-threaded mutation domain matching ably-cocoa’s model. The explicit internalQueue wiring into the mutex is correct.


263-267: Good: nosync_emit uses withoutLock on the internal queue and delivers on userCallbackQueue.

Separation between internal mutation and user callbacks is clear and avoids callback-induced re-entrancy.


276-306: Correct threading of internalQueue into replace/merge paths.

Passing internalQueue: mutableStateMutex.dispatchQueue through ensures downstream mutations occur on the same queue and aligns with the nosync discipline.


323-343: Apply path aligns with RTLM15 and emits on userCallbackQueue; queue propagation looks consistent.

No issues spotted; keep as-is.


710-716: Zero-value object creation is correctly routed with internalQueue/userCallbackQueue/clock.

This prevents cross-queue pool mutations. LGTM.


877-893: Channel-state validation moved to nosync_ API is appropriate inside the lock.

Given the CoreSDK’s nosync contract, validating while on the internal queue is consistent.


217-229: Ignore nested withLock suggestion: SubscriptionStorage.subscribe only stores the updateSelfLater closure and does not invoke it during subscription, so there’s no runtime nested withLock to trigger a dispatch precondition.

Likely an incorrect or invalid review comment.

Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift (1)

85-100: nosync_subscribe rename and nosync_validateChannelState usage look correct.

Matches the project-wide nosync discipline and keeps validation on the internal queue.

Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift (1)

50-73: Rename to nosync_toObjectData and use of nosync_objectID are consistent.

The mapping remains 1:1 with previous behavior; switching to nosync identifiers aligns with new isolation semantics.

Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift (3)

1285-1285: Good: switched to test-only accessor for object ID

Using testsOnly_objectID avoids relying on non-test internals. LGTM.


3141-3141: Good: test-only accessor for map object ID

testsOnly_objectID usage is appropriate here too. LGTM.


3787-3791: Good: test-only tombstone flags

testsOnly_isTombstone and testsOnly_tombstonedAt keep tests decoupled from production internals. LGTM.

Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift (1)

103-110: nosync variant consistency looks correct

Renaming to nosync_creationOperationForLiveMap and using nosync_toObjectData aligns with the internal-queue contract. Please ensure all call sites execute this on the plugin’s internal queue.

Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (2)

24-32: Static nosync accessor matches new threading model

Fetching with nosync_pluginDataValue and failing fast if missing is correct for internal use. LGTM.


107-134: Event dispatch to nosync handlers looks correct

Forwarding to nosync_onChannelAttached / nosync_handleObject{,Sync}ProtocolMessage aligns with the internal locking scheme. LGTM.

Tests/AblyLiveObjectsTests/Mocks/MockLiveMapObjectPoolDelegate.swift (2)

21-23: nosync accessor matches DispatchQueueMutex contract.

Precondition is enforced via withoutLock’s .onQueue check. LGTM.


6-10: Approve changes Good move to internalQueue-backed mutex; aligns with the nosync pattern and DispatchQueueMutex invariants, and no violations of withoutLock/withLock preconditions were detected.

Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift (1)

28-31: Correctly threading internalQueue through zero-valued creators.

Matches the new initializer signatures.

Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift (2)

12-14: Init wiring looks consistent with DispatchQueueMutex usage.

No issues spotted.


28-30: Annotate nosync_channelState with queue requirement

-    var nosync_channelState: _AblyPluginSupportPrivate.RealtimeChannelState {
+    /// Must be called on `internalQueue`; uses `DispatchQueueMutex.withoutLock`.
+    var nosync_channelState: _AblyPluginSupportPrivate.RealtimeChannelState {
         channelStateMutex.withoutLock { $0 }
     }
Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift (1)

231-235: LGTM: shared internalQueue is plumbed consistently through store/coreSDK

The tests correctly create and pass a single internalQueue into the store and MockCoreSDK, matching the new threading model.

Also applies to: 306-309

Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift (2)

14-17: LGTM: tests correctly drive nosync_ via internalQueue and validate channel state/publish paths*

The internalQueue-driven harness, channel-state error expectations, and publish assertions look correct and consistent.

Also applies to: 33-36, 52-54, 70-73, 103-106, 119-122, 138-141, 161-164, 188-191, 236-239, 263-266, 307-310, 334-337, 377-380, 417-420, 461-464, 492-495, 516-518, 536-539, 566-568, 590-593


38-41: Ensure a throwing DispatchQueue.sync extension is available

Tests invoke try internalQueue.sync { … }, but a code search found no rethrows extension on DispatchQueue.sync. Add or import a throwing sync extension in the test target to satisfy these calls.

Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (3)

106-143: LGTM: increment constructs message under lock and publishes after releasing it

Channel-state validation, finiteness check, and building OutboundObjectMessage under the mutex with publish outside is the right split.


210-223: LGTM: nosync_ mutation paths correctly use withoutLock and dispatch to userCallbackQueue*

The counter’s replace/merge/apply paths align with the internal-queue discipline and avoid holding the lock during emission.

Also applies to: 226-230, 246-266


198-202: Ensure all nosync_* accessors are only called on the internal queue
Manual verification: confirm nosync_emit, nosync_isTombstone, and nosync_tombstonedAt invocations occur exclusively within your internalQueue callbacks to maintain thread safety.

Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift (2)

200-204: Queue-isolated nosync usage looks solid

Wrapping nosync_* calls with internalQueue.sync keeps mutations serialized and matches the new threading model.

Also applies to: 221-224, 231-234, 253-256


1450-1461: Rename test methods to start with test for XCTest discovery
XCTest only runs methods prefixed with test; rename the following:

  • appliesOperationWhenCanBeAppliedtestAppliesOperationWhenCanBeApplied
  • createsNewEntryWhenNoExistingEntrytestCreatesNewEntryWhenNoExistingEntry
  • mapOperationApplicabilitytestMapOperationApplicability
  • publishesCorrectObjectMessageForDifferentValueTypestestPublishesCorrectObjectMessageForDifferentValueTypes
  • publishesCorrectObjectMessagetestPublishesCorrectObjectMessage

Likely an incorrect or invalid review comment.

Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift (1)

31-47: LGTM: deterministic internalQueue threading across tests

Consistent creation/propagation of internalQueue and nosync_* usage aligns with the documented model.

Also applies to: 75-81, 108-114

Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift (1)

109-112: LGTM: applySyncObjectsPool and other nosync ops correctly wrapped on internalQueue

Matches the serialized mutation contract; subscriber assertions validate emissions.

Also applies to: 147-150, 178-181, 209-212, 239-242, 268-269, 289-291, 354-357

Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (10)

145-148: Good implementation of nosync pattern

The method correctly uses withoutLock with the expected dispatch queue precondition, aligning with the threading model documented in DispatchQueueMutex.


91-115: Good parameter addition and mutex initialization

The addition of internalQueue parameter and its use in initializing DispatchQueueMutex aligns well with the PR's threading approach objectives.


293-301: Correct use of withoutLock for nosync method

The nosync_onChannelAttached method correctly uses withoutLock, indicating it's expected to be called when already on the internal queue. This aligns with the nosync naming convention and the broader threading model.


308-319: Good propagation of internalQueue through nosync methods

The method correctly passes mutableStateMutex.dispatchQueue as the internalQueue parameter to the nested nosync method call.


326-338: Consistent nosync pattern implementation

The nosync_handleObjectSyncProtocolMessage follows the same pattern as nosync_handleObjectProtocolMessage, correctly using withoutLock and passing the internal queue.


667-687: Clever use of closure for deferred updates

The UpdateMutableState type alias and its usage in the on method elegantly handle the need to update mutable state after releasing the lock. This pattern ensures thread safety while avoiding potential deadlocks.


563-570: Consistent nosync method naming

The change from applyObjectProtocolMessageObjectMessage to nosync_applyObjectProtocolMessageObjectMessage properly indicates that this method should be called when already on the internal queue.


468-468: Remove nosync_reset existence check
The nosync_reset() method is implemented in Sources/AblyLiveObjects/Internal/ObjectsPool.swift (around line 474), so the call is valid.


615-621: Ensure callers invoke on the internal queue

  • Call at Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift:563
  • Call at Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift:603
    Confirm each invocation runs on internalQueue (e.g. within internalQueue.async or already on that queue).

152-172: All nosync_validateChannelState calls are wrapped in mutableStateMutex.withLock, ensuring they execute on the internal queue.

Should have done this in ad60504 and 40b2d86. We do this for clarity so
that this closure doesn't look like a listener, same as in the existing
`subscribe` methods.
@lawrence-forooghian lawrence-forooghian force-pushed the 3-decide-how-threading-will-work branch from 55b834b to be8e9bf Compare September 3, 2025 12:08
@github-actions github-actions bot temporarily deployed to staging/pull/85/AblyLiveObjects September 3, 2025 12:09 Inactive
@lawrence-forooghian lawrence-forooghian force-pushed the 3-decide-how-threading-will-work branch from be8e9bf to e708951 Compare September 3, 2025 12:18
@github-actions github-actions bot temporarily deployed to staging/pull/85/AblyLiveObjects September 3, 2025 12:20 Inactive
@lawrence-forooghian lawrence-forooghian force-pushed the 3-decide-how-threading-will-work branch from e708951 to 5540135 Compare September 3, 2025 12:30
@github-actions github-actions bot temporarily deployed to staging/pull/85/AblyLiveObjects September 3, 2025 12:31 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift (1)

13-16: Good switch to ably_syncNoDeadlock to avoid self-queue deadlocks

Executing nosync_ retrieval under internalQueue.ably_syncNoDeadlock { ... } is correct and prevents accidental re-entry hangs. Matches the PR’s threading model.

🧹 Nitpick comments (14)
AblyLiveObjects.xcworkspace/xcshareddata/swiftpm/Package.resolved (1)

17-17: Same note for ably-cocoa-plugin-support: revision pin is fine; verify Package.swift and track unpin.

Covered by the script above; please ensure the manifest also pins this to the same commit style and add a reminder to revert to tags pre-release.

CONTRIBUTING.md (1)

73-84: Threading section: add the explicit “different queues” invariant and tighten one sentence

  • Please state explicitly that the user callback queue and the internal queue are always different; this prevents confusion and future regressions around nested locking.
  • Minor style: drop “in order to”.
 ### Threading
 
 Since this is an extension of ably-cocoa, we follow the same threading approach:
 
 1. The public API can be interacted with from any thread, including synchronous methods such as getters
 2. Callbacks passed to the public API are invoked on the same queue as used by the `ARTRealtime` instance (the `dispatchQueue` client option)
 3. Synchronisation of mutable state is performed using the same internal serial dispatch queue as is used by the `ARTRealtime` instance (the `internalDispatchQueue` client option)
 
 We follow the same naming convention as in ably-cocoa whereby if a method's name contains `nosync` then it must be called on the internal dispatch queue. This allows us to avoid deadlocks that would result from attempting to call `DispatchQueue.sync { … }` when already on the internal queue.
 
-We have an extension on `DispatchQueue`, `ably_syncNoDeadlock(execute:)`, which behaves the same as `sync(execute:)` but with a runtime precondition that we are not already on the queue; favour our extension in order to avoid deadlock.
+Note: the user callback queue and the internal queue are intentionally different. This avoids deadlocks when callbacks update internal state under `withLock`.
+
+We have an extension on `DispatchQueue`, `ably_syncNoDeadlock(execute:)`, which behaves the same as `sync(execute:)` but with a runtime precondition that we are not already on the queue; favour our extension to avoid deadlock.
Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift (2)

199-206: Nested withLock inside updateSelfLater: safe, but add a clarifying comment

Given updateSelfLater is stored and invoked later (e.g., during unsubscribe) outside the current locking context, reacquiring the mutex here won’t violate .notOnQueue. Add a brief comment to prevent future “false positive” refactors.

-                        try stored.nosync_subscribe(listener: listener, coreSDK: coreSDK, updateSelfLater: { [weak self] action in
+                        try stored.nosync_subscribe(listener: listener, coreSDK: coreSDK, updateSelfLater: { [weak self] action in
                             guard let self else {
                                 return
                             }
 
-                            mutex.withLock { stored in
+                            // Safe: invoked later (e.g., during unsubscribe), outside the outer withLock’s context.
+                            mutex.withLock { stored in
                                 action(&stored)
                             }
                         })

247-247: Typo: recieve → receive

Minor nit in the comment.

-            // @specOneOf(2/3) RTLO4b5b - Check we can unsubscribe using the `response` that's passed to the listener, and that when two updates are emitted back-to-back, the unsubscribe in the first listener causes us to not recieve the second update
+            // @specOneOf(2/3) RTLO4b5b - Check we can unsubscribe using the `response` that's passed to the listener, and that when two updates are emitted back-to-back, the unsubscribe in the first listener causes us to not receive the second update
Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (2)

37-53: Prepare: thread internalQueue/callbackQueue and store objects—consider asserting queues differ in debug

The wiring looks correct. As a defensive check, consider asserting that internalQueue and callbackQueue aren’t the same in debug builds to catch misconfiguration early.

     internal func nosync_prepare(_ channel: _AblyPluginSupportPrivate.RealtimeChannel, client: _AblyPluginSupportPrivate.RealtimeClient) {
         let pluginLogger = pluginAPI.logger(for: channel)
         let internalQueue = pluginAPI.internalQueue(for: client)
         let callbackQueue = pluginAPI.callbackQueue(for: client)
+        #if DEBUG
+        if internalQueue === callbackQueue {
+            assertionFailure("internalQueue and callbackQueue must be different")
+        }
+        #endif

138-165: sendObject: simplify continuation and align type naming

  • Consider withCheckedThrowingContinuation to avoid the intermediate Result.
  • Minor consistency: elsewhere types are fully qualified; use the same for PluginAPIProtocol here.
-    internal static func sendObject(
+    internal static func sendObject(
         objectMessages: [OutboundObjectMessage],
         channel: _AblyPluginSupportPrivate.RealtimeChannel,
         client: _AblyPluginSupportPrivate.RealtimeClient,
-        pluginAPI: PluginAPIProtocol,
+        pluginAPI: _AblyPluginSupportPrivate.PluginAPIProtocol,
     ) async throws(InternalError) {
         let objectMessageBoxes: [ObjectMessageBox<OutboundObjectMessage>] = objectMessages.map { .init(objectMessage: $0) }
 
-        try await withCheckedContinuation { (continuation: CheckedContinuation<Result<Void, InternalError>, _>) in
+        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
             let internalQueue = pluginAPI.internalQueue(for: client)
 
             internalQueue.async {
                 pluginAPI.nosync_sendObject(
                     withObjectMessages: objectMessageBoxes,
                     channel: channel,
                 ) { error in
                     // We don't currently rely on this documented behaviour of `nosync_sendObject` but we may do later, so assert it to be sure it's happening.
                     dispatchPrecondition(condition: .onQueue(internalQueue))
 
                     if let error {
-                        continuation.resume(returning: .failure(error.toInternalError()))
+                        continuation.resume(throwing: error.toInternalError())
                     } else {
-                        continuation.resume(returning: .success(()))
+                        continuation.resume()
                     }
                 }
             }
-        }.get()
+        }
     }
Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift (2)

24-24: Nit: use the local pluginAPI for consistency

You already bound let pluginAPI = Plugin.defaultPluginAPI; pass that instead of re-reading the static to avoid drift and improve clarity.

-            pluginAPI: Plugin.defaultPluginAPI,
+            pluginAPI: pluginAPI,

37-40: Nit: drop generic doc comment on testsOnly_ API

Per repo guidelines, testsOnly_ declarations don’t need explanatory comments. Consider removing the docstring here.

-    /// For tests to access the non-public API of `PublicDefaultRealtimeObjects`.
     internal var testsOnly_nonTypeErasedObjects: PublicDefaultRealtimeObjects {
         nonTypeErasedObjects
     }
Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift (1)

217-222: Nonce generation TODO — offer secure, spec-aligned helper

Happy to replace the ad-hoc charset generator with a CSPRNG-based Base64URL string to satisfy the TODO.

-    private static func generateNonce() -> String {
-        // TODO: confirm if there's any specific rules here: https://github.com/ably/specification/pull/353/files#r2228252389
-        let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
-        return String((0 ..< 16).map { _ in letters.randomElement()! })
-    }
+    private static func generateNonce() -> String {
+        // 16 random bytes, Base64URL-encoded (no padding)
+        var bytes = [UInt8](repeating: 0, count: 16)
+        _ = bytes.withUnsafeMutableBytes { ptr in
+            SecRandomCopyBytes(kSecRandomDefault, ptr.count, ptr.baseAddress!)
+        }
+        let b64 = Data(bytes).base64EncodedString()
+            .replacingOccurrences(of: "+", with: "-")
+            .replacingOccurrences(of: "/", with: "_")
+            .replacingOccurrences(of: "=", with: "")
+        return b64
+    }
Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (1)

25-35: Simplify withLock by leveraging rethrows; Result wrapper unnecessary

You can propagate the typed error directly and let ably_syncNoDeadlock rethrow, reducing nesting.

-    internal func withLock<R, E>(_ body: (inout T) throws(E) -> R) throws(E) -> R {
-        let result: Result<R, E> = dispatchQueue.ably_syncNoDeadlock {
-            do throws(E) {
-                return try .success(body(&value))
-            } catch {
-                return .failure(error)
-            }
-        }
-
-        return try result.get()
-    }
+    internal func withLock<R, E>(_ body: (inout T) throws(E) -> R) throws(E) -> R {
+        try dispatchQueue.ably_syncNoDeadlock {
+            try body(&value)
+        }
+    }
Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift (1)

247-256: Optionally run mutating helpers inside the internal queue for consistency.

testsOnly_* helpers likely lock internally, but executing them within internalQueue.ably_syncNoDeadlock keeps call sites uniform with the rest of the suite.

Also applies to: 275-283, 318-325

Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (3)

108-120: Nested withLock inside subscribe/on is acceptable given queue separation.

Given internalQueue != userCallbackQueue by design (per ably-cocoa conventions), invoking withLock from updateSelfLater is safe and non-reentrant. Consider adding a short comment to codify this invariant.

-            try mutableState.liveObjectMutableState.nosync_subscribe(listener: listener, coreSDK: coreSDK, updateSelfLater: { [weak self] action in
+            // Safe: updateSelfLater runs on userCallbackQueue, which is distinct from internalQueue by design.
+            try mutableState.liveObjectMutableState.nosync_subscribe(listener: listener, coreSDK: coreSDK, updateSelfLater: { [weak self] action in

Also applies to: 215-229, 231-257


906-922: Small iteration cleanup in nosync_entries.

Use compactMap to reduce temporary storage and branching.

-            var result: [(key: String, value: InternalLiveMapValue)] = []
-
-            for (key, entry) in data where !Self.nosync_isEntryTombstoned(entry, delegate: delegate) {
-                // Convert entry to LiveMapValue using the same logic as get(key:)
-                if let value = nosync_convertEntryToLiveMapValue(entry, delegate: delegate) {
-                    result.append((key: key, value: value))
-                }
-            }
-
-            return result
+            return data.compactMap { key, entry in
+                guard !Self.nosync_isEntryTombstoned(entry, delegate: delegate),
+                      let value = nosync_convertEntryToLiveMapValue(entry, delegate: delegate)
+                else { return nil }
+                return (key: key, value: value)
+            }

320-321: Consider aligning testsOnly_ helper locks with nosync variants.*

testsOnly_* helpers use withLock (fine for tests). If you later expose them for non-test diagnostics, add nosync_* counterparts to avoid surprise locking.

Also applies to: 355-367, 372-382

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e708951 and 5540135.

📒 Files selected for processing (26)
  • AblyLiveObjects.xcworkspace/xcshareddata/swiftpm/Package.resolved (1 hunks)
  • CONTRIBUTING.md (1 hunks)
  • Package.resolved (1 hunks)
  • Package.swift (1 hunks)
  • Sources/AblyLiveObjects/Internal/CoreSDK.swift (3 hunks)
  • Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (4 hunks)
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (9 hunks)
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (32 hunks)
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (22 hunks)
  • Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift (2 hunks)
  • Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift (1 hunks)
  • Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift (1 hunks)
  • Sources/AblyLiveObjects/Internal/ObjectsPool.swift (22 hunks)
  • Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift (1 hunks)
  • Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift (1 hunks)
  • Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (1 hunks)
  • Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (1 hunks)
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift (22 hunks)
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift (51 hunks)
  • Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift (53 hunks)
  • Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift (3 hunks)
  • Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift (8 hunks)
  • Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift (2 hunks)
  • Tests/AblyLiveObjectsTests/Mocks/MockLiveMapObjectPoolDelegate.swift (1 hunks)
  • Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift (1 hunks)
  • Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift (17 hunks)
🚧 Files skipped from review as they are similar to previous changes (13)
  • Tests/AblyLiveObjectsTests/Mocks/MockLiveMapObjectPoolDelegate.swift
  • Sources/AblyLiveObjects/Internal/CoreSDK.swift
  • Package.resolved
  • Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift
  • Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift
  • Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift
  • Package.swift
  • Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift
  • Sources/AblyLiveObjects/Internal/ObjectsPool.swift
  • Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift
  • Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift
  • Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift
🧰 Additional context used
📓 Path-based instructions (4)
**/*.swift

📄 CodeRabbit inference engine (.cursor/rules/swift.mdc)

**/*.swift: Specify an explicit access control level (SwiftLint explicit_acl) for all declarations in Swift code (tests are exempt)
When extending a type, put the access level on the extension declaration rather than on each member (tests are exempt)
Prefer implicit .init(...) when the type can be inferred in initializer expressions
Prefer enum case shorthand (.caseName) when the type can be inferred
For JSONValue or WireValue, prefer using literal syntax via ExpressibleBy*Literal where possible
Prefer Swift raw string literals for JSON strings instead of escaping double quotes
When an array literal begins with an initializer expression, place the initializer on the line after the opening bracket

Files:

  • Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift
  • Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift
  • Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift
  • Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
  • Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift
  • Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift
Sources/AblyLiveObjects/**/*.swift

📄 CodeRabbit inference engine (.cursor/rules/swift.mdc)

In AblyLiveObjects library (non-test) code, import modules as: Ably with import Ably, and _AblyPluginSupportPrivate with internal import _AblyPluginSupportPrivate

Files:

  • Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift
  • Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift
  • Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
  • Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift
  • Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
Sources/**/*.swift

📄 CodeRabbit inference engine (.cursor/rules/testing.mdc)

For testsOnly_ property declarations, do not add generic explanatory comments (their meaning is understood)

Files:

  • Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift
  • Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift
  • Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
  • Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift
  • Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
Tests/**/*.swift

📄 CodeRabbit inference engine (.cursor/rules/testing.mdc)

Tests/**/*.swift: Use the Swift Testing framework (import Testing), not XCTest, in test files
Do not use fatalError for expectation failures; prefer Swift Testing’s #require
Only add labels to test cases or suites when the label differs from the suite struct or test method name
Tag tests per CONTRIBUTING.md’s "Attributing tests to a spec point" with exact comment format; distinguish @spec vs @specPartial; do not repeat @spec for the same spec point
Add comments in tests to clarify when certain test data is irrelevant to the scenario
In tests, import Ably using import Ably
In tests, import AblyLiveObjects using @testable import AblyLiveObjects
In tests, import _AblyPluginSupportPrivate using import _AblyPluginSupportPrivate (do not use internal import)
When passing a logger to internal components in tests, use TestLogger()
When unwrapping optionals in tests, prefer #require over guard let

Files:

  • Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift
  • Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift
🧠 Learnings (9)
📓 Common learnings
Learnt from: lawrence-forooghian
PR: ably/ably-liveobjects-swift-plugin#85
File: Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift:239-250
Timestamp: 2025-09-03T11:54:29.104Z
Learning: In the ably-liveobjects-swift-plugin codebase (following ably-cocoa conventions), the userCallbackQueue and internalQueue are always different queues by design. This prevents deadlock scenarios with nested withLock calls when callbacks need to update internal state, making the nested withLock pattern safe.
Learnt from: lawrence-forooghian
PR: ably/ably-liveobjects-swift-plugin#85
File: Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift:239-250
Timestamp: 2025-09-03T11:54:29.105Z
Learning: In the ably-liveobjects-swift-plugin codebase (following ably-cocoa conventions), the userCallbackQueue and internalQueue are always different queues by design. This prevents deadlock scenarios with nested withLock calls when callbacks need to update internal state, making the nested withLock pattern safe.
📚 Learning: 2025-09-03T11:54:29.105Z
Learnt from: lawrence-forooghian
PR: ably/ably-liveobjects-swift-plugin#85
File: Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift:239-250
Timestamp: 2025-09-03T11:54:29.105Z
Learning: In the ably-liveobjects-swift-plugin codebase (following ably-cocoa conventions), the userCallbackQueue and internalQueue are always different queues by design. This prevents deadlock scenarios with nested withLock calls when callbacks need to update internal state, making the nested withLock pattern safe.

Applied to files:

  • Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift
  • Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift
  • Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
  • Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift
  • CONTRIBUTING.md
📚 Learning: 2025-09-03T11:58:36.335Z
Learnt from: lawrence-forooghian
PR: ably/ably-liveobjects-swift-plugin#85
File: Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift:152-163
Timestamp: 2025-09-03T11:58:36.335Z
Learning: The updateSelfLater closures in the ably-liveobjects-swift-plugin codebase are stored by the subscription system and invoked asynchronously later (e.g., during unsubscribe operations), not immediately during the current execution context. Therefore, using withLock inside these closures is safe and correct, as they execute outside the original locking context.

Applied to files:

  • Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift
  • Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift
  • Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
  • Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
📚 Learning: 2025-08-26T11:32:54.222Z
Learnt from: CR
PR: ably/ably-liveobjects-swift-plugin#0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-08-26T11:32:54.222Z
Learning: Applies to Tests/**/*.swift : In tests, import AblyLiveObjects using `testable import AblyLiveObjects`

Applied to files:

  • Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift
  • Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift
  • CONTRIBUTING.md
📚 Learning: 2025-09-03T11:58:36.335Z
Learnt from: lawrence-forooghian
PR: ably/ably-liveobjects-swift-plugin#85
File: Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift:152-163
Timestamp: 2025-09-03T11:58:36.335Z
Learning: The updateSelfLater closures in the ably-liveobjects-swift-plugin subscription system are stored during the subscribe call and only invoked later during unsubscribe operations (via SubscribeResponse.unsubscribe()). This means using withLock inside these closures is safe since they execute in separate contexts from the original subscription call.

Applied to files:

  • Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift
  • Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
📚 Learning: 2025-09-03T11:55:37.381Z
Learnt from: lawrence-forooghian
PR: ably/ably-liveobjects-swift-plugin#85
File: Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift:8-16
Timestamp: 2025-09-03T11:55:37.381Z
Learning: In Swift 5.10 and later, `nonisolated(unsafe)` can be used on stored properties and variables in classes (not just actors) to opt out of actor isolation checks. This eliminates the need for unchecked Sendable wrappers in scenarios where specific instances require opt-out from Sendable constraints across isolation boundaries. The pattern `private nonisolated(unsafe) var value: T` in a `final class DispatchQueueMutex<T>: Sendable` is valid and compiles correctly.

Applied to files:

  • Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift
  • Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift
📚 Learning: 2025-09-03T11:56:46.656Z
Learnt from: lawrence-forooghian
PR: ably/ably-liveobjects-swift-plugin#85
File: Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift:8-8
Timestamp: 2025-09-03T11:56:46.656Z
Learning: `nonisolated(unsafe)` is valid on class properties in Swift 5.10+, not just actor properties. The attribute can be used on stored properties and variables in classes to opt out of actor isolation checks and Sendable constraints. This corrects the misconception that it's only valid in actor contexts.

Applied to files:

  • Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift
📚 Learning: 2025-08-22T20:27:49.043Z
Learnt from: lawrence-forooghian
PR: ably/ably-liveobjects-swift-plugin#79
File: Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift:246-258
Timestamp: 2025-08-22T20:27:49.043Z
Learning: In Swift, `mutex.withLock { ... }` closures with single expressions automatically return the result of that expression, so explicit return statements are not needed when the closure contains only a call like `mutableState.on(...)` that returns the expected type.

Applied to files:

  • Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
📚 Learning: 2025-08-26T11:32:26.789Z
Learnt from: CR
PR: ably/ably-liveobjects-swift-plugin#0
File: .cursor/rules/swift.mdc:0-0
Timestamp: 2025-08-26T11:32:26.789Z
Learning: Applies to Sources/AblyLiveObjects/**/*.swift : In AblyLiveObjects library (non-test) code, import modules as: Ably with `import Ably`, and _AblyPluginSupportPrivate with `internal import _AblyPluginSupportPrivate`

Applied to files:

  • Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift
  • Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift
🧬 Code graph analysis (9)
Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift (1)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (1)
  • entries (136-140)
Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift (4)
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (6)
  • createInternalQueue (688-691)
  • counterObjectState (229-244)
  • objectOperation (336-356)
  • counterCreateOperation (374-383)
  • counterOp (386-388)
  • mapCreateOperation (359-371)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (4)
  • createZeroValued (87-104)
  • nosync_replaceData (277-293)
  • nosync_mergeInitialValue (296-307)
  • nosync_apply (324-344)
Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift (2)
  • ably_syncNoDeadlock (5-8)
  • ably_syncNoDeadlock (11-14)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (2)
  • nosync_replaceData (82-107)
  • nosync_apply (33-58)
Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift (3)
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (8)
  • createInternalQueue (688-691)
  • simpleMapMessage (651-661)
  • mapCreateOperationMessage (597-611)
  • mapObjectMessage (286-298)
  • counterObjectMessage (301-313)
  • objectReferenceMapEntry (508-522)
  • rootObjectMessage (316-326)
  • stringMapEntry (421-435)
Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift (2)
  • ably_syncNoDeadlock (5-8)
  • ably_syncNoDeadlock (11-14)
Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (7)
  • nosync_handleObjectSyncProtocolMessage (327-339)
  • nosync_handleObjectSyncProtocolMessage (480-580)
  • nosync_handleObjectProtocolMessage (309-320)
  • nosync_handleObjectProtocolMessage (583-613)
  • testsOnly_createZeroValueLiveObject (344-354)
  • nosync_onChannelAttached (294-302)
  • nosync_onChannelAttached (451-477)
Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift (6)
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (1)
  • createInternalQueue (688-691)
Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift (2)
  • ably_syncNoDeadlock (5-8)
  • ably_syncNoDeadlock (11-14)
Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift (3)
  • nosync_subscribe (84-100)
  • on (102-120)
  • unsubscribeAll (130-132)
Tests/AblyLiveObjectsTests/Helpers/Subscriber.swift (1)
  • createListener (36-58)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (3)
  • subscribe (215-229)
  • on (237-251)
  • unsubscribeAll (231-235)
Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (1)
  • withLock (25-35)
Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (2)
Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift (2)
  • ably_syncNoDeadlock (5-8)
  • ably_syncNoDeadlock (11-14)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (1)
  • get (124-128)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (4)
Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (5)
  • nosync_getObjectFromPool (144-148)
  • on (264-278)
  • on (670-688)
  • offAll (280-284)
  • offAll (698-700)
Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (2)
  • withLock (25-35)
  • withoutLock (43-47)
Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift (4)
  • nosync_subscribe (84-100)
  • unsubscribeAll (130-132)
  • on (102-120)
  • offAll (134-136)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (1)
  • createZeroValueObject (222-272)
Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift (1)
Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift (2)
  • ably_syncNoDeadlock (5-8)
  • ably_syncNoDeadlock (11-14)
Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (4)
Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (2)
  • withLock (25-35)
  • withoutLock (43-47)
Tests/AblyLiveObjectsTests/Mocks/MockLiveMapObjectPoolDelegate.swift (1)
  • nosync_getObjectFromPool (21-23)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (3)
  • entries (136-140)
  • on (237-251)
  • nosync_apply (324-344)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (7)
  • nosync_getOrCreateMap (434-471)
  • nosync_getOrCreateCounter (387-423)
  • createZeroValueObject (222-272)
  • nosync_performGarbageCollection (486-518)
  • nosync_reset (474-483)
  • nosync_applySyncObjectsPool (275-376)
  • nosync_apply (33-58)
Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift (6)
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (1)
  • createInternalQueue (688-691)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (7)
  • createZeroValued (87-104)
  • get (124-128)
  • nosync_replaceData (277-293)
  • entries (136-140)
  • nosync_mergeInitialValue (296-307)
  • nosync_apply (324-344)
  • set (152-182)
Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift (2)
  • ably_syncNoDeadlock (5-8)
  • ably_syncNoDeadlock (11-14)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (2)
  • nosync_replaceData (82-107)
  • nosync_apply (33-58)
Tests/AblyLiveObjectsTests/Helpers/Subscriber.swift (1)
  • createListener (36-58)
Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift (1)
  • setPublishHandler (33-37)
🪛 LanguageTool
CONTRIBUTING.md

[style] ~83-~83: Consider a more concise word here.
Context: ...eady on the queue; favour our extension in order to avoid deadlock. ### Testing guidelines...

(IN_ORDER_TO_PREMIUM)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Xcode, release configuration, tvOS (Xcode 16.3)
  • GitHub Check: Xcode, macOS (Xcode 16.3)
  • GitHub Check: Xcode, tvOS (Xcode 16.3)
  • GitHub Check: SPM (Xcode 16.3)
  • GitHub Check: Xcode, iOS (Xcode 16.3)
  • GitHub Check: Generate code coverage
🔇 Additional comments (45)
AblyLiveObjects.xcworkspace/xcshareddata/swiftpm/Package.resolved (2)

2-2: Origin hash bump looks correct after dependency state changes.

No action needed.


9-9: Verified revision pin and TODOs Package.swift pins ably-cocoa to revision 8dde3e841aa1f861176c1341cf44e92014b95857 (matching Package.resolved) and includes TODO comments to unpin before the next release.

Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift (3)

191-195: Good switch to DispatchQueueMutex and internalQueue injection

Using DispatchQueueMutex with a per-test internalQueue matches the nosync model and keeps tests deterministic. LGTM.


213-215: Emit/unsubscribeAll under mutex: correct synchronization boundaries

Both emit and unsubscribeAll are protected by the same mutex, aligning with the internalQueue serialization. LGTM.

Also applies to: 219-221


231-235: Consistent internalQueue propagation across tests

Creating a per-test internalQueue and threading it through MockCoreSDK and stores is consistent and clear. LGTM.

Also applies to: 253-257, 279-283, 306-309

Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (2)

24-32: nosync_realtimeObjects accessor: clear failure mode and internal-only cast

The guarded lookup with a strong failure message is appropriate. Forced cast is fine internally given we control the set path. LGTM.


56-58: Forwarders to InternalDefaultRealtimeObjects: aligned with nosync model

Simple delegations into the channel-scoped objects with nosync entry points look good.

Also applies to: 107-134

Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift (2)

103-110: Rename and nosync_ usage look correct

The nosync_creationOperationForLiveMap rename and switch to nosync_toObjectData align with the internal-queue model.


103-106: All call sites updated; no stale references found
Search for creationOperationForLiveMap( returned no matches in Sources/** or Tests/**.

Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift (1)

3-15: Deadlock-guarded sync helpers look solid

The precondition + sync pairing is concise and correctly generic. Good internal scoping.

Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (1)

37-47: withoutLock on-queue precondition is appropriate

The on-queue precondition enforces correct usage and matches the nosync_ pattern elsewhere.

Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift (2)

38-40: Good internalQueue plumbing and nosync usage.

All mutating reads/writes are correctly wrapped in internalQueue.ably_syncNoDeadlock and use nosync_* paths, matching the concurrency model.

Also applies to: 57-59, 79-81, 91-93, 109-111, 122-126, 145-147, 166-169, 193-195, 203-205, 222-224, 241-244, 268-271, 312-314, 339-345, 353-361, 388-396, 424-427, 436-444, 470-478, 492-495, 515-518, 565-568, 590-593


496-505: Error assertions are precise and stable.

Asserting ARTErrorInfo code/status and message substrings keeps tests robust. LGTM.

Also applies to: 519-528, 573-581

Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift (5)

11-22: Factory helper reads well; explicit internalQueue is clear.

createDefaultRealtimeObjects wires logger/internalQueue/userCallbackQueue/clock cleanly.


31-47: Correct sequencing for OBJECT_SYNC (single and multi-message).

The tests exercise sequence start/continuation/end and assert pool mutations and sync clearing as expected.

Also applies to: 74-81, 108-114


150-163: Buffered OBJECT handling during sync is well-covered.

Buffering, discard-on-new-sequence, and post-sync application paths are asserted clearly.

Also applies to: 168-174, 188-205, 216-223


424-431: Subscription assertions validate callback threading.

Using Subscriber with callbackQueue: .main aligns with userCallbackQueue; emitted updates are checked deterministically.

Also applies to: 433-445, 468-501


600-619: getRoot waiting logic is exercised across all completion modes.

Solid coverage for ATTACHED(HAS_OBJECTS=false), single OBJECT_SYNC, multi-message sync, and already-synced cases.

Also applies to: 624-648, 665-714

Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift (6)

14-19: Read-only accessors validate error and value-matrix well.

Matrix of RTLM5d2 value kinds, tombstones, and missing refs is thorough.

Also applies to: 34-38, 49-52, 58-63, 70-76, 83-88, 95-101, 108-113, 121-126, 134-139, 145-151, 162-171


193-205: replaceData paths cover siteTimeserials, createOp flag, and entry merges.

Nice separation of cases with/without createOp and combined merging.

Also applies to: 200-204, 212-217, 221-233


346-361: Accessors (size/entries/keys/values) filter tombstones consistently.

The consistency checks across properties are helpful to catch drift.

Also applies to: 395-406, 432-458


498-527: MAP_SET applicability and side-effects are well asserted.

Covers timeserial rules, pool zero-value creation, and returned updates.

Also applies to: 533-597, 615-659


718-777: MAP_REMOVE tests cover canApply, state updates, and return updates.

Good separation of existing vs. non-existing entry cases.

Also applies to: 786-801, 805-814


1096-1115: Publishing tests assert full ObjectMessage shape.

Message structure (action, objectId, op payload) and error propagation are checked precisely.

Also applies to: 1161-1169, 1218-1226, 1274-1282, 1296-1321, 1357-1372, 1408-1424, 1454-1484, 1489-1505

Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (8)

44-62: Initialization and zero-valued constructor are thread-safe and explicit.

mutableStateMutex is bound to internalQueue and state is fully initialized; explicit ACLs match guidelines.

Also applies to: 64-80, 82-104


276-292: Pass-through of internalQueue/userCallbackQueue in replaceData is correct.

The nosync_* boundary is respected via withoutLock; logger/clock are forwarded.


296-307: nosync_ mutators correctly require caller to be on internalQueue.*

withoutLock usage with mutableStateMutex.dispatchQueue passed downstream is consistent.

Also applies to: 323-344


835-845: resetData emits .removed per prior contents.

Emission on userCallbackQueue preserves callback isolation.


877-895: Channel state validation is enforced before access.

Using nosync_validateChannelState with precise operation descriptions is good.

Also applies to: 896-905


926-943: Pool lookups and tombstone propagation are correct.

nosync_getObjectFromPool + nosync_isTombstone ensures references don't surface tombstoned objects.

Also applies to: 984-1007


1-3: Module imports conform to repo guidelines.

internal import _AblyPluginSupportPrivate and import Ably are correct.


712-718: No typo in ObjectsPool.swift The identifier objects_pool does not exist—nosync_apply correctly forwards to .map and .counter with objectMessageSerial.

Likely an incorrect or invalid review comment.

Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (13)

6-7: LGTM! The threading model switch to DispatchQueue looks good.

The replacement of NSLock with DispatchQueueMutex aligns well with ably-cocoa's conventions and provides a cleaner concurrency model. From learnings, I understand this pattern is safe because userCallbackQueue and internalQueue are always different queues by design.


91-115: Good implementation of the threading architecture.

The initialization properly threads the internalQueue through all components, ensuring consistent isolation of mutable state to the ARTRealtime instance's internal queue as intended.


144-148: Correct use of withoutLock for nosync_ methods.

The nosync_ prefix appropriately indicates this method must be called on the internal queue, and withoutLock correctly enforces this precondition.


268-276: Good implementation of the updateSelfLater closure pattern.

The pattern of storing closures for asynchronous invocation avoids deadlocks as documented in the learnings. The weak self capture prevents retain cycles.


294-302: Consistent nosync_ pattern implementation.

The method correctly uses withoutLock and threads through required parameters while maintaining the nosync_ convention.


309-320: Proper threading of internalQueue through object protocol message handling.

The nosync_ variant correctly passes the internalQueue from the mutex's dispatchQueue to downstream operations.


327-339: Consistent implementation of nosync_ pattern for sync messages.

The method properly threads the internalQueue through to the mutable state operations.


345-353: Test helper properly updated for new threading model.

The test helper correctly uses withLock and passes the internalQueue to the object creation method.


367-374: Garbage collection correctly updated for nosync_ pattern.

The method properly passes all required parameters to the nosync_ variant.


552-558: Consistent threading of internalQueue through sync operations.

Both nosync_applySyncObjectsPool and nosync_applyObjectProtocolMessageObjectMessage correctly receive and use the internalQueue parameter.

Also applies to: 564-571


616-640: Object creation properly threads internalQueue.

The zero-value object creation correctly passes the internalQueue parameter when creating new objects during OBJECT message processing.


653-659: nosync_apply correctly handles object pool mutations.

The method properly applies operations with the object pool passed by reference.


668-668: Good use of @sendable for closure type alias.

The UpdateMutableState type alias correctly marks the closure as @Sendable to ensure thread safety.

Copy link
Collaborator

@maratal maratal left a comment

Choose a reason for hiding this comment

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

LGTM with minor questions.

@lawrence-forooghian lawrence-forooghian force-pushed the 3-decide-how-threading-will-work branch from 5540135 to a99fb43 Compare September 26, 2025 20:19
@github-actions github-actions bot temporarily deployed to staging/pull/85/AblyLiveObjects September 26, 2025 20:21 Inactive
Isolate the plugin's mutable state to the ARTRealtime instance's
internal queue, for consistency with ably-cocoa.

Will unpin ably-cocoa and ably-cocoa-plugin-support before our next
release.

Resolves #3.
@lawrence-forooghian lawrence-forooghian force-pushed the 3-decide-how-threading-will-work branch from a99fb43 to b86e09a Compare September 26, 2025 20:25
@github-actions github-actions bot temporarily deployed to staging/pull/85/AblyLiveObjects September 26, 2025 20:26 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (24)
Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift (2)

21-26: Use the local pluginAPI variable for consistency

Pass the pluginAPI you already fetched instead of Plugin.defaultPluginAPI again.

Apply this diff:

-            pluginAPI: Plugin.defaultPluginAPI,
+            pluginAPI: pluginAPI,

37-40: Remove explanatory doc comment on testsOnly_ API

Per repo guidance, testsOnly_ properties shouldn't carry generic explanatory comments.

As per coding guidelines

-    /// For tests to access the non-public API of `PublicDefaultRealtimeObjects`.
     internal var testsOnly_nonTypeErasedObjects: PublicDefaultRealtimeObjects {
         nonTypeErasedObjects
     }
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (6)

82-87: Remove unused userCallbackQueue parameter

userCallbackQueue is not used in Entry.nosync_replaceData and the callees already capture their own callback queues. Simplify the API and its call site.

Apply this diff:

-        fileprivate func nosync_replaceData(
+        fileprivate func nosync_replaceData(
             using state: ObjectState,
             objectMessageSerialTimestamp: Date?,
             objectsPool: inout ObjectsPool,
-            userCallbackQueue: DispatchQueue,
         ) -> DeferredUpdate {

And update the call site:

-                let deferredUpdate = existingEntry.nosync_replaceData(
+                let deferredUpdate = existingEntry.nosync_replaceData(
                     using: syncObjectsPoolEntry.state,
                     objectMessageSerialTimestamp: syncObjectsPoolEntry.objectMessageSerialTimestamp,
-                    objectsPool: &self,
-                    userCallbackQueue: userCallbackQueue,
+                    objectsPool: &self,
                 )

Also applies to: 299-304


128-146: Drop testsOnly_ doc comments

These comments are redundant per repo standards.

As per coding guidelines

-        /// Test-only accessor for isTombstone that handles locking internally.
         internal var testsOnly_isTombstone: Bool {
             switch self {
             case let .counter(counter):
                 counter.testsOnly_isTombstone
             case let .map(map):
                 map.testsOnly_isTombstone
             }
         }

-        /// Test-only accessor for tombstonedAt that handles locking internally.
         internal var testsOnly_tombstonedAt: Date? {
             switch self {
             case let .counter(counter):
                 counter.testsOnly_tombstonedAt
             case let .map(map):
                 map.testsOnly_tombstonedAt
             }
         }

222-228: Assert on the internal queue for nosync mutation

Guarding with a dispatchPrecondition helps catch misuse early since this mutates entries and is intended to run on internalQueue.

 internal mutating func createZeroValueObject(
     forObjectID objectID: String,
     logger: Logger,
     internalQueue: DispatchQueue,
     userCallbackQueue: DispatchQueue,
     clock: SimpleClock,
 ) -> Entry? {
+    dispatchPrecondition(condition: .onQueue(internalQueue))

275-281: Add queue precondition to nosync_applySyncObjectsPool

This function mutates the pool and calls nosync_ APIs; assert we’re on internalQueue.

 internal mutating func nosync_applySyncObjectsPool(
     _ syncObjectsPool: [SyncObjectsPoolEntry],
     logger: Logger,
     internalQueue: DispatchQueue,
     userCallbackQueue: DispatchQueue,
     clock: SimpleClock,
 ) {
+    dispatchPrecondition(condition: .onQueue(internalQueue))

387-393: Add queue precondition to nosync_getOrCreateCounter

Ensures all mutations happen on the intended serial queue.

 internal mutating func nosync_getOrCreateCounter(
     creationOperation: ObjectCreationHelpers.CounterCreationOperation,
     logger: Logger,
     internalQueue: DispatchQueue,
     userCallbackQueue: DispatchQueue,
     clock: SimpleClock,
 ) -> InternalDefaultLiveCounter {
+    dispatchPrecondition(condition: .onQueue(internalQueue))

434-440: Add queue precondition to nosync_getOrCreateMap

Same rationale as for the counter path.

 internal mutating func nosync_getOrCreateMap(
     creationOperation: ObjectCreationHelpers.MapCreationOperation,
     logger: Logger,
     internalQueue: DispatchQueue,
     userCallbackQueue: DispatchQueue,
     clock: SimpleClock,
 ) -> InternalDefaultLiveMap {
+    dispatchPrecondition(condition: .onQueue(internalQueue))
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (3)

91-96: Remove testsOnly_ doc comment

Avoid generic explanatory comments on testsOnly_ accessors.

As per coding guidelines

-    /// Test-only accessor for objectID that handles locking internally.
     internal var testsOnly_objectID: String {
         mutableStateMutex.withSync { mutableState in
             mutableState.liveObjectMutableState.objectID
         }
     }

278-283: Remove testsOnly_ doc comment

Same guideline applies here.

As per coding guidelines

-    /// Test-only accessor for isTombstone that handles locking internally.
     internal var testsOnly_isTombstone: Bool {
         mutableStateMutex.withSync { mutableState in
             mutableState.liveObjectMutableState.isTombstone
         }
     }

292-297: Remove testsOnly_ doc comment

Same guideline applies here.

As per coding guidelines

-    /// Test-only accessor for tombstonedAt that handles locking internally.
     internal var testsOnly_tombstonedAt: Date? {
         mutableStateMutex.withSync { mutableState in
             mutableState.liveObjectMutableState.tombstonedAt
         }
     }
Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (1)

24-31: Avoid force-cast; fail with clearer message if the stored plugin data is wrong

Safer unwrap improves diagnostics without changing behavior.

Apply this diff:

-        // swiftlint:disable:next force_cast
-        return pluginData as! InternalDefaultRealtimeObjects
+        guard let objects = pluginData as? InternalDefaultRealtimeObjects else {
+            preconditionFailure("LiveObjects plugin data had unexpected type; ensure nosync_prepare stored InternalDefaultRealtimeObjects")
+        }
+        return objects
Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (3)

236-241: Align naming: use nosync_creationOperationForLiveCounter for consistency

Map creation uses the nosync_ variant; counter should too for API consistency.

Apply this diff:

-            let creationOperation = ObjectCreationHelpers.creationOperationForLiveCounter(
+            let creationOperation = ObjectCreationHelpers.nosync_creationOperationForLiveCounter(
                 count: count,
                 timestamp: timestamp,
             )

451-457: Minor log message typo

Missing closing parenthesis in the log string.

Apply this diff:

-            logger.log("onChannelAttached(hasObjects: \(hasObjects)", level: .debug)
+            logger.log("onChannelAttached(hasObjects: \(hasObjects))", level: .debug)

54-57: Drop generic docs on testsOnly_ properties per guidelines

Per our coding guidelines, avoid explanatory docs on testsOnly_ properties in Sources. Consider removing these doc comments.

As an example, remove the doc above testsOnly_waitingForSyncEvents:

-    /// Emits an element whenever `getRoot()` starts waiting for the object sync sequence to complete per RTO1c.
     internal var testsOnly_waitingForSyncEvents: AsyncStream<Void> {
         waitingForSyncEvents
     }

Based on coding guidelines

Also applies to: 288-292, 381-383

Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift (2)

16-22: Guard publish handler read with the same lock to avoid races

Setter is locked but the read isn’t; lock the read for symmetry and thread-safety (even in tests).

Apply this diff:

-    func publish(objectMessages: [OutboundObjectMessage]) async throws(InternalError) {
-        if let handler = _publishHandler {
-            try await handler(objectMessages)
-        } else {
-            protocolRequirementNotImplemented()
-        }
-    }
+    func publish(objectMessages: [OutboundObjectMessage]) async throws(InternalError) {
+        let handler = mutex.withLock { _publishHandler }
+        if let handler {
+            try await handler(objectMessages)
+        } else {
+            protocolRequirementNotImplemented()
+        }
+    }

24-26: Implement testsOnly_overridePublish to actually override

Wire this to the backing handler so tests can use it.

Apply this diff:

-    func testsOnly_overridePublish(with _: @escaping ([OutboundObjectMessage]) async throws(InternalError) -> Void) {
-        protocolRequirementNotImplemented()
-    }
+    func testsOnly_overridePublish(with handler: @escaping ([OutboundObjectMessage]) async throws(InternalError) -> Void) {
+        mutex.withLock { _publishHandler = handler }
+    }
Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift (1)

242-245: Prefer explicit Set literal for clarity

Use Set([...]) on the RHS for consistency with the rest of the file and to avoid relying on type inference.

Apply:

-            #expect(Set(pool.entries.keys) == ["root", "counter:hash@456"])
+            #expect(Set(pool.entries.keys) == Set(["root", "counter:hash@456"]))
Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift (2)

351-355: Fix misleading comment about tombstoning

The comment says “tombstonedAt is false” but a Date is passed; this entry is tombstoned.

Apply:

-                    // tombstonedAt is false, so not considered tombstoned
+                    // tombstonedAt is set, so considered tombstoned

1122-1128: Correct the comment to reflect the assertion

You state “will be applied” when ts1 < ts2; the code asserts it’s discarded. Update the comment.

Apply:

-            // Apply operation with serial "ts1" which is lexicographically less than existing "ts2" and thus will be applied per RTLO4a (this is a non-pathological case of RTOL4a, that spec point being fully tested elsewhere)
+            // Apply operation with serial "ts1" which is lexicographically less than existing "ts2" and thus will be discarded per RTLO4a (this is a non-pathological case of RTLO4a, that spec point being fully tested elsewhere)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (4)

901-903: Ensure count(where:) is available, or use a stdlib-safe alternative

Sequence.count(where:) isn’t in older stdlib versions. If you don’t define this extension, this won’t compile.

If you want to avoid relying on an extension, use filter.count:

-            return data.values.count { entry in
-                !Self.nosync_isEntryTombstoned(entry, delegate: delegate)
-            }
+            return data.values
+                .filter { !Self.nosync_isEntryTombstoned($0, delegate: delegate) }
+                .count

If you already have a project-wide count(where:) extension, ignore this change. Based on guidelines


114-119: Remove generic comments on testsOnly_ accessors

Per repo guidelines, avoid generic explanatory comments for testsOnly_ properties.

Apply:

-    /// Test-only accessor for objectID that handles locking internally.
     internal var testsOnly_objectID: String {

As per coding guidelines


407-412: Remove generic comments on testsOnly_ accessors

Same guideline applies here.

Apply:

-    /// Test-only accessor for isTombstone that handles locking internally.
     internal var testsOnly_isTombstone: Bool {

As per coding guidelines


421-426: Remove generic comments on testsOnly_ accessors

Same guideline applies here.

Apply:

-    /// Test-only accessor for tombstonedAt that handles locking internally.
     internal var testsOnly_tombstonedAt: Date? {

As per coding guidelines

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5540135 and b86e09a.

📒 Files selected for processing (26)
  • AblyLiveObjects.xcworkspace/xcshareddata/swiftpm/Package.resolved (1 hunks)
  • CONTRIBUTING.md (1 hunks)
  • Package.resolved (1 hunks)
  • Package.swift (1 hunks)
  • Sources/AblyLiveObjects/Internal/CoreSDK.swift (3 hunks)
  • Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (4 hunks)
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (9 hunks)
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (32 hunks)
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (22 hunks)
  • Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift (2 hunks)
  • Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift (1 hunks)
  • Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift (1 hunks)
  • Sources/AblyLiveObjects/Internal/ObjectsPool.swift (22 hunks)
  • Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift (1 hunks)
  • Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift (1 hunks)
  • Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (1 hunks)
  • Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (1 hunks)
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift (22 hunks)
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift (51 hunks)
  • Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift (53 hunks)
  • Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift (3 hunks)
  • Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift (8 hunks)
  • Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift (2 hunks)
  • Tests/AblyLiveObjectsTests/Mocks/MockLiveMapObjectPoolDelegate.swift (1 hunks)
  • Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift (1 hunks)
  • Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift (17 hunks)
🚧 Files skipped from review as they are similar to previous changes (11)
  • AblyLiveObjects.xcworkspace/xcshareddata/swiftpm/Package.resolved
  • Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift
  • Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift
  • Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift
  • Sources/AblyLiveObjects/Internal/ObjectCreationHelpers.swift
  • Package.swift
  • Tests/AblyLiveObjectsTests/Mocks/MockLiveMapObjectPoolDelegate.swift
  • CONTRIBUTING.md
  • Tests/AblyLiveObjectsTests/ObjectCreationHelpersTests.swift
  • Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift
  • Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift
🧰 Additional context used
📓 Path-based instructions (4)
**/*.swift

📄 CodeRabbit inference engine (.cursor/rules/swift.mdc)

**/*.swift: Specify an explicit access control level (SwiftLint explicit_acl) for all declarations in Swift code (tests are exempt)
When extending a type, put the access level on the extension declaration rather than on each member (tests are exempt)
Prefer implicit .init(...) when the type can be inferred in initializer expressions
Prefer enum case shorthand (.caseName) when the type can be inferred
For JSONValue or WireValue, prefer using literal syntax via ExpressibleBy*Literal where possible
Prefer Swift raw string literals for JSON strings instead of escaping double quotes
When an array literal begins with an initializer expression, place the initializer on the line after the opening bracket

Files:

  • Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift
  • Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift
  • Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift
  • Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift
  • Sources/AblyLiveObjects/Internal/CoreSDK.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift
  • Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift
  • Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
  • Sources/AblyLiveObjects/Internal/ObjectsPool.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
Tests/**/*.swift

📄 CodeRabbit inference engine (.cursor/rules/testing.mdc)

Tests/**/*.swift: Use the Swift Testing framework (import Testing), not XCTest, in test files
Do not use fatalError for expectation failures; prefer Swift Testing’s #require
Only add labels to test cases or suites when the label differs from the suite struct or test method name
Tag tests per CONTRIBUTING.md’s "Attributing tests to a spec point" with exact comment format; distinguish @spec vs @specPartial; do not repeat @spec for the same spec point
Add comments in tests to clarify when certain test data is irrelevant to the scenario
In tests, import Ably using import Ably
In tests, import AblyLiveObjects using @testable import AblyLiveObjects
In tests, import _AblyPluginSupportPrivate using import _AblyPluginSupportPrivate (do not use internal import)
When passing a logger to internal components in tests, use TestLogger()
When unwrapping optionals in tests, prefer #require over guard let

Files:

  • Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift
  • Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift
  • Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift
  • Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift
  • Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift
Sources/AblyLiveObjects/**/*.swift

📄 CodeRabbit inference engine (.cursor/rules/swift.mdc)

In AblyLiveObjects library (non-test) code, import modules as: Ably with import Ably, and _AblyPluginSupportPrivate with internal import _AblyPluginSupportPrivate

Files:

  • Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift
  • Sources/AblyLiveObjects/Internal/CoreSDK.swift
  • Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
  • Sources/AblyLiveObjects/Internal/ObjectsPool.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
Sources/**/*.swift

📄 CodeRabbit inference engine (.cursor/rules/testing.mdc)

For testsOnly_ property declarations, do not add generic explanatory comments (their meaning is understood)

Files:

  • Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift
  • Sources/AblyLiveObjects/Internal/CoreSDK.swift
  • Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
  • Sources/AblyLiveObjects/Internal/ObjectsPool.swift
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
🧠 Learnings (1)
📚 Learning: 2025-09-03T11:54:29.136Z
Learnt from: lawrence-forooghian
PR: ably/ably-liveobjects-swift-plugin#85
File: Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift:239-250
Timestamp: 2025-09-03T11:54:29.136Z
Learning: In the ably-liveobjects-swift-plugin codebase (following ably-cocoa conventions), the userCallbackQueue and internalQueue are always different queues by design. This prevents deadlock scenarios with nested withLock calls when callbacks need to update internal state, making the nested withLock pattern safe.

Applied to files:

  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift
🧬 Code graph analysis (12)
Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift (1)
Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (1)
  • withoutSync (43-47)
Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift (5)
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (1)
  • createInternalQueue (688-691)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (3)
  • createZeroValued (66-81)
  • value (100-104)
  • subscribe (150-164)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (3)
  • createZeroValued (87-104)
  • entries (136-140)
  • subscribe (215-229)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (2)
  • createZeroValueObject (222-272)
  • nosync_applySyncObjectsPool (275-376)
Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift (2)
  • ably_syncNoDeadlock (5-8)
  • ably_syncNoDeadlock (11-14)
Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift (2)
Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift (2)
  • ably_syncNoDeadlock (5-8)
  • ably_syncNoDeadlock (11-14)
Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (2)
  • nosync_realtimeObjects (24-32)
  • nosync_realtimeObjects (56-58)
Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift (4)
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (6)
  • createInternalQueue (688-691)
  • counterObjectState (229-244)
  • objectOperation (336-356)
  • counterCreateOperation (374-383)
  • counterOp (386-388)
  • mapCreateOperation (359-371)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (5)
  • createZeroValued (66-81)
  • nosync_replaceData (211-224)
  • value (100-104)
  • nosync_mergeInitialValue (227-231)
  • nosync_apply (248-267)
Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift (2)
  • ably_syncNoDeadlock (5-8)
  • ably_syncNoDeadlock (11-14)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (2)
  • nosync_replaceData (82-107)
  • nosync_apply (33-58)
Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift (7)
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (9)
  • createInternalQueue (688-691)
  • internalMapEntry (408-418)
  • objectState (189-205)
  • mapCreateOperation (359-371)
  • stringMapEntry (421-435)
  • mapObjectState (208-226)
  • internalStringMapEntry (440-454)
  • objectOperation (336-356)
  • counterCreateOperation (374-383)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (6)
  • createZeroValued (66-81)
  • value (100-104)
  • nosync_replaceData (211-224)
  • nosync_mergeInitialValue (227-231)
  • nosync_apply (248-267)
  • subscribe (150-164)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (8)
  • createZeroValued (87-104)
  • get (124-128)
  • nosync_replaceData (277-293)
  • entries (136-140)
  • nosync_mergeInitialValue (296-307)
  • nosync_apply (324-344)
  • subscribe (215-229)
  • set (152-182)
Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift (2)
  • ably_syncNoDeadlock (5-8)
  • ably_syncNoDeadlock (11-14)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (2)
  • nosync_replaceData (82-107)
  • nosync_apply (33-58)
Tests/AblyLiveObjectsTests/Mocks/MockCoreSDK.swift (1)
  • setPublishHandler (33-37)
Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift (1)
  • publishesCorrectObjectMessage (534-560)
Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift (4)
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (1)
  • createInternalQueue (688-691)
Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift (2)
  • ably_syncNoDeadlock (5-8)
  • ably_syncNoDeadlock (11-14)
Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (3)
  • nosync_handleObjectSyncProtocolMessage (123-134)
  • nosync_handleObjectProtocolMessage (111-121)
  • nosync_onChannelAttached (107-109)
Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (7)
  • nosync_handleObjectSyncProtocolMessage (327-339)
  • nosync_handleObjectSyncProtocolMessage (480-580)
  • nosync_handleObjectProtocolMessage (309-320)
  • nosync_handleObjectProtocolMessage (583-613)
  • testsOnly_createZeroValueLiveObject (344-354)
  • nosync_onChannelAttached (294-302)
  • nosync_onChannelAttached (451-477)
Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (1)
Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (6)
  • nosync_onChannelAttached (294-302)
  • nosync_onChannelAttached (451-477)
  • nosync_handleObjectProtocolMessage (309-320)
  • nosync_handleObjectProtocolMessage (583-613)
  • nosync_handleObjectSyncProtocolMessage (327-339)
  • nosync_handleObjectSyncProtocolMessage (480-580)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (5)
Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (2)
  • withSync (25-35)
  • withoutSync (43-47)
Sources/AblyLiveObjects/Internal/CoreSDK.swift (1)
  • nosync_validateChannelState (99-111)
Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift (4)
  • nosync_subscribe (84-100)
  • unsubscribeAll (130-132)
  • on (102-120)
  • offAll (134-136)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (8)
  • unsubscribeAll (231-235)
  • on (237-251)
  • offAll (253-257)
  • nosync_emit (264-268)
  • nosync_replaceData (277-293)
  • nosync_mergeInitialValue (296-307)
  • mergeInitialValue (518-576)
  • nosync_apply (324-344)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (3)
  • nosync_emit (66-73)
  • nosync_replaceData (82-107)
  • nosync_apply (33-58)
Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift (5)
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (1)
  • createInternalQueue (688-691)
Sources/AblyLiveObjects/Utility/DispatchQueue+Extensions.swift (2)
  • ably_syncNoDeadlock (5-8)
  • ably_syncNoDeadlock (11-14)
Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift (4)
  • nosync_subscribe (84-100)
  • emit (138-147)
  • on (102-120)
  • unsubscribeAll (130-132)
Tests/AblyLiveObjectsTests/Helpers/Subscriber.swift (1)
  • createListener (36-58)
Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (1)
  • withSync (25-35)
Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (7)
Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (2)
  • withSync (25-35)
  • withoutSync (43-47)
Tests/AblyLiveObjectsTests/Mocks/MockLiveMapObjectPoolDelegate.swift (1)
  • nosync_getObjectFromPool (21-23)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (3)
  • entries (136-140)
  • on (237-251)
  • nosync_apply (324-344)
Sources/AblyLiveObjects/Internal/CoreSDK.swift (1)
  • nosync_validateChannelState (99-111)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (7)
  • nosync_getOrCreateMap (434-471)
  • nosync_getOrCreateCounter (387-423)
  • createZeroValueObject (222-272)
  • nosync_performGarbageCollection (486-518)
  • nosync_reset (474-483)
  • nosync_applySyncObjectsPool (275-376)
  • nosync_apply (33-58)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (2)
  • on (172-186)
  • nosync_apply (248-267)
Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (3)
  • nosync_onChannelAttached (107-109)
  • nosync_handleObjectProtocolMessage (111-121)
  • nosync_handleObjectSyncProtocolMessage (123-134)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (2)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (5)
  • nosync_apply (248-267)
  • nosync_emit (199-203)
  • nosync_replaceData (211-224)
  • createZeroValued (66-81)
  • nosync_mergeInitialValue (227-231)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (7)
  • nosync_apply (324-344)
  • nosync_emit (264-268)
  • nosync_replaceData (277-293)
  • createZeroValued (87-104)
  • nosync_mergeInitialValue (296-307)
  • nosync_resetData (385-389)
  • nosync_releaseTombstonedEntries (392-396)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (6)
Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (3)
  • nosync_getObjectFromPool (144-148)
  • on (264-278)
  • on (670-688)
Tests/AblyLiveObjectsTests/Mocks/MockLiveMapObjectPoolDelegate.swift (1)
  • nosync_getObjectFromPool (21-23)
Sources/AblyLiveObjects/Utility/DispatchQueueMutex.swift (2)
  • withSync (25-35)
  • withoutSync (43-47)
Sources/AblyLiveObjects/Internal/CoreSDK.swift (1)
  • nosync_validateChannelState (99-111)
Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift (2)
  • nosync_subscribe (84-100)
  • on (102-120)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (1)
  • createZeroValueObject (222-272)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: SPM (Xcode 16.3)
  • GitHub Check: Xcode, iOS (Xcode 16.3)
  • GitHub Check: Xcode, release configuration, macOS (Xcode 16.3)
  • GitHub Check: Xcode, release configuration, tvOS (Xcode 16.3)
  • GitHub Check: Xcode, tvOS (Xcode 16.3)
  • GitHub Check: Xcode, macOS (Xcode 16.3)
  • GitHub Check: Generate code coverage
  • GitHub Check: lint
🔇 Additional comments (23)
Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift (1)

13-16: Good use of ably_syncNoDeadlock to avoid re-entrancy deadlocks

Running the lookup on the client's internal queue behind ably_syncNoDeadlock is the right pattern here.

Package.resolved (1)

2-2: Pin updates acknowledged; verify CI across the pinned commits

Looks fine. Please confirm CI builds and tests pass against these revisions; plan to unpin before release as noted in the PR.

Sources/AblyLiveObjects/Internal/ObjectsPool.swift (2)

32-57: nosync_apply correctly delegates to map/counter variants

The split to nosync_apply for each object type is consistent with the new locking model.


66-73: Deferred updates emitted via nosync_emit is correct

Deferring emission until after pool reconciliation matches spec intent.

Sources/AblyLiveObjects/Internal/CoreSDK.swift (2)

84-86: Channel state via nosync_ accessor looks good

Directly reading via pluginAPI.nosync_state(for:) aligns with the new model.


99-111: nosync_validateChannelState is correct

Validation and error mapping match expectations.

Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (6)

101-104: Value path conforms to nosync/withSync discipline

Reads under withSync then propagates typed error via CoreSDK; good.


108-136: Increment path validates and builds message under lock, publishes outside

This follows spec points and avoids holding the lock during await.


152-164: Subscription wiring consistent with LiveMap

Using withSync at subscribe time and deferring state updates via updateSelfLater is appropriate.


174-185: Lifecycle on(event:) mirrors the subscribe path

Looks correct and consistent.


199-203: Emit on userCallbackQueue withoutSync is correct

Preconditioned on internal queue; emission happens on callback queue.


470-476: nosync_value validates channel state then returns data

Matches RTLC5 behavior.

Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (2)

37-53: LGTM: queues threaded correctly via nosync_prepare

Constructs InternalDefaultRealtimeObjects with internalQueue/callbackQueue and stores it through nosync_setPluginDataValue. Matches the threading model.


146-156: No qualification needed for PluginAPIProtocol
DefaultInternalPlugin.swift has internal import _AblyPluginSupportPrivate, so the unqualified PluginAPIProtocol correctly refers to _AblyPluginSupportPrivate.PluginAPIProtocol.

Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (1)

686-691: LGTM: test queue helper

Creates a unique serial internalQueue with appropriate QoS; fits the new threading model.

Tests/AblyLiveObjectsTests/LiveObjectMutableStateTests.swift (3)

131-133: Replace fatalError with #require in tests

Per testing guidelines, avoid crashing the process.

Apply this diff:

-                    try mutableState.nosync_subscribe(listener: subscriber.createListener(), coreSDK: coreSDK, updateSelfLater: { _ in fatalError("Not expected") })
+                    try mutableState.nosync_subscribe(
+                        listener: subscriber.createListener(),
+                        coreSDK: coreSDK,
+                        updateSelfLater: { _ in #require(false, "Not expected to be invoked") }
+                    )

As per coding guidelines


154-156: Replace fatalError with #require in tests

Same rationale as above.

Apply this diff:

-                    _ = try mutableState.nosync_subscribe(listener: subscriber.createListener(), coreSDK: coreSDK, updateSelfLater: { _ in fatalError("Not expected") })
+                    _ = try mutableState.nosync_subscribe(
+                        listener: subscriber.createListener(),
+                        coreSDK: coreSDK,
+                        updateSelfLater: { _ in #require(false, "Not expected to be invoked") }
+                    )

As per coding guidelines


176-178: Replace fatalError with #require in tests

Same rationale as above.

Apply this diff:

-                    _ = try mutableState.nosync_subscribe(listener: subscriber.createListener(), coreSDK: coreSDK, updateSelfLater: { _ in fatalError("Not expected") })
+                    _ = try mutableState.nosync_subscribe(
+                        listener: subscriber.createListener(),
+                        coreSDK: coreSDK,
+                        updateSelfLater: { _ in #require(false, "Not expected to be invoked") }
+                    )

As per coding guidelines

Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift (1)

109-111: Good use of internalQueue for nosync path

Wrapping nosync_applySyncObjectsPool in internalQueue.ably_syncNoDeadlock is correct and aligns with the intended threading model.

Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift (2)

41-47: LGTM: Sync handling on internalQueue

Using internalQueue.ably_syncNoDeadlock with nosync_handleObjectSyncProtocolMessage is correct and faithfully exercises the internal queue constraints.


155-162: LGTM: Discarding in-flight sync on new sequence

The test accurately validates RTO5a2 behavior with buffering and discard semantics. Nice setup via OBJECT before switching sequences.

Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift (1)

1454-1460: LGTM: Missing @test was addressed

The test now has @test and will execute under the Swift Testing runner.

Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (1)

239-250: Nested withSync in callbacks is acceptable under your queue separation

Given internalQueue and userCallbackQueue are distinct by design, this pattern won’t deadlock. No change needed.

Based on learnings

@lawrence-forooghian lawrence-forooghian merged commit 41c319b into main Sep 29, 2025
18 checks passed
@lawrence-forooghian lawrence-forooghian deleted the 3-decide-how-threading-will-work branch September 29, 2025 11:52
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.

Decide how threading will work

3 participants