Skip to content

[ECO-5251] Messages tests (single channel)#249

Merged
maratal merged 1 commit intomainfrom
fix/235-messages-tests
Jun 30, 2025
Merged

[ECO-5251] Messages tests (single channel)#249
maratal merged 1 commit intomainfrom
fix/235-messages-tests

Conversation

@maratal
Copy link
Copy Markdown
Collaborator

@maratal maratal commented Mar 30, 2025

Part of #235

Summary by CodeRabbit

  • New Features

    • Refactored subscription APIs across chat components to use callback-based subscriptions with explicit unsubscription handles, improving control and resource management.
    • Added support for discontinuity event subscriptions with callback and async sequence interfaces.
    • Introduced utility functions for delayed and periodic execution on the main actor.
    • Enhanced mock classes with callback-based subscription patterns and enriched mock data for more realistic testing.
    • Improved chat room event handling by replacing async sequence listeners with callback-driven subscriptions.
  • Bug Fixes

    • Prevented malformed realtime events from being emitted to subscribers, enhancing message reliability.
    • Fixed pagination logic in message mocks to better simulate last-page detection and data consistency.
  • Chores

    • Updated test suites with comprehensive coverage of message sending, subscription points, error propagation, and event reception.
    • Switched internal error equality checks to support better error comparisons.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2025

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.
## Walkthrough
This pull request adds Equatable support to the `InternalError` enum by comparing error details, restructures and renames several tests in the default messages suite to validate REST API message sending and error handling, refactors the mock paginated response by removing an obsolete property and updating its logic, and enhances realtime mocks by adding call recording and improved state change handling with thread-safe callback mechanisms. It also refactors subscription APIs across core components from async sequence-based to callback-based with explicit `SubscriptionHandle`s, introduces new subscription storage classes, updates the `Room` and `RoomLifecycleManager` protocols and implementations accordingly, and adds a new discontinuity event subscription mechanism.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| Sources/AblyChat/InternalError.swift | Added an extension for `InternalError` to conform to Equatable by defining a static `==` that compares `toARTErrorInfo()` results. |
| Tests/AblyChatTests/DefaultMessagesTests.swift | Restructured tests: renamed and added methods to verify REST Chat API message sending, proper error handling, subscription point determination based on realtime channel state, and filtering of malformed messages. |
| Tests/AblyChatTests/Mocks/MockHTTPPaginatedResponse.swift | Removed the `_isLast` property; updated the initializer to derive `isLast` from `_hasNext` and modified the static `nextPage` instance to include new fields and adjusted pagination logic. |
| Tests/AblyChatTests/Mocks/MockRealtime.swift | Added a `callRecorder` property to log `request` calls in `MockRealtime`. |
| Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift | Enhanced `MockRealtimeChannel` with new state change and message JSON properties, updated `on` methods to store callbacks and immediately invoke them if a state change is preset, and improved subscription behavior to emit constructed messages from JSON. |
| Example/AblyChatExample/ContentView.swift | Refactored event subscriptions from async sequence iteration to callback-based subscriptions with explicit unsubscription, simplifying message and event handling logic. |
| Example/AblyChatExample/Misc/Utils.swift | Added utility async functions `after` and `periodic` to schedule closures on the main actor after delays or periodically. |
| Example/AblyChatExample/Mocks/MockClients.swift | Refactored mock client classes to use callback-based subscription methods returning `SubscriptionHandle` instead of async sequences, unifying subscription patterns. |
| Example/AblyChatExample/Mocks/MockSubscriptionStorage.swift | Added new classes `MockSubscriptionHandleStorage` and `MockMessageSubscriptionHandleStorage` to manage callback-based subscriptions with handles and periodic event emission. |
| Sources/AblyChat/Connection.swift | Changed `Connection` protocol subscription method to callback-based with `SubscriptionHandle`, providing async sequence subscription as a default extension. |
| Sources/AblyChat/DefaultConnection.swift | Updated `DefaultConnection` to use callback-based subscription with `SubscriptionHandle` for connection status changes instead of async sequence subscription. |
| Sources/AblyChat/DefaultMessages.swift | Refactored `DefaultMessages` subscription method to callback-based returning `MessageSubscriptionHandle`, removing buffering policy and async sequence subscription. |
| Sources/AblyChat/DefaultOccupancy.swift | Changed occupancy subscription to callback-based with `SubscriptionHandle`, removing async sequence subscription and buffering policy. |
| Sources/AblyChat/DefaultPresence.swift | Changed presence subscription methods to callback-based with `SubscriptionHandle`, removing buffering policy and async sequence subscription. |
| Sources/AblyChat/DefaultRoomReactions.swift | Changed room reactions subscription to callback-based with `SubscriptionHandle`, removing buffering policy and async sequence subscription. |
| Sources/AblyChat/DefaultTyping.swift | Changed typing subscription to callback-based with `SubscriptionHandle`, removing buffering policy and async sequence subscription. |
| Sources/AblyChat/Discontinuity.swift | Added new `DiscontinuityEvent` struct and `ProvidesDiscontinuity` protocol with callback-based subscription method and async sequence default implementations. |
| Sources/AblyChat/DiscontinuityEvent.swift | Deleted old `DiscontinuityEvent` struct definition (moved to `Discontinuity.swift`). |
| Sources/AblyChat/Messages.swift | Changed `Messages` protocol subscription to callback-based with `MessageSubscriptionHandle`, added default async sequence subscription implementation. |
| Sources/AblyChat/Occupancy.swift | Changed `Occupancy` protocol subscription to callback-based with `SubscriptionHandle`, added default async sequence subscription implementation. |
| Sources/AblyChat/Presence.swift | Changed `Presence` protocol subscription methods to callback-based with `SubscriptionHandle`, added default async sequence subscription implementations. |
| Sources/AblyChat/Room.swift | Extended `Room` protocol to conform to `ProvidesDiscontinuity`, changed subscription methods for status changes and discontinuity events to callback-based with `SubscriptionHandle`, removed async sequence subscription methods. |
| Sources/AblyChat/RoomLifecycleManager.swift | Extended `RoomLifecycleManager` protocol to conform to `ProvidesDiscontinuity`, changed subscription methods for room status and discontinuity events to callback-based with `SubscriptionHandle`, updated waiting logic to use continuations. |
| Sources/AblyChat/RoomReactions.swift | Added callback-based subscription method with `SubscriptionHandle` to `RoomReactions` protocol, added default async sequence subscription implementations. |
| Sources/AblyChat/Subscription.swift | Added new `SubscriptionHandle` struct encapsulating an unsubscribe closure. |
| Sources/AblyChat/SubscriptionHandleStorage.swift | Added internal `SubscriptionHandleStorage` class to manage callback-based subscriptions with handles and emit events. |
| Sources/AblyChat/Typing.swift | Changed `Typing` protocol subscription to callback-based with `SubscriptionHandle`, added default async sequence subscription implementations. |
| Tests/AblyChatTests/Helpers/Subscription+RoomStatusChange.swift | Added extension on `RoomLifecycleManager` providing async sequence subscription methods for room status changes with buffering policy. |
| Tests/AblyChatTests/Mocks/MockRoom.swift | Added stub callback-based subscription methods `onStatusChange` and `onDiscontinuity` returning `SubscriptionHandle`. |
| Tests/AblyChatTests/Mocks/MockRoomLifecycleManager.swift | Replaced subscription storage with `SubscriptionHandleStorage`, changed subscription methods to callback-based returning `SubscriptionHandle`. Removed old async sequence subscription methods. |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
  participant Client
  participant RoomLifecycleManager
  participant SubscriptionHandleStorage

  Client->>RoomLifecycleManager: onRoomStatusChange(callback)
  RoomLifecycleManager->>SubscriptionHandleStorage: create(callback)
  SubscriptionHandleStorage-->>RoomLifecycleManager: SubscriptionHandle(unsubscribe)
  RoomLifecycleManager-->>Client: SubscriptionHandle

  Note over SubscriptionHandleStorage, Client: When event occurs
  SubscriptionHandleStorage->>Client: callback(event)

  Client->>SubscriptionHandleStorage: unsubscribe()
  SubscriptionHandleStorage-->>Client: removes subscription
sequenceDiagram
  participant Client
  participant DefaultMessages
  participant RealtimeChannel

  Client->>DefaultMessages: subscribe(callback)
  DefaultMessages->>RealtimeChannel: on("chatMessage", listener)
  RealtimeChannel-->>DefaultMessages: message event
  DefaultMessages->>Client: callback(message)
  
  Client->>DefaultMessages: unsubscribe()
  DefaultMessages->>RealtimeChannel: off("chatMessage", listener)
Loading

Assessment against linked issues

Objective Addressed Explanation
Restore messages tests (ECO-5251)
Restore presence and room reactions tests (ECO-5251) Tests for presence and room reactions were restructured and expanded in DefaultMessagesTests and mocks.

Possibly related PRs

