Skip to content

[ECO-5384] Implement memory management pattern similar to ably-cocoa #26

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 29, 2025

Conversation

lawrence-forooghian
Copy link
Collaborator

@lawrence-forooghian lawrence-forooghian commented Jul 11, 2025

Note: This PR is based on top of #25; please review that one first.

We adopt a pattern whereby the LiveObjects SDK functions correctly as long as the user is holding a reference to any object vended by the public API of the SDK. We do this by copying the ably-cocoa approach of having separate public and internal versions of objects.

See commit messages for more details.

Resolves #9.

Summary by CodeRabbit

  • New Features

    • Introduced public-facing wrapper classes for LiveObjects, LiveMap, and LiveCounter, ensuring stable identity and improved memory management.
    • Added a centralized store to manage and cache public proxy objects for internal live objects.
    • Enhanced conversion between internal and public live map values.
    • Added new helper utilities for integration testing.
  • Bug Fixes

    • Improved lifecycle and memory management for LiveObjects, ensuring correct deallocation and stable references.
  • Documentation

    • Updated contributing guidelines with a detailed section on memory management and object lifetimes.
  • Refactor

    • Renamed and restructured internal classes and methods for clarity and separation between internal and public APIs.
    • Simplified method signatures by removing unnecessary dependencies and updating parameter passing.
  • Tests

    • Added and updated test suites to verify memory management, object lifetimes, and new public APIs.
    • Refactored test helpers and test cases to align with the new architecture and naming conventions.
  • Chores

    • Updated dependencies and package configurations for test targets.

See accompanying ably-cocoa change.

The additional test dependencies in Package.swift are because I was
getting "Undefined symbol:
_OBJC_CLASS_$_APDefaultPublicRealtimeChannelUnderlyingObjects" when
compiling. Now, not entirely sure _what_ was causing this error, but
given that the tests do indeed import these two packages it seems
reasonable enough to declare them as dependencies.
@github-actions github-actions bot temporarily deployed to staging/pull/26/AblyLiveObjects July 16, 2025 14:47 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/26/AblyLiveObjects July 16, 2025 17:26 Inactive
…WithObjects helper

Preparation for pulling this out to be used more widely.
Will use in some upcoming tests for object lifetimes.
@lawrence-forooghian lawrence-forooghian changed the title [WIP] Memory management [WIP, ECO-5384] Memory management Jul 16, 2025
@lawrence-forooghian lawrence-forooghian changed the title [WIP, ECO-5384] Memory management [WIP, ECO-5384] Implement memory management pattern similar to ably-cocoa Jul 16, 2025
@lawrence-forooghian lawrence-forooghian changed the title [WIP, ECO-5384] Implement memory management pattern similar to ably-cocoa [ECO-5384] Implement memory management pattern similar to ably-cocoa Jul 16, 2025
@github-actions github-actions bot temporarily deployed to staging/pull/26/AblyLiveObjects July 16, 2025 20:48 Inactive
@lawrence-forooghian lawrence-forooghian marked this pull request as ready for review July 16, 2025 20:51
We adopt a pattern whereby the LiveObjects SDK functions correctly as
long as the user is holding a reference to any object vended by the
public API of the SDK. We do this by copying the ably-cocoa approach of
having separate public and internal versions of objects.

All written by me, except for getting Cursor to help with updating the
tests in response to the new way of injecting CoreSDK.

Resolves #9.
@umair-ably
Copy link
Collaborator

@coderabbitai full review

Copy link

coderabbitai bot commented Jul 25, 2025

✅ Actions performed

Full review triggered.

Copy link

coderabbitai bot commented Jul 25, 2025

Walkthrough

This update introduces a comprehensive overhaul of memory management and object lifecycles for Ably LiveObjects in Swift. It removes weak references between core SDK and plugin components, implements strong reference chains from public API objects to internal SDK components, and introduces public proxy wrappers with a shared store to ensure stable public object identity. The change is documented, thoroughly tested (including new lifecycle tests), and reflected in both internal and public-facing APIs.

Changes

