Repository: ably/ably-chat-js
Pull request #498: refactor: common type for off-able subscriptions
Author: login: AndyTWF
Created: 2025-03-11T16:39:35Z | Merged: 2025-03-12T11:47:17Z

### Description

Following on from https://github.com/ably/ably-chat-js/pull/487, adds a common interface for subscription removal that is "off" based, e.g "onStatusChange". This allows us to remove redundant, copied types.

### Checklist

* [x] QA'd by the author.
* [x] Unit tests created (if applicable).
* [x] Integration tests created (if applicable).
* [x] Follow coding style guidelines found [here](https://github.com/ably/engineering/tree/main/best-practices).
* [x] TypeDoc updated (if applicable).
* [x] Updated the [website documentation](https://github.com/ably/docs) (if applicable).
* [x] Browser tests created (if applicable).
* [x] In repo demo app updated (if applicable).

### Testing Instructions (Optional)

N/A



Diff:
diff --git a/src/core/connection.ts b/src/core/connection.ts
index 7b1bebb0d..1866d68ae 100644
--- a/src/core/connection.ts
+++ b/src/core/connection.ts
@@ -1,6 +1,7 @@
 import * as Ably from 'ably';
 
 import { Logger } from './logger.js';
+import { StatusSubscription } from './subscription.js';
 import EventEmitter from './utils/event-emitter.js';
 
 /**
@@ -75,16 +76,6 @@ export interface ConnectionStatusChange {
  */
 export type ConnectionStatusListener = (change: ConnectionStatusChange) => void;
 
-/**
- * The response from the `onStatusChange` method.
- */
-export interface OnConnectionStatusChangeResponse {
-  /**
-   * Unregisters the listener that was added by the `onStatusChange` method.
-   */
-  off: () => void;
-}
-
 /**
  * Represents a connection to Ably.
  */
@@ -104,7 +95,7 @@ export interface Connection {
    * @param listener The function to call when the status changes.
    * @returns An object that can be used to unregister the listener.
    */
-  onStatusChange(listener: ConnectionStatusListener): OnConnectionStatusChangeResponse;
+  onStatusChange(listener: ConnectionStatusListener): StatusSubscription;
 
   /**
    * Removes all listeners that were added by the `onStatusChange` method.
@@ -194,7 +185,7 @@ export class DefaultConnection extends EventEmitter<ConnectionEventsMap> impleme
   /**
    * @inheritdoc
    */
-  onStatusChange(listener: ConnectionStatusListener): OnConnectionStatusChangeResponse {
+  onStatusChange(listener: ConnectionStatusListener): StatusSubscription {
     this.on(listener);
 
     return {
diff --git a/src/core/index.ts b/src/core/index.ts
index c995130bb..2ac0cf2c0 100644
--- a/src/core/index.ts
+++ b/src/core/index.ts
@@ -4,12 +4,7 @@
 
 export { ChatClient } from './chat.js';
 export type { ChatClientOptions } from './config.js';
-export type {
-  Connection,
-  ConnectionStatusChange,
-  ConnectionStatusListener,
-  OnConnectionStatusChangeResponse,
-} from './connection.js';
+export type { Connection, ConnectionStatusChange, ConnectionStatusListener } from './connection.js';
 export { ConnectionStatus } from './connection.js';
 export type { DiscontinuityListener, OnDiscontinuitySubscriptionResponse } from './discontinuity.js';
 export { ErrorCodes, errorInfoIs } from './errors.js';
@@ -52,9 +47,9 @@ export type {
 } from './room-options.js';
 export { AllFeaturesEnabled } from './room-options.js';
 export type { RoomReactionListener, RoomReactions, SendReactionParams } from './room-reactions.js';
-export type { OnRoomStatusChangeResponse, RoomStatusChange, RoomStatusListener } from './room-status.js';
+export type { RoomStatusChange, RoomStatusListener } from './room-status.js';
 export { RoomStatus } from './room-status.js';
 export type { Rooms } from './rooms.js';
-export type { Subscription } from './subscription.js';
+export type { StatusSubscription, Subscription } from './subscription.js';
 export type { Typing, TypingEvent, TypingListener } from './typing.js';
 export type { ChannelStateChange, ErrorInfo, RealtimePresenceParams } from 'ably';
diff --git a/src/core/room-status.ts b/src/core/room-status.ts
index cbdca459e..1cc725504 100644
--- a/src/core/room-status.ts
+++ b/src/core/room-status.ts
@@ -1,6 +1,7 @@
 import * as Ably from 'ably';
 
 import { Logger } from './logger.js';
+import { StatusSubscription } from './subscription.js';
 import EventEmitter from './utils/event-emitter.js';
 
 /**
@@ -86,16 +87,6 @@ export interface RoomStatusChange {
  */
 export type RoomStatusListener = (change: RoomStatusChange) => void;
 
-/**
- * The response from the `onChange` method.
- */
-export interface OnRoomStatusChangeResponse {
-  /**
-   * Unregisters the listener that was added by the `onChange` method.
-   */
-  off: () => void;
-}
-
 /**
  * Represents the status of a Room.
  */
@@ -115,7 +106,7 @@ export interface RoomLifecycle {
    * @param listener The function to call when the status changes.
    * @returns An object that can be used to unregister the listener.
    */
-  onChange(listener: RoomStatusListener): OnRoomStatusChangeResponse;
+  onChange(listener: RoomStatusListener): StatusSubscription;
 
   /**
    * Removes all listeners that were added by the `onChange` method.
@@ -201,7 +192,7 @@ export class DefaultRoomLifecycle extends EventEmitter<RoomStatusEventsMap> impl
   /**
    * @inheritdoc
    */
-  onChange(listener: RoomStatusListener): OnRoomStatusChangeResponse {
+  onChange(listener: RoomStatusListener): StatusSubscription {
     this.on(listener);
 
     return {
diff --git a/src/core/room.ts b/src/core/room.ts
index 810ef486a..2ccfe65eb 100644
--- a/src/core/room.ts
+++ b/src/core/room.ts
@@ -10,13 +10,8 @@ import { DefaultPresence, Presence } from './presence.js';
 import { ContributesToRoomLifecycle, RoomLifecycleManager } from './room-lifecycle-manager.js';
 import { NormalizedRoomOptions, RoomOptions, validateRoomOptions } from './room-options.js';
 import { DefaultRoomReactions, RoomReactions } from './room-reactions.js';
-import {
-  DefaultRoomLifecycle,
-  InternalRoomLifecycle,
-  OnRoomStatusChangeResponse,
-  RoomStatus,
-  RoomStatusListener,
-} from './room-status.js';
+import { DefaultRoomLifecycle, InternalRoomLifecycle, RoomStatus, RoomStatusListener } from './room-status.js';
+import { StatusSubscription } from './subscription.js';
 import { DefaultTyping, Typing } from './typing.js';
 
 /**
@@ -85,7 +80,7 @@ export interface Room {
    * @param listener The function to call when the status changes.
    * @returns An object that can be used to unregister the listener.
    */
-  onStatusChange(listener: RoomStatusListener): OnRoomStatusChangeResponse;
+  onStatusChange(listener: RoomStatusListener): StatusSubscription;
 
   /**
    * Removes all listeners that were added by the `onStatusChange` method.
@@ -328,7 +323,7 @@ export class DefaultRoom implements Room {
   /**
    * @inheritdoc Room
    */
-  onStatusChange(listener: RoomStatusListener): OnRoomStatusChangeResponse {
+  onStatusChange(listener: RoomStatusListener): StatusSubscription {
     return this._lifecycle.onChange(listener);
   }
 
diff --git a/src/core/subscription.ts b/src/core/subscription.ts
index 33ee2a8bf..739df7f23 100644
--- a/src/core/subscription.ts
+++ b/src/core/subscription.ts
@@ -19,3 +19,25 @@ export interface Subscription {
    */
   unsubscribe: () => void;
 }
+/**
+ * Represents a subscription to status change events that can be unsubscribed from. This
+ * interface provides a way to clean up and remove subscriptions when they are no longer needed.
+ *
+ * @interface
+ * @example
+ * ```typescript
+ * const s = someService.onStatusChange();
+ * const s2 = someOtherService.on()
+ * // Later when done with the subscription
+ * s.off();
+ * s2.off();
+ * ```
+ */
+export interface StatusSubscription {
+  /**
+   * Unsubscribes from the status change events. It will ensure that no
+   * further status change events will be sent to the subscriber and
+   * that references to the subscriber are cleaned up.
+   */
+  off: () => void;
+}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR refactors the subscription mechanism to use a common "off-able" interface for unsubscribing from events, reducing type duplication across modules. Key changes include:
- Introducing the new StatusSubscription interface in subscription.ts.
- Updating the return types for onStatusChange and onChange methods in connection.ts, room.ts, and room-status.ts.
- Removing redundant types like OnRoomStatusChangeResponse from the codebase.

### Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                      | Description                                                                    |
| ------------------------- | ------------------------------------------------------------------------------ |
| src/core/subscription.ts  | Added the StatusSubscription interface and updated documentation examples.   |
| src/core/index.ts         | Updated type exports to use the new StatusSubscription interface.            |
| src/core/room.ts          | Modified onStatusChange return type to StatusSubscription.                     |
| src/core/connection.ts    | Modified onStatusChange return type to StatusSubscription and removed redundancy. |
| src/core/room-status.ts   | Updated onChange return type to StatusSubscription and removed redundant types.|
</details>





state: COMMENTED
author_association: NONE
submitted_at: 2025-03-11T16:41:03Z
user: login: AndyTWF
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T16:41:27Z
user: login: vladvelici
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T11:21:05Z

Review comments:
diff_hunk: @@ -19,3 +19,25 @@ export interface Subscription {
    */
   unsubscribe: () => void;
 }
+/**
+ * Represents a subscription to status change events that can be unsubscribed from. This
+ * interface provides a way to clean up and remove subscriptions when they are no longer needed.
+ *
+ * @interface
+ * @example
+ * ```typescript
+ * const s = someService.onStatusChange();
+ * const s2 = someOtherService.on()
path: src/core/subscription.ts
user: login: Copilot
body: The example code appears to call 'on()' instead of 'onStatusChange()'. Please update the example to reflect the correct method call.
```suggestion
 * const s2 = someOtherService.onStatusChange();
```
created_at: 2025-03-11T16:41:03Z
author_association: NONE
diff_hunk: @@ -19,3 +19,25 @@ export interface Subscription {
    */
   unsubscribe: () => void;
 }
+/**
+ * Represents a subscription to status change events that can be unsubscribed from. This
+ * interface provides a way to clean up and remove subscriptions when they are no longer needed.
+ *
+ * @interface
+ * @example
+ * ```typescript
+ * const s = someService.onStatusChange();
+ * const s2 = someOtherService.on()
path: src/core/subscription.ts
user: login: AndyTWF
body: This is fine, it's an example.
created_at: 2025-03-11T16:41:27Z
author_association: COLLABORATOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-11T16:39:45Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on this repository.
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- 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-js&utm_content=498):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `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 generate unit testing code for this file.`
  -	`@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 generate unit testing code.`
  - `@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.`

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 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 -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-11T16:41:22Z
body: <h2>Coverage Report</h2>
<table> <thead> <tr> <th align="center">Status</th> <th align="left">Category</th> <th align="right">Percentage</th> <th align="right">Covered / Total</th> </tr> </thead> <tbody> <tr> <td align="center">🟢</td> <td align="left">Lines</td> <td align="right">94.09% (🎯 92%)</td> <td align="right">3283 / 3489</td> </tr> <tr> <td align="center">🟢</td> <td align="left">Statements</td> <td align="right">94.09% (🎯 92%)</td> <td align="right">3283 / 3489</td> </tr> <tr> <td align="center">🟢</td> <td align="left">Functions</td> <td align="right">98.84% (🎯 92%)</td> <td align="right">257 / 260</td> </tr> <tr> <td align="center">🟢</td> <td align="left">Branches</td> <td align="right">95.3% (🎯 93%)</td> <td align="right">853 / 895</td> </tr> </tbody> </table><details><summary>File Coverage</summary><table> <thead> <tr> <th align="left">File</th> <th align="right">Stmts</th> <th align="right">Branches</th> <th align="right">Functions</th> <th align="right">Lines</th> <th align="left">Uncovered Lines</th> </tr> </thead> <tbody> <tr> <td align="left" colspan="6"><b>Changed Files</b></td> </tr> <tr> <td align="left"><a href="https://github.com/ably/ably-chat-js/blob/cbfac9e77e91b2e961708586e5c7cf3178d109f0/src/core/connection.ts">src/core/connection.ts</a></td> <td align="right">97.84%</td> <td align="right">96.29%</td> <td align="right">100%</td> <td align="right">97.84%</td> <td align="left"><a href="https://github.com/ably/ably-chat-js/blob/cbfac9e77e91b2e961708586e5c7cf3178d109f0/src/core/connection.ts#L164-L165" class="text-red">164-165</a></td> </tr> <tr> <td align="left"><a href="https://github.com/ably/ably-chat-js/blob/cbfac9e77e91b2e961708586e5c7cf3178d109f0/src/core/room-status.ts">src/core/room-status.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/ably/ably-chat-js/blob/cbfac9e77e91b2e961708586e5c7cf3178d109f0/src/core/room.ts">src/core/room.ts</a></td> <td align="right">98.63%</td> <td align="right">100%</td> <td align="right">95%</td> <td align="right">98.63%</td> <td align="left"><a href="https://github.com/ably/ably-chat-js/blob/cbfac9e77e91b2e961708586e5c7cf3178d109f0/src/core/room.ts#L381-L382" class="text-red">381-382</a></td> </tr> <tr> <td align="left"><a href="https://github.com/ably/ably-chat-js/blob/cbfac9e77e91b2e961708586e5c7cf3178d109f0/src/core/subscription.ts">src/core/subscription.ts</a></td> <td align="right">0%</td> <td align="right">0%</td> <td align="right">0%</td> <td align="right">0%</td> <td align="left"></td> </tr> </tbody> </table></details>
<em>Generated in workflow <a href=https://github.com/ably/ably-chat-js/actions/runs/13810124776>#2105</a> for commit <a href="https://github.com/ably/ably-chat-js/commit/cbfac9e77e91b2e961708586e5c7cf3178d109f0">cbfac9e</a> by the <a href="https://github.com/davelosert/vitest-coverage-report-action">Vitest Coverage Report Action</a></em>

<!-- vitest-coverage-report-marker-root -->
author_association: NONE