Suggested reviewers

  • umair-ably
  • lawrence-forooghian

Poem

I hopped through lines of code with delight,
Equatable errors shining ever so bright.
Messages now restored and tests refined,
Mocks recording calls, state changes aligned.
In a world of code, this rabbit sings in flight!
🐇💻🌟


<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

<details open="true">
<summary>🧪 Generate Unit Tests</summary>

- [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Create PR with Unit Tests
- [ ] <!-- {"checkboxId": "07f1e7d6-8a8e-4e23-9900-8731c2c87f58", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Post Copyable Unit Tests in Comment
- [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Commit Unit Tests in branch `fix/235-messages-tests`

</details>

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

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.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=ably/ably-chat-swift&utm_content=249):

- 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](https://www.coderabbit.ai/contact-us/support) 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](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai auto-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](https://docs.coderabbit.ai/guides/configure-coderabbit) 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](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

@github-actions github-actions bot temporarily deployed to staging/pull/249/AblyChat March 30, 2025 23:26 Inactive
@maratal maratal changed the base branch from main to fix/235-presence-tests March 30, 2025 23:41
@maratal maratal changed the title Add messages tests [ECO-5251] Messages tests Apr 5, 2025
@github-actions github-actions bot temporarily deployed to staging/pull/249/AblyChat April 11, 2025 12:42 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/249/AblyChat April 11, 2025 12:51 Inactive
@maratal maratal force-pushed the fix/235-messages-tests branch from 5c4b962 to e946995 Compare April 11, 2025 12:54
@github-actions github-actions bot temporarily deployed to staging/pull/249/AblyChat April 11, 2025 12:56 Inactive
@maratal maratal marked this pull request as ready for review April 11, 2025 12:59
@maratal
Copy link
Copy Markdown
Collaborator Author

maratal commented Apr 11, 2025

With recent changes in main re using @MainActor for SDK classes all tests now pass. FYI @umair-ably @lawrence-forooghian

Copy link
Copy Markdown

@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: 0

🧹 Nitpick comments (1)
Tests/AblyChatTests/DefaultMessagesTests.swift (1)

13-21: Test method structure looks clear.
The "Given-When-Then" format is well-defined, and the assertion on the recorded HTTP request (#expect usage) aligns with verifying the request path, method, and payload. As an optional improvement, consider verifying aspects of the response or returned message to ensure all round-trip behavior is tested where possible.

Also applies to: 23-24, 26-32

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between b3427a1 and e946995.

📒 Files selected for processing (5)
  • Sources/AblyChat/InternalError.swift (1 hunks)
  • Tests/AblyChatTests/DefaultMessagesTests.swift (5 hunks)
  • Tests/AblyChatTests/Mocks/MockHTTPPaginatedResponse.swift (4 hunks)
  • Tests/AblyChatTests/Mocks/MockRealtime.swift (2 hunks)
  • Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift (5 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
Sources/AblyChat/InternalError.swift (2)
Tests/AblyChatTests/Helpers/Helpers.swift (1)
  • lhs (127-132)
Sources/AblyChat/PaginatedResult.swift (1)
  • lhs (99-104)
Tests/AblyChatTests/DefaultMessagesTests.swift (8)
Sources/AblyChat/DefaultMessages.swift (10)
  • send (36-38)
  • send (182-188)
  • subscribe (28-30)
  • subscribe (76-171)
  • update (40-42)
  • update (190-196)
  • get (32-34)
  • get (174-180)
  • onDiscontinuity (48-50)
  • onDiscontinuity (207-209)
Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift (2)
  • subscribe (137-161)
  • get (76-83)
Tests/AblyChatTests/Mocks/MockRealtimePresence.swift (5)
  • subscribe (7-9)
  • subscribe (11-13)
  • update (53-58)
  • get (23-29)
  • get (31-37)
Sources/AblyChat/Messages.swift (1)
  • getPreviousMessages (313-315)
Tests/AblyChatTests/Mocks/MockHTTPPaginatedResponse.swift (1)
  • first (50-52)
Sources/AblyChat/InternalError.swift (5)
  • toInternalError (40-42)
  • toInternalError (46-48)
  • toInternalError (52-54)
  • toInternalError (58-60)
  • toInternalError (64-66)
Sources/AblyChat/RoomFeature.swift (1)
  • onDiscontinuity (69-71)
Tests/AblyChatTests/Mocks/MockFeatureChannel.swift (1)
  • onDiscontinuity (19-21)
🔇 Additional comments (25)
Sources/AblyChat/InternalError.swift (1)

33-37: Good addition of Equatable conformance.

Adding Equatable support to InternalError is a good improvement that will make testing error handling more straightforward. The implementation correctly compares two errors by using their converted ARTErrorInfo representations.

Tests/AblyChatTests/Mocks/MockRealtime.swift (2)

7-7: Good addition of call recording capability.

Adding a call recorder will make testing method invocations more robust, allowing tests to verify that methods are being called with expected parameters.


39-42: Properly implementing call recording.

The implementation correctly records the request method calls with all relevant parameters. This will be valuable for verifying API interactions in tests.

Tests/AblyChatTests/Mocks/MockHTTPPaginatedResponse.swift (4)

13-13: Simplified initializer by removing redundant parameter.

Good simplification by removing the isLast parameter since it can be derived from hasNext.


43-43: Improved property design for isLast.

The implementation now correctly defines isLast as the logical opposite of hasNext, which eliminates the potential for inconsistent state between these two related properties.


120-122: Updated pagination settings for test data.

Setting hasNext: true for the messages test data correctly configures it to simulate a paginated result with more items available.


128-155: Enhanced mock data with additional fields.

The updated mock data now includes important fields like clientId, action, and version, making it more representative of real-world responses. The pagination is also properly configured with hasNext: false to indicate it's the last page.

Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift (5)

14-14: Good addition of state change handling.

Adding the stateChangeToEmitForListener property enables the mock to simulate channel state changes during tests.


28-31: Enhanced mock initialization options.

The additional parameters provide more flexibility for configuring the mock's behavior during tests, particularly for message emission and state change handling.


134-156: Improved message subscription handling.

The implementation now correctly handles creating and emitting a message from JSON data, which provides more flexibility in testing different message scenarios with varied content and metadata.


167-186: Added thread-safe callback management.

The implementation now properly uses NSLock to ensure thread safety when managing callbacks. This is important to prevent race conditions during tests, especially when callbacks might be invoked from different threads.


230-242: Added helpful utility methods for callback invocation.

These methods provide a clean and thread-safe way to trigger callbacks during tests, which will make it easier to simulate various channel events and state changes.

Tests/AblyChatTests/DefaultMessagesTests.swift (13)

7-11: Good addition of specification markers.
These lines neatly reference the relevant specification sections (CHA-M3, CHA-M3a, etc.), clarifying the purpose of the test suite.


34-36: Error handling test is thorough.
This test properly checks that ARTErrorInfo is raised when the REST Chat API returns an error. The usage of #expect to assert the thrown error matches the intended domain and code is clear and maintainable.

Also applies to: 38-40, 42-42, 49-49, 51-51, 53-55


58-58: Subscription point test for ATTACHED channel is solid.
You correctly verify that when the channel is already attached, the getPreviousMessages call uses the current channelSerial. This approach ensures that the test covers the scenario of an attached channel’s subscription point.

Also applies to: 60-84


86-88: Attach-serial logic confirmation.
Verifying that the attachSerial is used when the channel is not yet attached (and transitions from attaching to attached) confirms correct fallback behavior. The test’s structure is consistent with the prior approach.

Also applies to: 90-93, 95-98, 101-113


115-161: Reattachment handling test is comprehensive.
This test checks the scenario where resumed == false after reattachment, confirming that the subscription resets to attachSerial. All transitions and state changes are clearly tested, ensuring correctness.


162-162: UPDATE event handling for unsubscribed states is solid.
Ensuring the subscription point resets to the attachSerial upon an update event with resumed == false is crucial. The test’s explicit path transitions successfully validate channel state changes.

Also applies to: 164-203


207-243: Standard history query options test is well-demonstrated.
This verifies the acceptance of normal query options and enforces “direction = backwards” by default, which is consistent with the specification. The assertion that .init(orderBy: .oldestFirst) results in a “backwards” param helps confirm correct internal logic.


247-268: Server error throw test in subscription getPreviousMessages.
Your verification of error propagation from MockRealtime to ARTErrorInfo provides clear coverage for negative server responses.


270-298: GET messages test aligns with the specification.
Confirming that a PaginatedResult can be iterated upon satisfies the real-world usage of retrieving messages in multiple pages. This coverage is both necessary and well structured.


302-327: Error throw on GET messages.
You ensure that the thrown ARTErrorInfo is tested for the correct code and message. Good negative-path coverage.


334-373: Incoming messages subscription coverage is comprehensive.
Verifying headers and metadata in the received message ensures the subscription pipeline is correctly preserving message properties. This thoroughness helps protect against regression in the future.


375-416: Malformed message handling test is valuable.
Testing that malformed JSON events are not emitted to subscribers is an essential reliability check. The test logic ensures only valid events make it through.


422-441: Discontinuity forwarding test is straightforward and clear.
By emitting a DiscontinuityEvent and verifying that DefaultMessages relays the event, the test confirms the correct propagation of discontinuities.

@github-actions github-actions bot temporarily deployed to staging/pull/249/AblyChat April 11, 2025 14:27 Inactive
Copy link
Copy Markdown

@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

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between e946995 and f285b4a.

📒 Files selected for processing (1)
  • Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift (5 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (13)
  • GitHub Check: Example app, tvOS (Xcode 16.2)
  • GitHub Check: Generate code coverage (Xcode 16.2)
  • GitHub Check: Example app, iOS (Xcode 16.2)
  • GitHub Check: Xcode, tvOS (Xcode 16.2)
  • GitHub Check: Example app, macOS (Xcode 16.2)
  • GitHub Check: Xcode, release configuration, tvOS (Xcode 16.2)
  • GitHub Check: Xcode, iOS (Xcode 16.2)
  • GitHub Check: Xcode, release configuration, iOS (Xcode 16.2)
  • GitHub Check: Xcode, macOS (Xcode 16.2)
  • GitHub Check: Xcode, release configuration, macOS (Xcode 16.2)
  • GitHub Check: SPM (Xcode 16.2)
  • GitHub Check: SPM, release configuration (Xcode 16.2)
  • GitHub Check: check-documentation
🔇 Additional comments (5)
Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift (5)

14-14: Good addition of stateChangeToEmitForListener property

This provides a clean way to simulate channel state changes in tests, allowing test code to specify a state change that should be immediately emitted when a listener is registered.

Also applies to: 31-31, 43-43


28-28: Good enhancement with messageJSONToEmitOnSubscribe property

This addition complements the existing messageToEmitOnSubscribe property by providing more flexibility for tests to construct messages from JSON data, which is useful for testing various scenarios.

Also applies to: 38-38, 134-134


167-169: Good thread safety implementation with NSLock

Adding explicit locking mechanism using NSLock is a good practice for ensuring thread-safe access to the callback properties, especially since they might be accessed from different threads in test scenarios.


171-186: Well-implemented thread-safe callback registration

The on(_:) methods now properly use locking to ensure thread safety when registering callbacks, and the improved parameter naming enhances readability.


230-242: Good addition of thread-safe callback invocation methods

These new methods provide a clean API for tests to trigger state change events while ensuring thread safety with proper locking.

@maratal
Copy link
Copy Markdown
Collaborator Author

maratal commented Apr 11, 2025

I'll fix commit history before the merge.

@maratal maratal linked an issue Apr 11, 2025 that may be closed by this pull request
@lawrence-forooghian
Copy link
Copy Markdown
Collaborator

@maratal according to the spec coverage report, there are some CHA-M spec points that are not tested. Please can you address?

@maratal maratal force-pushed the fix/235-presence-tests branch 4 times, most recently from 410e4ae to 6cf2821 Compare May 17, 2025 21:06
@maratal maratal force-pushed the fix/235-messages-tests branch from f285b4a to b20087a Compare May 18, 2025 18:14
@github-actions github-actions bot temporarily deployed to staging/pull/249/AblyChat May 18, 2025 18:15 Inactive
Copy link
Copy Markdown

@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 (2)
Tests/AblyChatTests/DefaultMessagesTests.swift (2)

253-255: Same missing async throws annotation as above

This doIt helper suffers from the same compilation issue—apply the explicit signature fix here as well.
(See earlier comment for the diff.)


311-313: Repeat of the async throws closure problem

For consistency and successful compilation, annotate this third doIt closure too.

🧹 Nitpick comments (2)
Tests/AblyChatTests/DefaultMessagesTests.swift (2)

366-368: Mis-labelled spec reference can mislead future maintainers

The inline comment says “Wrong name, should be CHA-M4k” but still tags @spec CHA-M5k.
Consider correcting the tag to avoid confusion when generating coverage reports.

-// Wrong name, should be CHA-M4k
-// @spec CHA-M5k
+// @spec CHA-M4k   // corrected

360-364: Prefer XCTExpectFailure or #expectFailure over commenting-out assertions

Keeping assertions commented out hides potentially valuable tests and risks them being forgotten.
Mark the section as an expected failure until the compiler bug is fixed:

XCTExpectFailure("Awaiting fix for https://github.com/ably/ably-chat-swift/issues/233") {
    let receivedMessage = try #require(await messagesSubscription.first { _ in true })
    #expect(receivedMessage.headers == ["numberKey": .number(10), "stringKey": .string("hello")])
    #expect(receivedMessage.metadata == ["numberKey": .number(10), "stringKey": .string("hello")])
}

This keeps the test active while making CI pass.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between f285b4a and b20087a.

📒 Files selected for processing (5)
  • Sources/AblyChat/InternalError.swift (1 hunks)
  • Tests/AblyChatTests/DefaultMessagesTests.swift (4 hunks)
  • Tests/AblyChatTests/Mocks/MockHTTPPaginatedResponse.swift (4 hunks)
  • Tests/AblyChatTests/Mocks/MockRealtime.swift (2 hunks)
  • Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • Tests/AblyChatTests/Mocks/MockRealtime.swift
  • Sources/AblyChat/InternalError.swift
  • Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift
  • Tests/AblyChatTests/Mocks/MockHTTPPaginatedResponse.swift
⏰ Context from checks skipped due to timeout of 90000ms (34)
  • GitHub Check: Example app, macOS (Xcode 16.3)
  • GitHub Check: Example app, tvOS (Xcode 16.3)
  • GitHub Check: Example app, iOS (Xcode 16.3)
  • GitHub Check: Example app, macOS (Xcode 16.1)
  • GitHub Check: Example app, iOS (Xcode 16.2)
  • GitHub Check: Example app, tvOS (Xcode 16.2)
  • GitHub Check: Example app, macOS (Xcode 16.2)
  • GitHub Check: Xcode, release configuration, iOS (Xcode 16.3)
  • GitHub Check: Xcode, release configuration, tvOS (Xcode 16.1)
  • GitHub Check: Xcode, release configuration, tvOS (Xcode 16.3)
  • GitHub Check: Xcode, release configuration, tvOS (Xcode 16.2)
  • GitHub Check: Xcode, tvOS (Xcode 16.3)
  • GitHub Check: Example app, iOS (Xcode 16.1)
  • GitHub Check: Example app, tvOS (Xcode 16.1)
  • GitHub Check: Xcode, release configuration, iOS (Xcode 16.2)
  • GitHub Check: Xcode, release configuration, iOS (Xcode 16.1)
  • GitHub Check: Xcode, release configuration, macOS (Xcode 16.3)
  • GitHub Check: Xcode, release configuration, macOS (Xcode 16.1)
  • GitHub Check: Xcode, tvOS (Xcode 16.2)
  • GitHub Check: Xcode, tvOS (Xcode 16.1)
  • GitHub Check: Xcode, release configuration, macOS (Xcode 16.2)
  • GitHub Check: Xcode, iOS (Xcode 16.2)
  • GitHub Check: Xcode, macOS (Xcode 16.3)
  • GitHub Check: Xcode, macOS (Xcode 16.2)
  • GitHub Check: Xcode, iOS (Xcode 16.3)
  • GitHub Check: Xcode, macOS (Xcode 16.1)
  • GitHub Check: Xcode, iOS (Xcode 16.1)
  • GitHub Check: SPM, release configuration (Xcode 16.1)
  • GitHub Check: SPM, release configuration (Xcode 16.2)
  • GitHub Check: SPM, release configuration (Xcode 16.3)
  • GitHub Check: SPM (Xcode 16.1)
  • GitHub Check: SPM (Xcode 16.3)
  • GitHub Check: SPM (Xcode 16.2)
  • GitHub Check: Generate code coverage

@maratal maratal force-pushed the fix/235-messages-tests branch from b20087a to 9537490 Compare May 18, 2025 21:11
@github-actions github-actions bot temporarily deployed to staging/pull/249/AblyChat May 18, 2025 21:12 Inactive
Copy link
Copy Markdown

@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: 11

🔭 Outside diff range comments (4)
Sources/AblyChat/DefaultConnection.swift (1)

20-77: ⚠️ Potential issue

Callback may execute off the MainActor despite @MainActor expectation

realtime.connection.on invokes the closure on the queue chosen by the Ably SDK (not necessarily the main thread).
Because the body calls callback(statusChange) directly three times (lines 51, 62 & 76), the @MainActor guarantee of the callback is violated and Swift’s actor-isolation checker will not protect you at runtime. This can surface as race conditions or “MainActor-only class cannot be used on background thread” crashes in clients holding UIKit / SwiftUI state inside their listener.

Suggested fix – hop to the MainActor before invoking the listener:

-                callback(statusChange)
+                Task { @MainActor in
+                    callback(statusChange)
+                }

Apply the same hop at all call-sites (lines 51, 62, 76).

Doing so keeps the public API contract intact and avoids unexpected thread hops for the SDK user.

Sources/AblyChat/DefaultTyping.swift (1)

74-110: ⚠️ Potential issue

Cross-actor access inside Ably listener – dispatch back to the MainActor

Implementation is isolated to the main actor.
The closure you pass to channel.subscribe is executed on Ably’s internal queue, so every access to self, logger, or typingTimerManager, and the @MainActor callback violates actor isolation rules and can trigger Swift Concurrency runtime warnings.

-            let startedEventListener = channel.subscribe(TypingEventType.started.rawValue) { [weak self] message in
-                guard let self, let messageClientID = message.clientId else {
-                    return
-                }
-                logger.log(message: "Received started typing message: \(message)", level: .debug)
-
-                callback(
-                    TypingSetEvent(
-
-                    )
-                )
-
-                callback(
-                    TypingSetEvent(
-
-                    )
-                )
-            }
+            let startedEventListener = channel.subscribe(TypingEventType.started.rawValue) { [weak self] message in
+                Task { @MainActor [weak self] in
+                    guard let self, let messageClientID = message.clientId else { return }
+
+                    logger.log(message: "Received started typing message: \(message)", level: .debug)
+
+                    callback(
+                        TypingSetEvent(
+
+                        )
+                    )
+                }
+            }

Apply the same pattern to the stoppedEventListener below.
This keeps all state changes and callback invocations inside the main actor boundary.

Sources/AblyChat/DefaultOccupancy.swift (1)

40-71: ⚠️ Potential issue

Actor-isolation breach & unmanaged background thread

The closure passed to channel.subscribe executes off-actor.
All accesses to logger, options, and the @MainActor callback break isolation.

-            let eventListener = channel.subscribe(OccupancyEvents.meta.rawValue) { [logger] message in
-                logger.log(message: "Received occupancy message: \(message)", level: .debug)
-
-                callback(occupancyEvent)
-            }
+            let eventListener = channel.subscribe(OccupancyEvents.meta.rawValue) { [weak self] message in
+                Task { @MainActor [weak self] in
+                    guard let self else { return }
+                    logger.log(message: "Received occupancy message: \(message)", level: .debug)
+
+                    callback(occupancyEvent)
+                }
+            }

Also dispatch off(eventListener) back onto the main actor in the returned SubscriptionHandle, mirroring the change suggested for typing.

Sources/AblyChat/DefaultMessages.swift (1)

74-86: 🛠️ Refactor suggestion

Thrown errors inside listener are swallowed – add logging or surfacing

The inner do / catch ignores every parsing/conversion error. Silent failure makes diagnosing malformed realtime events difficult.

                 } catch {
-                    // note: this replaces some existing code that also didn't handle any thrown error; I suspect not intentional, will leave whoever writes the tests for this class to see what's going on
+                    logger.log(
+                        message: "Failed to decode incoming message: \(error)",
+                        level: .error
+                    )
 }
♻️ Duplicate comments (1)
Tests/AblyChatTests/DefaultMessagesTests.swift (1)

44-53: ⚠️ Potential issue

Closure doIt still lacks an explicit async throws signature

We raised this earlier — without the signature the compiler treats the closure as synchronous & non-throwing, so await/try inside it is illegal. Xcode 16.2/16.3 will fail to compile.

-        let doIt = {
-            _ = try await defaultMessages.send(params: .init(text: "hey"))
-        }
+        let doIt: @Sendable () async throws -> Void = {
+            _ = try await defaultMessages.send(params: .init(text: "hey"))
+        }

Replicate the same fix for the similar doIt helpers around lines 253 and 311.

🧰 Tools
🪛 SwiftLint (0.57.0)

[Error] 52-52: Force casts should be avoided

(force_cast)

🧹 Nitpick comments (20)
Sources/AblyChat/Typing.swift (1)

15-21: Clarify thread expectations for implementers

The callback parameter is annotated @MainActor, but the protocol does not state that implementations must dispatch onto the main actor before calling it. Consider adding a short doc string remark such as “Implementers must invoke the listener on the main actor” to avoid accidental misuse.

No code change required, only documentation.

Sources/AblyChat/Occupancy.swift (1)

44-58: Termination handler needlessly spawns a new task

Inside the async-sequence wrapper you already run on MainActor, but unsubscribe() is dispatched through a brand-new Task { @MainActor in … }.
Creating another hop is unnecessary overhead and adds an extra asynchronous boundary that can make deterministic shutdown harder in tests.

- subscription.addTerminationHandler {
-     Task { @MainActor in
-         subscriptionHandle.unsubscribe()
-     }
- }
+ subscription.addTerminationHandler {
+     subscriptionHandle.unsubscribe()   // Already on MainActor
+ }

Unless SubscriptionHandle.unsubscribe() explicitly requires suspension (it currently does not), calling it synchronously is simpler and avoids race conditions when the caller relies on immediate unsubscription.

Sources/AblyChat/DefaultRoomReactions.swift (1)

14-16: Expose the same discardable-result guarantee as the inner implementation

Most callers ignore the returned SubscriptionHandle. The inner Implementation.subscribe is annotated with @discardableResult, but the façade method is not, so unused-result warnings will leak out to SDK users.

-internal func subscribe(_ callback: @escaping @MainActor (Reaction) -> Void) -> SubscriptionHandle {
+@discardableResult
+internal func subscribe(_ callback: @escaping @MainActor (Reaction) -> Void) -> SubscriptionHandle {
     implementation.subscribe(callback)
 }

This keeps the public surface irritation-free and consistent with the rest of the subscription APIs.

Sources/AblyChat/Discontinuity.swift (2)

5-9: Prefer immutable stored property

error is never reassigned after initialisation; declaring it as let communicates intent and enables additional compiler optimisations.

-    public var error: ARTErrorInfo
+    public let error: ARTErrorInfo

37-46: Avoid spawning an extra Task just to unsubscribe

Subscription.addTerminationHandler already executes its closure on the same context that terminates the async sequence.
Wrapping unsubscribe() in Task { @MainActor … } introduces unnecessary indirection and can lead to actor-hop overhead.

-        subscription.addTerminationHandler {
-            Task { @MainActor in
-                subscriptionHandle.unsubscribe()
-            }
-        }
+        subscription.addTerminationHandler {
+            subscriptionHandle.unsubscribe()
+        }
Sources/AblyChat/DefaultTyping.swift (1)

11-14: Surface mismatch between protocol isolation and callback execution

subscribe(_:) exposes a @MainActor callback, but the call-site here merely forwards the closure to implementation.subscribe without guaranteeing main-actor execution when the callback is invoked.
This is harmless here, yet worth documenting so callers know their closure is indeed executed on the main actor.

Sources/AblyChat/DefaultOccupancy.swift (1)

10-13: Document callback execution context

As with typing, callers are promised a @MainActor callback but nothing here enforces that guarantee. State this explicitly in the doc-comment or ensure dispatch in implementation.subscribe.

Example/AblyChatExample/Mocks/MockSubscriptionStorage.swift (2)

47-61: Retain and cancel the repeating task to avoid runaway timers

periodic(with:interval:) presumably returns a Task (or similar handle) that must stay alive for the closure to execute.
Because Subscription drops the returned task, it may get de-allocated immediately, stopping emission.
Conversely, if periodic manages its own lifecycle, you still need a way to cancel it when the subscription ends.

-            periodic(with: interval) {
+            let timer = periodic(with: interval) {
                 if needNext {
                     callback(randomElement())
                 }
                 return needNext
             }
             handle = SubscriptionHandle {
                 needNext = false
+                timer.cancel()      // ensure the loop stops
                 onTerminate()
             }

Store timer as a property (let timer: Task<(), Never>) to keep it alive.


97-118: Duplicate timer-retention fix for MockMessageSubscriptionHandleStorage

Apply the same timer retention & cancellation pattern here to keep both mock storages consistent and leak-free.

Sources/AblyChat/Connection.swift (2)

19-29: Minor doc-comment polish

“subscription handler” should read “subscription handle”, and the callback parameter description could clarify that it runs on the main actor.

- * - Returns: A subscription handler that can be used to unsubscribe …
+ * - Returns: A ``SubscriptionHandle`` that can be used to unsubscribe …

This keeps the public docs precise and in sync with the actual API surface.


46-60: Consider cancelling the handle without spinning a new Task

Inside addTerminationHandler you already run on the current actor; wrapping unsubscribe() in Task { @MainActor in … } adds an extra hop and may race with object deallocation.

If the goal is merely to ensure main-actor execution, you can replace the Task block with @MainActor isolation on the closure:

subscription.addTerminationHandler { @MainActor in
    subscriptionHandle.unsubscribe()
}

Simpler, cheaper, and deterministic.

Sources/AblyChat/Messages.swift (1)

354-361: Consider making unsubscribe asynchronous

unsubscribe may hit Ably-Cocoa and network-bound state-changes; a synchronous closure gives no way to await completion or surface errors.
Returning @MainActor () async -> Void (and, if needed, throws) would future-proof the API and match the rest of the async surface.

Sources/AblyChat/DefaultMessages.swift (1)

5-7: Name no longer reflects stored type

MessageSubscriptionWrapper.subscription now stores a MessageSubscriptionHandle, not a MessageSubscription.
Consider renaming to avoid confusion, e.g. handle or subscriptionHandle.

Sources/AblyChat/Presence.swift (1)

79-85: Missing @MainActor on callback parameter

Presence is annotated @MainActor, but the callback closure in subscribe(event:_:) is not.
Marking it @MainActor (or explicitly documenting that it’s invoked on the main actor) prevents accidental cross-actor UI updates.

-@discardableResult
-func subscribe(event: PresenceEventType, _ callback: @escaping ((PresenceEvent) -> Void)) -> SubscriptionHandle
+@discardableResult
+func subscribe(event: PresenceEventType, _ callback: @escaping @MainActor (PresenceEvent) -> Void) -> SubscriptionHandle
Example/AblyChatExample/Mocks/MockClients.swift (3)

69-74: randomStatusChange can crash if the array is ever emptied

You’re force‐unwrapping the result of randomElement():

RoomStatusChange(current: [...].randomElement()!, previous: .attaching(error: nil))

If someone later “tidies up” the literals and the array becomes empty, the mock will crash at runtime.
A tiny guard keeps the mock robust:

-        RoomStatusChange(current: [.attached(error: nil), ... , .suspended(error: .createUnknownError())].randomElement()!, previous: .attaching(error: nil))
+        let next = [
+            RoomStatus.attached(error: nil), RoomStatus.attached(error: nil),
+            /* … */
+            RoomStatus.suspended(error: .createUnknownError())
+        ].randomElement() ?? .attached(error: nil)
+        RoomStatusChange(current: next, previous: .attaching(error: nil))

83-98: Potential retain-cycle & cancellation race in onStatusChange

needNext is captured strongly by the timer and mutated from the cancellation closure.
If periodic calls its block after SubscriptionHandle’s cancel has set needNext = false, you’re safe, but if it fires while needNext is still true and the handle is being torn down on another thread, you might end up invoking callback one last time after cancellation.

Two low-cost safeguards:

-        var needNext = true
+        let lock = NSLock()
+        var needNext = true-            if needNext {
+            lock.lock(); defer { lock.unlock() }
+            if needNext {
                 callback(randomStatusChange())
             }
             return needNext
…
-            needNext = false
+            lock.lock(); needNext = false; lock.unlock()

(or mark periodic’s block @MainActor to serialize updates).


117-140: Return value relies on implicit return – consider being explicit

Although Swift allows single-expression implicit returns, these async/throws mocks are quite dense.
An explicit return improves readability and silences “implicit return” SwiftLint rules many teams enable.

-        mockSubscriptions.create(
+        return mockSubscriptions.create(
Tests/AblyChatTests/DefaultMessagesTests.swift (3)

51-53: Avoid force-casting to ARTErrorInfo in tests

as! ARTErrorInfo will crash the test suite if the error type ever changes.
Safer pattern:

#expect {
    guard let art = error as? ARTErrorInfo else { return false }
    return art == expected
}

Or use pattern-matching:

} throws: { case let art as ARTErrorInfo in
    art == expected
}

Also applies to: 259-261, 318-318

🧰 Tools
🪛 SwiftLint (0.57.0)

[Error] 52-52: Force casts should be avoided

(force_cast)


262-262: Strip trailing blank line before closing brace

SwiftLint’s vertical_whitespace_closing_braces rule flags this empty line.

🧰 Tools
🪛 SwiftLint (0.57.0)

[Error] 262-262: Don't include vertical whitespace (empty line) before closing braces

(vertical_whitespace_closing_braces)


367-369: Incorrect spec tag comment

The inline note says

// Wrong name, should be CHA-M4k
// @spec CHA-M5k

but the test covers malformed realtime events (CHA-M4k).
Update the @spec tag to avoid confusion in coverage tooling.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between b20087a and 9537490.

📒 Files selected for processing (30)
  • Example/AblyChatExample/ContentView.swift (2 hunks)
  • Example/AblyChatExample/Misc/Utils.swift (1 hunks)
  • Example/AblyChatExample/Mocks/MockClients.swift (9 hunks)
  • Example/AblyChatExample/Mocks/MockSubscriptionStorage.swift (2 hunks)
  • Sources/AblyChat/Connection.swift (1 hunks)
  • Sources/AblyChat/DefaultConnection.swift (4 hunks)
  • Sources/AblyChat/DefaultMessages.swift (4 hunks)
  • Sources/AblyChat/DefaultOccupancy.swift (3 hunks)
  • Sources/AblyChat/DefaultPresence.swift (2 hunks)
  • Sources/AblyChat/DefaultRoomReactions.swift (3 hunks)
  • Sources/AblyChat/DefaultTyping.swift (6 hunks)
  • Sources/AblyChat/Discontinuity.swift (1 hunks)
  • Sources/AblyChat/DiscontinuityEvent.swift (0 hunks)
  • Sources/AblyChat/InternalError.swift (1 hunks)
  • Sources/AblyChat/Messages.swift (6 hunks)
  • Sources/AblyChat/Occupancy.swift (2 hunks)
  • Sources/AblyChat/Presence.swift (3 hunks)
  • Sources/AblyChat/Room.swift (5 hunks)
  • Sources/AblyChat/RoomLifecycleManager.swift (5 hunks)
  • Sources/AblyChat/RoomReactions.swift (2 hunks)
  • Sources/AblyChat/Subscription.swift (1 hunks)
  • Sources/AblyChat/SubscriptionHandleStorage.swift (1 hunks)
  • Sources/AblyChat/Typing.swift (2 hunks)
  • Tests/AblyChatTests/DefaultMessagesTests.swift (3 hunks)
  • Tests/AblyChatTests/Helpers/Subscription+RoomStatusChange.swift (2 hunks)
  • Tests/AblyChatTests/Mocks/MockHTTPPaginatedResponse.swift (4 hunks)
  • Tests/AblyChatTests/Mocks/MockRealtime.swift (2 hunks)
  • Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift (4 hunks)
  • Tests/AblyChatTests/Mocks/MockRoom.swift (1 hunks)
  • Tests/AblyChatTests/Mocks/MockRoomLifecycleManager.swift (2 hunks)
💤 Files with no reviewable changes (1)
  • Sources/AblyChat/DiscontinuityEvent.swift
✅ Files skipped from review due to trivial changes (1)
  • Example/AblyChatExample/Misc/Utils.swift
🚧 Files skipped from review as they are similar to previous changes (4)
  • Sources/AblyChat/InternalError.swift
  • Tests/AblyChatTests/Mocks/MockRealtime.swift
  • Tests/AblyChatTests/Mocks/MockHTTPPaginatedResponse.swift
  • Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift
🧰 Additional context used
🧠 Learnings (7)
Sources/AblyChat/Subscription.swift (4)
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Sources/AblyChat/SubscriptionHandleStorage.swift:37-40
Timestamp: 2025-05-12T21:02:28.274Z
Learning: In the AblyChat Swift codebase, the `SubscriptionHandle` struct has an `unsubscribe()` method for terminating subscriptions, not a `cancel()` method.
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Example/AblyChatExample/ContentView.swift:203-209
Timestamp: 2025-05-12T21:11:08.937Z
Learning: In the AblyChat Swift codebase, SubscriptionHandle doesn't need to be retained/stored to keep subscriptions active. The subscription continues to work even if the handle is not stored, as the underlying services maintain references to the callbacks until explicitly unsubscribed via the handle's unsubscribe() method.
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Example/AblyChatExample/ContentView.swift:203-209
Timestamp: 2025-05-12T21:11:08.937Z
Learning: In the AblyChat Swift codebase, SubscriptionHandle doesn't need to be retained/stored to keep subscriptions active. The subscription callbacks continue to work even when the handle is not stored, as the underlying services maintain their own storage of callbacks that remain registered until explicitly unsubscribed via the handle's unsubscribe() method.
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Sources/AblyChat/DefaultTyping.swift:131-138
Timestamp: 2025-05-12T21:04:36.263Z
Learning: In the AblyChat Swift codebase, SubscriptionHandle closures should capture self weakly with `[weak self]` to avoid potential retain cycles, particularly when calling channel.unsubscribe() within the closure.
Sources/AblyChat/DefaultConnection.swift (3)
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Example/AblyChatExample/ContentView.swift:203-209
Timestamp: 2025-05-12T21:11:08.937Z
Learning: In the AblyChat Swift codebase, SubscriptionHandle doesn't need to be retained/stored to keep subscriptions active. The subscription continues to work even if the handle is not stored, as the underlying services maintain references to the callbacks until explicitly unsubscribed via the handle's unsubscribe() method.
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Example/AblyChatExample/ContentView.swift:203-209
Timestamp: 2025-05-12T21:11:08.937Z
Learning: In the AblyChat Swift codebase, SubscriptionHandle doesn't need to be retained/stored to keep subscriptions active. The subscription callbacks continue to work even when the handle is not stored, as the underlying services maintain their own storage of callbacks that remain registered until explicitly unsubscribed via the handle's unsubscribe() method.
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Sources/AblyChat/DefaultTyping.swift:131-138
Timestamp: 2025-05-12T21:04:36.263Z
Learning: In the AblyChat Swift codebase, SubscriptionHandle closures should capture self weakly with `[weak self]` to avoid potential retain cycles, particularly when calling channel.unsubscribe() within the closure.
Sources/AblyChat/SubscriptionHandleStorage.swift (3)
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Example/AblyChatExample/ContentView.swift:203-209
Timestamp: 2025-05-12T21:11:08.937Z
Learning: In the AblyChat Swift codebase, SubscriptionHandle doesn't need to be retained/stored to keep subscriptions active. The subscription callbacks continue to work even when the handle is not stored, as the underlying services maintain their own storage of callbacks that remain registered until explicitly unsubscribed via the handle's unsubscribe() method.
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Example/AblyChatExample/ContentView.swift:203-209
Timestamp: 2025-05-12T21:11:08.937Z
Learning: In the AblyChat Swift codebase, SubscriptionHandle doesn't need to be retained/stored to keep subscriptions active. The subscription continues to work even if the handle is not stored, as the underlying services maintain references to the callbacks until explicitly unsubscribed via the handle's unsubscribe() method.
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Sources/AblyChat/DefaultTyping.swift:131-138
Timestamp: 2025-05-12T21:04:36.263Z
Learning: In the AblyChat Swift codebase, SubscriptionHandle closures should capture self weakly with `[weak self]` to avoid potential retain cycles, particularly when calling channel.unsubscribe() within the closure.
Sources/AblyChat/Occupancy.swift (1)
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Sources/AblyChat/DefaultOccupancy.swift:10-13
Timestamp: 2025-05-12T21:01:14.109Z
Learning: The `Occupancy` protocol in the Ably Chat Swift SDK is annotated with `@MainActor`, making all conforming types like `DefaultOccupancy` automatically main actor-isolated without requiring explicit annotations on their methods.
Sources/AblyChat/DefaultTyping.swift (2)
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Sources/AblyChat/DefaultTyping.swift:11-14
Timestamp: 2025-05-12T21:03:31.914Z
Learning: The `Typing` protocol in the Ably Chat Swift SDK is annotated with `@MainActor`, making all conforming types like `DefaultTyping` automatically main actor-isolated without requiring explicit annotations on their methods.
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Sources/AblyChat/DefaultTyping.swift:11-14
Timestamp: 2025-05-12T21:03:31.914Z
Learning: The `Typing` protocol in the Ably Chat Swift SDK is annotated with `@MainActor`, making all conforming types like `DefaultTyping` automatically main actor-isolated without requiring explicit annotations on their methods.
Sources/AblyChat/DefaultOccupancy.swift (1)
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Sources/AblyChat/DefaultOccupancy.swift:10-13
Timestamp: 2025-05-12T21:01:14.109Z
Learning: The `Occupancy` protocol in the Ably Chat Swift SDK is annotated with `@MainActor`, making all conforming types like `DefaultOccupancy` automatically main actor-isolated without requiring explicit annotations on their methods.
Example/AblyChatExample/Mocks/MockClients.swift (1)
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Example/AblyChatExample/Mocks/MockClients.swift:0-0
Timestamp: 2025-05-16T21:04:26.221Z
Learning: In the ably-chat-swift project, mock implementations (like those in MockClients.swift) are intentionally kept simple, sometimes omitting parameter-based filtering behavior for testing simplicity.
🧬 Code Graph Analysis (8)
Sources/AblyChat/Subscription.swift (4)
Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift (1)
  • unsubscribe (178-180)
Tests/AblyChatTests/Mocks/MockRealtimePresence.swift (1)
  • unsubscribe (15-17)
Sources/AblyChat/AblyCocoaExtensions/InternalAblyCocoaTypes.swift (2)
  • unsubscribe (256-258)
  • unsubscribe (368-370)
Example/AblyChatExample/Mocks/MockRealtime.swift (6)
  • unsubscribe (154-156)
  • unsubscribe (158-160)
  • unsubscribe (162-164)
  • unsubscribe (328-330)
  • unsubscribe (332-334)
  • unsubscribe (336-338)
Tests/AblyChatTests/Mocks/MockRoom.swift (6)
Example/AblyChatExample/Mocks/MockClients.swift (3)
  • onStatusChange (83-98)
  • onStatusChange (456-469)
  • onDiscontinuity (100-103)
Sources/AblyChat/Connection.swift (2)
  • onStatusChange (46-60)
  • onStatusChange (65-67)
Sources/AblyChat/Room.swift (4)
  • onStatusChange (122-136)
  • onStatusChange (141-143)
  • onStatusChange (330-333)
  • onDiscontinuity (341-344)
Tests/AblyChatTests/DefaultRoomTests.swift (2)
  • onStatusChange (257-279)
  • onDiscontinuity (284-306)
Sources/AblyChat/Discontinuity.swift (2)
  • onDiscontinuity (36-49)
  • onDiscontinuity (54-56)
Tests/AblyChatTests/Mocks/MockRoomLifecycleManager.swift (1)
  • onDiscontinuity (82-85)
Sources/AblyChat/SubscriptionHandleStorage.swift (2)
Example/AblyChatExample/Mocks/MockSubscriptionStorage.swift (9)
  • create (15-27)
  • create (66-77)
  • create (122-139)
  • subscriptionDidTerminate (29-31)
  • subscriptionDidTerminate (79-81)
  • subscriptionDidTerminate (141-143)
  • emit (33-37)
  • emit (83-87)
  • emit (145-149)
Sources/AblyChat/Subscription.swift (1)
  • emit (78-85)
Sources/AblyChat/DefaultPresence.swift (6)
Sources/AblyChat/DefaultOccupancy.swift (2)
  • subscribe (10-13)
  • subscribe (39-72)
Sources/AblyChat/DefaultTyping.swift (2)
  • subscribe (11-14)
  • subscribe (74-145)
Sources/AblyChat/Presence.swift (5)
  • subscribe (137-151)
  • subscribe (164-178)
  • subscribe (181-183)
  • subscribe (186-188)
  • toARTPresenceAction (277-288)
Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift (1)
  • unsubscribe (178-180)
Tests/AblyChatTests/Mocks/MockRealtimePresence.swift (1)
  • unsubscribe (15-17)
Sources/AblyChat/AblyCocoaExtensions/InternalAblyCocoaTypes.swift (2)
  • unsubscribe (256-258)
  • unsubscribe (368-370)
Sources/AblyChat/Discontinuity.swift (5)
Example/AblyChatExample/Mocks/MockClients.swift (1)
  • onDiscontinuity (100-103)
Sources/AblyChat/Room.swift (1)
  • onDiscontinuity (341-344)
Sources/AblyChat/RoomLifecycleManager.swift (1)
  • onDiscontinuity (221-224)
Sources/AblyChat/SubscriptionHandleStorage.swift (1)
  • emit (37-41)
Sources/AblyChat/Subscription.swift (2)
  • emit (78-85)
  • addTerminationHandler (103-118)
Sources/AblyChat/Connection.swift (5)
Sources/AblyChat/DefaultConnection.swift (1)
  • onStatusChange (20-89)
Sources/AblyChat/Room.swift (3)
  • onStatusChange (122-136)
  • onStatusChange (141-143)
  • onStatusChange (330-333)
Sources/AblyChat/SubscriptionHandleStorage.swift (1)
  • emit (37-41)
Sources/AblyChat/Subscription.swift (2)
  • emit (78-85)
  • addTerminationHandler (103-118)
Tests/AblyChatTests/SubscriptionTests.swift (1)
  • emit (13-23)
Example/AblyChatExample/ContentView.swift (5)
Example/AblyChatExample/Mocks/MockClients.swift (12)
  • subscribe (117-140)
  • subscribe (224-240)
  • subscribe (254-270)
  • subscribe (408-410)
  • subscribe (412-414)
  • subscribe (428-438)
  • update (164-180)
  • update (370-372)
  • update (374-376)
  • update (378-387)
  • onStatusChange (83-98)
  • onStatusChange (456-469)
Sources/AblyChat/Room.swift (3)
  • onStatusChange (122-136)
  • onStatusChange (141-143)
  • onStatusChange (330-333)
Example/AblyChatExample/Mocks/MockSubscriptionStorage.swift (3)
  • create (15-27)
  • create (66-77)
  • create (122-139)
Sources/AblyChat/DefaultConnection.swift (1)
  • onStatusChange (20-89)
Example/AblyChatExample/Misc/Utils.swift (1)
  • after (4-9)
Tests/AblyChatTests/DefaultMessagesTests.swift (5)
Example/AblyChatExample/Mocks/MockClients.swift (18)
  • send (146-162)
  • send (212-222)
  • subscribe (117-140)
  • subscribe (224-240)
  • subscribe (254-270)
  • subscribe (408-410)
  • subscribe (412-414)
  • subscribe (428-438)
  • update (164-180)
  • update (370-372)
  • update (374-376)
  • update (378-387)
  • get (26-33)
  • get (142-144)
  • get (272-274)
  • get (323-333)
  • get (335-345)
  • get (440-442)
Sources/AblyChat/DefaultMessages.swift (8)
  • send (26-28)
  • send (170-176)
  • subscribe (18-20)
  • subscribe (66-159)
  • update (30-32)
  • update (178-184)
  • get (22-24)
  • get (162-168)
Tests/AblyChatTests/Helpers/Helpers.swift (1)
  • hasRecord (152-164)
Tests/AblyChatTests/Mocks/MockRealtimeChannel.swift (3)
  • subscribe (142-170)
  • emitEvent (197-201)
  • get (78-85)
Example/AblyChatExample/Mocks/MockRealtime.swift (2)
  • update (272-274)
  • update (276-278)
🪛 SwiftLint (0.57.0)
Tests/AblyChatTests/DefaultMessagesTests.swift

[Error] 52-52: Force casts should be avoided

(force_cast)


[Error] 260-260: Force casts should be avoided

(force_cast)


[Error] 318-318: Force casts should be avoided

(force_cast)


[Error] 262-262: Don't include vertical whitespace (empty line) before closing braces

(vertical_whitespace_closing_braces)

⏰ Context from checks skipped due to timeout of 90000ms (34)
  • GitHub Check: Xcode, release configuration, tvOS (Xcode 16.2)
  • GitHub Check: Example app, macOS (Xcode 16.2)
  • GitHub Check: Xcode, release configuration, macOS (Xcode 16.3)
  • GitHub Check: Xcode, release configuration, tvOS (Xcode 16.3)
  • GitHub Check: Xcode, release configuration, iOS (Xcode 16.3)
  • GitHub Check: Xcode, release configuration, iOS (Xcode 16.2)
  • GitHub Check: Example app, iOS (Xcode 16.3)
  • GitHub Check: Xcode, iOS (Xcode 16.3)
  • GitHub Check: Xcode, iOS (Xcode 16.2)
  • GitHub Check: Xcode, macOS (Xcode 16.3)
  • GitHub Check: Xcode, release configuration, macOS (Xcode 16.1)
  • GitHub Check: Xcode, release configuration, tvOS (Xcode 16.1)
  • GitHub Check: Xcode, macOS (Xcode 16.2)
  • GitHub Check: Xcode, release configuration, iOS (Xcode 16.1)
  • GitHub Check: Xcode, macOS (Xcode 16.1)
  • GitHub Check: Xcode, tvOS (Xcode 16.3)
  • GitHub Check: Xcode, release configuration, macOS (Xcode 16.2)
  • GitHub Check: Xcode, iOS (Xcode 16.1)
  • GitHub Check: Xcode, tvOS (Xcode 16.2)
  • GitHub Check: Example app, iOS (Xcode 16.1)
  • GitHub Check: Example app, tvOS (Xcode 16.2)
  • GitHub Check: Xcode, tvOS (Xcode 16.1)
  • GitHub Check: SPM (Xcode 16.3)
  • GitHub Check: Example app, tvOS (Xcode 16.1)
  • GitHub Check: Example app, macOS (Xcode 16.3)
  • GitHub Check: Example app, tvOS (Xcode 16.3)
  • GitHub Check: Example app, iOS (Xcode 16.2)
  • GitHub Check: Example app, macOS (Xcode 16.1)
  • GitHub Check: SPM, release configuration (Xcode 16.1)
  • GitHub Check: SPM (Xcode 16.1)
  • GitHub Check: SPM, release configuration (Xcode 16.2)
  • GitHub Check: SPM (Xcode 16.2)
  • GitHub Check: SPM, release configuration (Xcode 16.3)
  • GitHub Check: Generate code coverage
🔇 Additional comments (23)
Sources/AblyChat/Subscription.swift (1)

3-7: Well-structured SubscriptionHandle implementation!

The SubscriptionHandle struct provides a clean, type-safe way to manage unsubscription from events. The @MainActor annotation on the unsubscribe closure ensures thread safety for UI operations.

Tests/AblyChatTests/Mocks/MockRoom.swift (2)

71-74: Mock implementation for onStatusChange follows protocol pattern

The stub implementation aligns with the callback-based subscription pattern used throughout the codebase. The @MainActor annotation ensures consistency with the real implementation.


76-79: Mock implementation for onDiscontinuity follows protocol pattern

This stub implementation aligns with the callback-based subscription pattern used throughout the codebase. The @discardableResult annotation is appropriate for methods where the return value might not always be needed.

Tests/AblyChatTests/Helpers/Subscription+RoomStatusChange.swift (2)

2-2: Appropriate use of @testable import

Using @testable import AblyChat is necessary to access internal entities in tests.


53-74: Well-implemented AsyncSequence wrapper for subscription events

This extension provides a bridge between the new callback-based subscription pattern and the AsyncSequence-based testing approach. It correctly manages subscription lifecycle by adding a termination handler that unsubscribes from the original callback-based subscription.

The implementation follows the pattern seen in other parts of the codebase where similar extensions exist, maintaining consistency.

Sources/AblyChat/SubscriptionHandleStorage.swift (5)

3-14: Well-designed subscription storage implementation

The SubscriptionHandleStorage class provides a clean, type-safe way to manage callback-based subscriptions. The class structure with a private SubscriptionElement struct is well-organized.


16-24: Correctly implemented subscription creation with weak self capture

The create method properly generates a unique ID for each subscription and correctly captures self weakly in the unsubscribe closure to prevent potential retain cycles, which aligns with the best practices for this codebase.


26-30: Useful debug property for testing

The testsOnly_subscriptionCount property provides a convenient way to verify subscription management in tests without exposing internal implementation details in production code.


32-34: Clean subscription termination

The subscriptionDidTerminate method correctly removes the subscription from the storage dictionary using the unique ID.


36-41: Efficient event emission to all subscriptions

The emit method properly iterates through all active subscriptions and invokes their callbacks with the provided element. This implementation is straightforward and efficient.

Sources/AblyChat/DefaultConnection.swift (2)

59-67: Potential retain-cycle between timer and self

Inside timerManager.setTimer the closure captures self strongly (self.error = …).
If the connection never reconnects and the user forgets to call unsubscribe(), the timer retains the closure → the closure retains selfDefaultConnection stays alive and the timer never invalidates.

Capture self weakly to eliminate the cycle:

-                timerManager.setTimer(interval: 5.0) { [timerManager] in
+                timerManager.setTimer(interval: 5.0) { [weak self, timerManager] in
                     timerManager.cancelTimer()
-                    callback(statusChange)
+                    guard let self else { return }
+                    Task { @MainActor in
+                        callback(statusChange)
+                    }

[ suggest_essential_refactor ]


82-88: Clean, minimal unsubscribe logic

The returned SubscriptionHandle cancels the transient-disconnect timer and unregisters the Ably listener, both under a weak self capture. This prevents accidental retention of DefaultConnection and neatly encapsulates teardown logic – nice job!

Sources/AblyChat/Typing.swift (1)

51-75: Async-sequence wrapper looks solid

The adapter correctly:

  1. Forwards events into a Subscription buffer.
  2. Unsubscribes when the sequence completes via a termination handler executed on the main actor.

Good balance between new callback API and backward-compatibility.

Sources/AblyChat/RoomReactions.swift (2)

20-30: Same MainActor dispatch caveat as Typing / Connection

Ensure that concrete implementations of RoomReactions.subscribe(_:) call the callback on the main actor; otherwise the @MainActor contract is broken for SDK users.

Mirror the fix proposed for DefaultConnection (wrap invocations in Task { @MainActor in … }).


32-56: Wrapper implementation mirrors Typing – looks correct

The async-sequence adapter follows the same pattern as other modules, ensuring event forwarding and clean unsubscription. Consistency across protocols is appreciated.

Sources/AblyChat/DefaultTyping.swift (1)

137-144: 🛠️ Refactor suggestion

Ensure unsubscription happens on MainActor

unsubscribe / off mutate Ably channel state that is normally expected to run on the main actor.
Dispatch back similarly to avoid cross-actor violations:

-            return SubscriptionHandle {
-                if let startedEventListener {
-                    self.channel.unsubscribe(startedEventListener)
-                }
-                if let stoppedEventListener {
-                    self.channel.unsubscribe(stoppedEventListener)
-                }
-            }
+            return SubscriptionHandle { [weak self] in
+                Task { @MainActor [weak self] in
+                    guard let self else { return }
+                    if let startedEventListener { self.channel.unsubscribe(startedEventListener) }
+                    if let stoppedEventListener { self.channel.unsubscribe(stoppedEventListener) }
+                }
+            }
⛔ Skipped due to learnings
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Sources/AblyChat/DefaultOccupancy.swift:66-69
Timestamp: 2025-05-12T21:02:25.928Z
Learning: SubscriptionHandle.unsubscribe is already marked with @MainActor annotation, so there's no need to wrap code in MainActor.run when calling it.
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Sources/AblyChat/DefaultOccupancy.swift:66-69
Timestamp: 2025-05-12T21:02:25.928Z
Learning: SubscriptionHandle.unsubscribe is already annotated with @MainActor, so there's no need to wrap the code in MainActor.run when using it.
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Sources/AblyChat/DefaultTyping.swift:131-138
Timestamp: 2025-05-12T21:04:36.263Z
Learning: In the AblyChat Swift codebase, SubscriptionHandle closures should capture self weakly with `[weak self]` to avoid potential retain cycles, particularly when calling channel.unsubscribe() within the closure.
Example/AblyChatExample/ContentView.swift (4)

203-209: Ordering of side-effects before an attach() is fine – just confirming intent

Subscribing before the room is attached is safe because the SDK will buffer events until attach() completes.
No change requested – just making sure this was an intentional choice.


322-330: Good practice: time-boxed connection status logging

Capturing the handle in the after closure ensures it stays alive only for the required duration and unsubscribes deterministically. Nice touch.


239-265: ⚠️ Potential issue

Message stream stops after showMessages returns – store the SubscriptionHandle

subscriptionHandle is a local variable; when showMessages returns it goes out of scope, is de-initialised and automatically unsubscribes.
As a result you will only receive messages that arrive during the execution of showMessages; live updates afterwards will be silently dropped.

@@
-    let subscriptionHandle = try await room.messages.subscribe { message in
+    // Keep the handle in a view-level state property so it lives
+    // for as long as the view needs realtime updates.
+    messagesSubscriptionHandle = try await room.messages.subscribe { message in
@@
 }
-    let previousMessages = try await subscriptionHandle.getPreviousMessages(.init())
+    let previousMessages = try await messagesSubscriptionHandle!.getPreviousMessages(.init())

and add a property at the top of the view:

 @State private var chatClient = Environment.current.createChatClient()
+@State private var messagesSubscriptionHandle: SubscriptionHandle?

Repeat for the other subscription helpers (reactions, presence, typing, occupancy) or collect them in an array:

@State private var subscriptionHandles: [SubscriptionHandle] = []

This prevents accidental unsubscription and gives you a place to clean up in deinit if the example grows.

⛔ Skipped due to learnings
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Example/AblyChatExample/ContentView.swift:203-209
Timestamp: 2025-05-12T21:11:08.937Z
Learning: In the AblyChat Swift codebase, SubscriptionHandle doesn't need to be retained/stored to keep subscriptions active. The subscription continues to work even if the handle is not stored, as the underlying services maintain references to the callbacks until explicitly unsubscribed via the handle's unsubscribe() method.
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Example/AblyChatExample/ContentView.swift:203-209
Timestamp: 2025-05-12T21:11:08.937Z
Learning: In the AblyChat Swift codebase, SubscriptionHandle doesn't need to be retained/stored to keep subscriptions active. The subscription callbacks continue to work even when the handle is not stored, as the underlying services maintain their own storage of callbacks that remain registered until explicitly unsubscribed via the handle's unsubscribe() method.
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Sources/AblyChat/DefaultTyping.swift:131-138
Timestamp: 2025-05-12T21:04:36.263Z
Learning: In the AblyChat Swift codebase, SubscriptionHandle closures should capture self weakly with `[weak self]` to avoid potential retain cycles, particularly when calling channel.unsubscribe() within the closure.

276-297: 🛠️ Refactor suggestion

Same “lost handle” issue for reactions / presence subscriptions

subscribeToReactions, subscribeToPresence, subscribeToTypingEvents, and subscribeToOccupancy all discard the returned SubscriptionHandle, so the callbacks are removed immediately after the method exits.

A concise pattern:

 func subscribeToReactions(room: Room) {
-    room.reactions.subscribe { reaction in
+    subscriptionHandles.append(
+        room.reactions.subscribe { reaction in
             withAnimation {
                 showReaction(reaction.displayedText)
             }
-    }
+        }
+    )
 }

Keeping the handles solves the same real-time drop-out described above.

⛔ Skipped due to learnings
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Example/AblyChatExample/ContentView.swift:203-209
Timestamp: 2025-05-12T21:11:08.937Z
Learning: In the AblyChat Swift codebase, SubscriptionHandle doesn't need to be retained/stored to keep subscriptions active. The subscription callbacks continue to work even when the handle is not stored, as the underlying services maintain their own storage of callbacks that remain registered until explicitly unsubscribed via the handle's unsubscribe() method.
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Example/AblyChatExample/ContentView.swift:203-209
Timestamp: 2025-05-12T21:11:08.937Z
Learning: In the AblyChat Swift codebase, SubscriptionHandle doesn't need to be retained/stored to keep subscriptions active. The subscription continues to work even if the handle is not stored, as the underlying services maintain references to the callbacks until explicitly unsubscribed via the handle's unsubscribe() method.
Learnt from: maratal
PR: ably/ably-chat-swift#286
File: Sources/AblyChat/DefaultTyping.swift:131-138
Timestamp: 2025-05-12T21:04:36.263Z
Learning: In the AblyChat Swift codebase, SubscriptionHandle closures should capture self weakly with `[weak self]` to avoid potential retain cycles, particularly when calling channel.unsubscribe() within the closure.
Sources/AblyChat/RoomLifecycleManager.swift (1)

221-224: Callback-based discontinuity handler looks good

The new onDiscontinuity mirrors the status-change API and cleanly delegates to SubscriptionHandleStorage.
Implementation is correct and thread-safe under the actor; no issues spotted.

Sources/AblyChat/Room.swift (2)

112-144: Nice ergonomic wrapper for async-sequence consumers

Providing an async-sequence façade (Subscription<RoomStatusChange>) on top of the new callback API preserves source compatibility and keeps buffering behaviour explicit. The termination handler correctly unsubscribes.

Minor nit: the Task { @MainActor in … } could be replaced with Task { subscriptionHandle.unsubscribe() } because the enclosing extension is already @MainActor, but current code is perfectly safe.


330-333: Delegation to lifecycle manager is straightforward

DefaultRoom simply forwards status-change subscriptions to RoomLifecycleManager, ensuring single source of truth for status updates. Implementation looks correct.

@maratal maratal force-pushed the fix/235-messages-tests branch from 9537490 to 2bbc803 Compare May 18, 2025 21:18
@github-actions github-actions bot temporarily deployed to staging/pull/249/AblyChat June 22, 2025 22:45 Inactive
@maratal maratal changed the base branch from 259-switch-to-callbacks to main June 22, 2025 22:46
@maratal maratal changed the base branch from main to 259-switch-to-callbacks June 22, 2025 22:47
@maratal maratal force-pushed the fix/235-messages-tests branch from 5fa7ce5 to 1164f2f Compare June 22, 2025 22:51
@github-actions github-actions bot temporarily deployed to staging/pull/249/AblyChat June 22, 2025 22:52 Inactive
@maratal maratal force-pushed the fix/235-messages-tests branch from 1164f2f to d85e713 Compare June 22, 2025 22:57
@github-actions github-actions bot temporarily deployed to staging/pull/249/AblyChat June 22, 2025 22:58 Inactive
@maratal maratal force-pushed the 259-switch-to-callbacks branch from c997f5a to 3e07df6 Compare June 22, 2025 23:03
@maratal maratal force-pushed the fix/235-messages-tests branch from d85e713 to 912abb3 Compare June 22, 2025 23:05
@github-actions github-actions bot temporarily deployed to staging/pull/249/AblyChat June 22, 2025 23:06 Inactive
@maratal maratal force-pushed the 259-switch-to-callbacks branch 4 times, most recently from f3d52dc to e694b85 Compare June 28, 2025 17:21
@maratal maratal force-pushed the fix/235-messages-tests branch from 912abb3 to 02b8d33 Compare June 28, 2025 22:54
@github-actions github-actions bot temporarily deployed to staging/pull/249/AblyChat June 28, 2025 22:55 Inactive
Base automatically changed from 259-switch-to-callbacks to main June 30, 2025 13:12
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.

Restore messages, presence, and room reactions tests

2 participants