File(s) / Group Change Summary
CONTRIBUTING.md Added detailed "Memory management" section, describing internal and public object lifecycles and reference strategies.
Package.swift, ably-cocoa Updated test target dependencies and subproject commit reference.
Sources/AblyLiveObjects/Internal/CoreSDK.swift Replaced weak with strong references for channels and clients in DefaultCoreSDK; updated constructor.
Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift Removed bridging for public channels; switched to internal-only objects; updated method signatures and visibility.
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift Renamed class; removed stored coreSDK; now passed as method argument; updated method signatures.
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift Renamed class; removed stored delegate and coreSDK; now passed as parameters to methods; updated all relevant signatures.
Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift Renamed class; removed stored coreSDK; now passed as parameter to methods; updated method and constructor signatures.
Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift Introduced new internal enum to represent internal live map values with associated convenience accessors.
Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift Updated documentation to use new internal class names.
Sources/AblyLiveObjects/Internal/ObjectsPool.swift Replaced references to old classes with new ones; removed delegate/coreSDK parameters from methods and initializers.
Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift Switched to new public proxy objects; updated property and method signatures for objects access.
Sources/AblyLiveObjects/Public/Plugin.swift Added centralized static property for default plugin API instance.
Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift Added extension to convert internal live map values to public-facing values using shared store.
Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveCounter.swift Added public proxy class for live counters, wrapping internal implementation and delegating calls.
Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveMap.swift Added public proxy class for live maps, wrapping internal implementation and delegating calls.
Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift Added public proxy class for realtime objects, wrapping internal implementation and delegating calls.
Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicObjectsStore.swift Introduced shared store for caching and managing public proxy object identity and lifecycle.
Sources/AblyLiveObjects/Utility/WeakRef.swift Removed protocol-specific weak reference struct; generic remains.
Tests/AblyLiveObjectsTests/AblyLiveObjectsTests.swift Updated to use new public proxy objects and properties in all test cases.
Tests/AblyLiveObjectsTests/Helpers/ClientHelper.swift Added helper for creating clients and channels with LiveObjects for integration tests.
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift Updated factory to use new internal live map class and signatures.
Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift Updated to use new internal counter class and method signatures.
Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift Updated to use new internal map/counter classes and method signatures; renamed suite.
Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift Updated to use new internal realtime objects class and method signatures; renamed suite.
Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift Refactored to use shared client helper and removed redundant lifetime management.
Tests/AblyLiveObjectsTests/ObjectLifetimesTests.swift Added new suite testing public object lifetimes, deallocation, and identity semantics.
Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift Refactored to use new internal classes and simplified method signatures.

Sequence Diagram(s)

sequenceDiagram
    participant UserApp
    participant PublicRealtimeObjects
    participant PublicObjectsStore
    participant InternalRealtimeObjects
    participant CoreSDK
    participant AblyChannel
    participant AblyClient

    UserApp->>AblyChannel: Obtain objects property
    AblyChannel->>PublicObjectsStore: getOrCreateRealtimeObjects(proxying: InternalRealtimeObjects, args)
    PublicObjectsStore->>PublicRealtimeObjects: create or fetch proxy
    PublicRealtimeObjects->>InternalRealtimeObjects: delegate API calls
    PublicRealtimeObjects->>CoreSDK: delegate API calls
    CoreSDK->>AblyChannel: hold strong reference
    AblyChannel->>AblyClient: hold strong reference
    Note right of PublicObjectsStore: Weak references ensure deallocation when user releases public object
Loading
sequenceDiagram
    participant UserApp
    participant PublicLiveMap
    participant PublicObjectsStore
    participant InternalLiveMap
    participant CoreSDK

    UserApp->>PublicRealtimeObjects: getRoot()
    PublicRealtimeObjects->>InternalRealtimeObjects: getRoot(coreSDK)
    InternalRealtimeObjects->>InternalLiveMap: return root map
    PublicRealtimeObjects->>PublicObjectsStore: getOrCreateMap(proxying: InternalLiveMap, args)
    PublicObjectsStore->>PublicLiveMap: create or fetch proxy
    PublicLiveMap->>InternalLiveMap: delegate API calls
    PublicLiveMap->>CoreSDK: pass coreSDK as needed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇
In tunnels deep, where references hide,
We swapped the weak for strong with pride.
Now objects live as long as you care,
With proxies and stores, they’re always there.
When you let go, they softly fade—
Memory managed, cycles allayed!
🌱✨

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 9-memory-management

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 2

🧹 Nitpick comments (8)
CONTRIBUTING.md (1)

59-59: Minor: Add period after "etc" for consistency.

In American English, abbreviations like "etc." should include a period.

-that is, the ably-cocoa components that hold a strong reference to these internal components (that is, the realtime client or channel); thus, they must never hold a strong reference to a `CoreSDK` object
+that is, the ably-cocoa components that hold a strong reference to these internal components (that is, the realtime client or channel); thus, they must never hold a strong reference to a `CoreSDK` object.
Tests/AblyLiveObjectsTests/Helpers/ClientHelper.swift (1)

36-39: Consider adding documentation for PartialClientOptions.

The PartialClientOptions struct would benefit from brief documentation explaining its purpose and when each optional parameter should be used.

+    /// Partial client options for customizing client behavior in tests.
     struct PartialClientOptions: Encodable, Hashable {
+        /// Whether to use binary protocol instead of JSON.
         var useBinaryProtocol: Bool?
+        /// Whether the client should automatically connect on creation.
         var autoConnect: Bool?
     }
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (1)

85-106: Note: Multiple unimplemented methods

Several methods required by the internal API are marked as notYetImplemented(). This is acceptable for an early implementation phase, but these will need to be implemented for the SDK to be fully functional.

Would you like me to open issues to track the implementation of these methods: increment, decrement, subscribe, unsubscribeAll, on, and offAll?

Tests/AblyLiveObjectsTests/ObjectLifetimesTests.swift (2)

138-138: Inconsistency: Missing autoConnect parameter

The first test explicitly sets autoConnect: false, but this line doesn't specify it. According to the comment on line 139, you need the connection for this test, but it would be clearer to explicitly set autoConnect: true here for consistency.

-                let realtime = try await ClientHelper.realtimeWithObjects()
+                let realtime = try await ClientHelper.realtimeWithObjects(options: .init(autoConnect: true))

71-71: Track TODOs for comprehensive test coverage

The TODO comments indicate important missing test coverage:

  • Testing event reception on LiveObjects
  • Testing with non-root LiveMap and counter objects

These are important for validating the complete memory management pattern.

Would you like me to create GitHub issues to track these TODO items for future implementation?

Also applies to: 188-188, 235-235

Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift (1)

878-879: Consider extracting repeated MockCoreSDK initialization.

Multiple test methods create MockCoreSDK(channelState: .attached) instances. Consider extracting this into a test helper to reduce duplication.

+    private static func createAttachedCoreSDK() -> MockCoreSDK {
+        MockCoreSDK(channelState: .attached)
+    }

Then in the tests:

-                let coreSDK = MockCoreSDK(channelState: .attached)
+                let coreSDK = Self.createAttachedCoreSDK()
Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (1)

36-36: Document or use the unused client parameter.

The client parameter is added but marked as unused with _. If this is for interface compatibility, consider adding a comment explaining why it's needed but unused.

-    internal func prepare(_ channel: AblyPlugin.RealtimeChannel, client _: AblyPlugin.RealtimeClient) {
+    // client parameter is required by the LiveObjectsInternalPluginProtocol interface
+    internal func prepare(_ channel: AblyPlugin.RealtimeChannel, client _: AblyPlugin.RealtimeClient) {
Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicObjectsStore.swift (1)

16-17: Consider using os_unfair_lock for better performance.

NSLock is a good choice for thread safety, but os_unfair_lock would provide better performance for the short critical sections in this code.

-    private let mutex = NSLock()
+    private let mutex = os_unfair_lock()

Note: This would require adjusting the lock usage pattern from mutex.withLock to the appropriate os_unfair_lock APIs.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 836f502 and dcbe617.

📒 Files selected for processing (28)
  • CONTRIBUTING.md (1 hunks)
  • Package.swift (1 hunks)
  • Sources/AblyLiveObjects/Internal/CoreSDK.swift (1 hunks)
  • Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift (1 hunks)
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (3 hunks)
  • Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (6 hunks)
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (5 hunks)
  • Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift (1 hunks)
  • Sources/AblyLiveObjects/Internal/LiveObjectMutableState.swift (1 hunks)
  • Sources/AblyLiveObjects/Internal/ObjectsPool.swift (6 hunks)
  • Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift (1 hunks)
  • Sources/AblyLiveObjects/Public/Plugin.swift (1 hunks)
  • Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift (1 hunks)
  • Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveCounter.swift (1 hunks)
  • Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveMap.swift (1 hunks)
  • Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift (1 hunks)
  • Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicObjectsStore.swift (1 hunks)
  • Sources/AblyLiveObjects/Utility/WeakRef.swift (0 hunks)
  • Tests/AblyLiveObjectsTests/AblyLiveObjectsTests.swift (4 hunks)
  • Tests/AblyLiveObjectsTests/Helpers/ClientHelper.swift (1 hunks)
  • Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (1 hunks)
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift (19 hunks)
  • Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift (52 hunks)
  • Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift (40 hunks)
  • Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift (6 hunks)
  • Tests/AblyLiveObjectsTests/ObjectLifetimesTests.swift (1 hunks)
  • Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift (18 hunks)
  • ably-cocoa (1 hunks)
💤 Files with no reviewable changes (1)
  • Sources/AblyLiveObjects/Utility/WeakRef.swift
🧰 Additional context used
🧬 Code Graph Analysis (6)
Tests/AblyLiveObjectsTests/ObjectLifetimesTests.swift (5)
Tests/AblyLiveObjectsTests/Helpers/ClientHelper.swift (2)
  • realtimeWithObjects (7-27)
  • channelOptionsWithObjects (30-34)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (2)
  • get (96-118)
  • on (194-196)
Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveMap.swift (2)
  • get (25-32)
  • on (91-93)
Tests/AblyLiveObjectsTests/Helpers/Ably+Concurrency.swift (1)
  • attachAsync (6-16)
Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift (2)
  • getRoot (23-32)
  • on (54-56)
Sources/AblyLiveObjects/Internal/InternalLiveMapValue.swift (1)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (1)
  • value (68-83)
Tests/AblyLiveObjectsTests/Helpers/ClientHelper.swift (2)
Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift (2)
  • realtimeWithObjects (12-14)
  • channelOptionsWithObjects (16-18)
Tests/AblyLiveObjectsTests/Helpers/Sandbox.swift (1)
  • fetchSharedAPIKey (89-91)
Sources/AblyLiveObjects/Internal/ObjectsPool.swift (3)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveMap.swift (4)
  • entries (141-166)
  • createZeroValued (80-91)
  • replaceData (208-216)
  • replaceData (306-328)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (3)
  • createZeroValued (55-64)
  • replaceData (112-116)
  • replaceData (167-181)
Tests/AblyLiveObjectsTests/Helpers/TestFactories.swift (2)
  • objectState (189-205)
  • objectsMap (495-503)
Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicObjectsStore.swift (1)
Sources/AblyLiveObjects/Internal/InternalDefaultLiveCounter.swift (1)
  • value (68-83)
Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift (1)
Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicObjectsStore.swift (4)
  • getOrCreateMap (49-53)
  • getOrCreateMap (116-127)
  • getOrCreateCounter (37-41)
  • getOrCreateCounter (104-114)
🪛 LanguageTool
CONTRIBUTING.md

[style] ~50-~50: Consider removing “of” to be more concise
Context: ... similar to that of ably-cocoa: we keep all of the internal components of the SDK alive as...

(ALL_OF_THE)


[style] ~52-~52: Consider removing “of” to be more concise
Context: ...pes which maintain strong references to all of the internal components of the SDK which ar...

(ALL_OF_THE)


[style] ~57-~57: Consider a more concise word here.
Context: ...ts The key rules that must be followed in order to avoid a strong reference cycle are that...

(IN_ORDER_TO_PREMIUM)


[style] ~57-~57: In American English, abbreviations like “etc.” require a period.
Context: ...asses (that is InternalDefaultLiveMap etc) _must never hold a strong reference to...

(ETC_PERIOD)

🔇 Additional comments (53)
ably-cocoa (1)

1-1: Submodule pointer looks fine – please verify the hash is pushed and CI pulls it correctly

This file only updates the ably-cocoa submodule to commit 4e0601a4567235a2e5afd36eb19d363057b642da.
Double-check that:

  1. The referenced commit exists on the remote (not a local‐only WIP).
  2. .gitmodules hasn’t changed path or branch inadvertently.
  3. CI workflows include git submodule update --init --recursive so builds pick up the new revision.

No other issues spotted here.

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

5-5: Documentation accurately reflects the internal class renaming.

The update correctly changes the class references from DefaultLiveCounter and DefaultLiveMap to InternalDefaultLiveCounter and InternalDefaultLiveMap, aligning with the memory management refactor that separates internal and public API classes.

Package.swift (1)

59-66: Test dependencies correctly added for the refactored API.

Adding "Ably" and "AblyPlugin" dependencies to the test target is appropriate given the introduction of public wrapper classes and internal API changes that the tests need to verify directly.

Sources/AblyLiveObjects/Public/Plugin.swift (2)

26-26: Good centralization of plugin API instance.

Introducing the defaultPluginAPI constant centralizes plugin API creation, improving testability and making it easier to inject different implementations if needed.


30-30: Consistent usage of centralized plugin API.

Using the centralized defaultPluginAPI constant instead of direct instantiation maintains consistency and supports the dependency injection pattern.

Tests/AblyLiveObjectsTests/AblyLiveObjectsTests.swift (2)

23-23: Type check correctly updated for public wrapper class.

The test now correctly expects PublicDefaultRealtimeObjects instead of the internal DefaultRealtimeObjects, reflecting the new public API wrapper pattern.


82-82: Test access properties correctly updated for new architecture.

The changes from testsOnly_internallyTypedObjects to testsOnly_nonTypeErasedObjects appropriately reflect the new memory management pattern where public objects wrap internal implementations. The test functionality remains intact while working through the updated public API.

Also applies to: 85-85, 93-93, 104-104, 113-113

Tests/AblyLiveObjectsTests/InternalDefaultLiveCounterTests.swift (2)

13-14: LGTM! Correct adaptation to the new memory management pattern.

The test correctly creates the coreSDK instance separately and passes it as a parameter to the value() method, following the new pattern where internal objects don't hold references to their dependencies.

Also applies to: 17-17


272-272: LGTM! Simplified ObjectsPool initialization.

The removal of rootDelegate and rootCoreSDK parameters aligns with the new memory management approach where dependencies are passed explicitly during method calls rather than stored.

Also applies to: 273-273

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

4-8: Well-designed internal value representation.

The enum correctly encapsulates the different types of values that can be stored in a live map, with appropriate use of Sendable for thread safety. This aligns well with the new memory management pattern where internal types don't hold references to dependencies.


36-53: Clean and idiomatic convenience API.

The convenience getters provide an elegant way to access specific value types, properly delegating to PrimitiveObjectValue for primitive type extraction. The consistent pattern of returning nil for non-matching cases is appropriate.

CONTRIBUTING.md (1)

48-69: Clear and comprehensive memory management documentation.

The documentation effectively explains the SDK's memory management approach, including:

  • The rationale for keeping internal components alive through strong references
  • Clear rules to avoid reference cycles
  • The distinction between internal and public types
  • References to relevant code and tests

This will be valuable for contributors understanding the architecture.

Tests/AblyLiveObjectsTests/ObjectsPoolTests.swift (2)

12-18: Correct usage of the new internal types.

The test properly uses InternalDefaultLiveMap.createZeroValued and correctly demonstrates that existing objects in the pool are returned by reference.


251-328: Excellent comprehensive sync scenario test.

This test effectively covers the complete sync behavior including:

  • Updating existing objects
  • Creating new objects
  • Removing objects not in sync
  • Preserving the root object

The explicit dependency passing pattern is correctly applied throughout.

Tests/AblyLiveObjectsTests/InternalDefaultLiveMapTests.swift (3)

13-24: Proper error handling test with explicit dependency injection.

The test correctly validates that operations throw appropriate errors when the channel is in DETACHED or FAILED state, properly passing the MockCoreSDK instance with the test state.


540-546: Good test of zero-value object creation.

The test properly verifies that MAP_SET operations create zero-value objects in the pool when referencing object IDs, which is crucial for the lazy initialization pattern.


579-615: Excellent test verifying RTO6a behavior.

This test effectively validates that existing objects in the pool are not replaced when referenced by MAP_SET operations, which is crucial for maintaining object identity and preventing unnecessary recreations.

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

1-40: Well-implemented test helper utility.

This new ClientHelper provides a clean, reusable interface for creating LiveObjects-enabled clients in integration tests. The implementation correctly handles all necessary configuration including plugin setup, sandbox environment, and optional parameters.

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

15-27: No strong reference cycles introduced.

The switch to strong references in DefaultCoreSDK is correctly applied and does not create retain cycles:

  • DefaultCoreSDK now holds strong references to channel, client, and pluginAPI (Sources/AblyLiveObjects/Internal/CoreSDK.swift).
  • Public proxies (e.g. PublicDefaultLiveMap, PublicDefaultLiveCounter) hold CoreSDK strongly, with no back-references from DefaultCoreSDK.
  • Searches (rg/ast-grep) confirmed no internal component retains DefaultCoreSDK.

All good to merge.

Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveCounter.swift (2)

1-52: Well-implemented public proxy wrapper.

This PublicDefaultLiveCounter correctly implements the proxy pattern, holding strong references to both the internal implementation and CoreSDK while delegating all LiveCounter protocol methods. The design ensures proper memory management and provides a stable public API.


23-35: No change needed for CoreSDK parameter passing

The internal InternalDefaultLiveCounter defines:

  • func value(coreSDK: CoreSDK) throws -> Double
  • func increment(amount: Double) async throws
  • func decrement(amount: Double) async throws

The public proxies correctly match those signatures:

  • proxied.value(coreSDK: coreSDK)
  • proxied.increment(amount: amount)
  • proxied.decrement(amount: amount)

There is no inconsistency—increment and decrement intentionally don’t take a CoreSDK parameter. Please disregard the original suggestion.

Likely an incorrect or invalid review comment.

Sources/AblyLiveObjects/Public/ARTRealtimeChannel+Objects.swift (2)

29-32: Good test accessibility with clear naming.

The testsOnly_nonTypeErasedObjects property provides appropriate test access while clearly indicating its test-only purpose through naming conventions.


10-27: Thread safety of PublicObjectsStore confirmed

The PublicObjectsStore safely synchronizes access to its mutable state using an NSLock and the mutex.withLock { … } pattern in each getOrCreate… method. No further changes are needed.

  • Internal mutex = NSLock() with mutableState protected by mutex.withLock { … }
  • Class is marked Sendable and uses a private lock for all mutations
Sources/AblyLiveObjects/Public/Public Proxy Objects/InternalLiveMapValue+ToPublic.swift (2)

1-38: Excellent conversion layer implementation.

This extension provides a clean and well-structured mechanism for converting internal live map values to their public counterparts. The implementation correctly:

  • Uses PublicObjectsStore for consistent proxy object identity
  • Handles all enum cases appropriately (primitives pass-through, live objects get proxied)
  • Encapsulates creation dependencies in a clear PublicValueCreationArgs structure
  • Maintains the memory management guarantees of the new architecture

4-15: Well-designed creation arguments pattern.

The PublicValueCreationArgs struct with computed properties for specific creation args provides a clean way to pass dependencies while maintaining type safety and avoiding parameter proliferation.

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

5-6: LGTM! Clean separation of internal implementation from public API

The renaming to InternalDefaultLiveCounter and change to Sendable conformance clearly indicates this is an internal implementation class. This aligns well with the memory management pattern where public proxy objects wrap these internal implementations.


68-83: Good architectural change: Explicit dependency passing

Converting from a stored coreSDK property to an explicit parameter improves testability and reduces coupling. The error handling for invalid channel states is preserved correctly.

Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultLiveMap.swift (3)

1-22: Well-structured proxy implementation with proper lifecycle management

The implementation correctly maintains strong references to dependencies (coreSDK and delegate) as documented in the comments. This ensures the internal components remain alive while the public object exists, which is essential for the memory management pattern.


6-6: Verify: Should this be public instead of internal?

The class is in the "Public Proxy Objects" directory but is marked as internal. Is this intentional for the current implementation phase, or should it be public to match its role as a public-facing API?


25-32: Consistent and correct delegation pattern

All methods properly delegate to the internal implementation and handle type conversion using toPublic. The error propagation with throws(ARTErrorInfo) is handled correctly throughout.

Also applies to: 40-54, 63-72

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

4-5: Consistent refactoring: Internal class with explicit dependencies

The renaming and protocol changes align with the broader refactoring pattern. The class now clearly indicates its internal nature and properly implements Sendable for thread safety.


72-78: Simplified initialization without stored dependencies

The constructor no longer requires coreSDK or delegate parameters, which simplifies object creation and reduces coupling. The ObjectsPool is initialized with just the logger.


90-99: Clean parameter passing for channel state validation

The getRoot method now receives coreSDK as a parameter, allowing it to check the channel state without maintaining a stored reference. The error handling for invalid channel states is preserved correctly.

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

9-10: Consistent type updates to use internal classes

All type references have been correctly updated from DefaultLiveMap/DefaultLiveCounter to their Internal counterparts. The convenience getters maintain the same functionality with the new types.

Also applies to: 13-23, 91-91


85-85: Simplified object creation without dependency injection

The removal of delegate and coreSDK parameters from createZeroValued calls aligns with the architectural change where these dependencies are passed explicitly to methods rather than stored in objects. This simplifies the object creation logic.

Also applies to: 130-130, 132-132, 177-186

Tests/AblyLiveObjectsTests/ObjectLifetimesTests.swift (2)

6-99: Excellent test coverage for memory management with public objects property

This test thoroughly validates the memory management pattern by:

  • Using weak references to track object lifetimes
  • Ensuring internal dependencies remain alive while public references exist
  • Properly handling async deallocation via dispatch queues
  • Verifying cleanup after releasing references

The test structure is clear and well-documented.


220-236: Good validation of object identity stability

This test confirms that the public objects maintain stable pointer identity across multiple accesses, which is crucial for the caching mechanism in PublicObjectsStore.

Tests/AblyLiveObjectsTests/InternalDefaultRealtimeObjectsTests.swift (2)

10-14: LGTM! Simplified test helper aligns with the refactor.

The removal of the channelState parameter is consistent with the architectural change where InternalDefaultRealtimeObjects no longer stores channel state internally.


511-514: Explicit dependency injection pattern correctly implemented.

The pattern of creating a MockCoreSDK instance and passing it explicitly to methods is consistently applied throughout the tests. This aligns well with the documented memory management approach where internal objects don't hold strong references.

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

12-14: Good consolidation of client helper functions.

Delegating to ClientHelper methods reduces duplication and centralizes client setup logic.


179-180: Simplified test case initialization with boolean flag.

Using the boolean directly instead of constructing intermediate option instances improves readability.

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

23-23: Appropriate visibility change for internal architecture.

Changing the visibility from private to internal for realtimeObjects method allows proper access for the new public proxy pattern while maintaining encapsulation.

Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicDefaultRealtimeObjects.swift (1)

64-65: Good separation of test-only APIs.

The clear documentation and naming convention (testsOnly_ prefix) for test-only properties helps prevent accidental usage in production code.

Sources/AblyLiveObjects/Public/Public Proxy Objects/PublicObjectsStore.swift (2)

85-89: Efficient cleanup of deallocated entries.

The removeDeallocatedEntries method correctly filters out entries with nil weak references. This prevents memory leaks from accumulating stale dictionary entries.


65-83: Well-implemented get-or-create pattern with proper cleanup.

The implementation correctly:

  1. Cleans up deallocated entries before checking
  2. Returns existing proxies when available
  3. Creates and stores new proxies only when needed
  4. Uses weak references to avoid retain cycles

This ensures stable object identity while respecting user ownership.

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

10-11: LGTM! Clear documentation and appropriate architectural changes.

The rename to InternalDefaultLiveMap and conformance change to Sendable properly reflect this class's new role as an internal implementation backing the public API.


93-93: Good documentation structure.

The section header clearly delineates the internal methods that implement the LiveMap functionality.


95-96: Consistent implementation of explicit dependency injection.

The method signature change properly implements the new pattern of passing dependencies explicitly rather than storing them as properties.

Also applies to: 117-117


120-138: Proper conversion from computed property to method.

The size method correctly implements the explicit dependency pattern while preserving all the required business logic and error handling.


141-165: Well-implemented conversion with proper delegate passing.

The entries method correctly implements the new signature pattern and properly passes the delegate parameter to convertEntryToLiveMapValue for each entry.


168-176: Efficient delegation pattern for keys and values methods.

Both methods correctly leverage the entries implementation, avoiding code duplication while maintaining the proper parameter passing pattern.


455-455: Consistent with simplified object creation pattern.

The removal of mapDelegate and coreSDK parameters from createZeroValueObject aligns with the overall memory management improvements.


553-553: Proper explicit dependency injection in helper method.

The convertEntryToLiveMapValue method correctly receives and uses the delegate parameter, maintaining functionality while improving memory management.

Also applies to: 591-591

Base automatically changed from take-abstract-channel to main July 29, 2025 08:07
Copy link
Collaborator

@umair-ably umair-ably left a comment

Choose a reason for hiding this comment

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

LGTM - again, i just skimmed across the tests

@lawrence-forooghian lawrence-forooghian merged commit e71ff0c into main Jul 29, 2025
17 checks passed
@lawrence-forooghian lawrence-forooghian deleted the 9-memory-management branch July 29, 2025 09:25
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 approach to memory management
2 participants