Skip to content

Commit a7d25d3

Browse files
committed
feat!: update unsubscribe
1 parent 34aa113 commit a7d25d3

4 files changed

Lines changed: 82 additions & 20 deletions

File tree

packages/core-backend/CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- EVM (`eip155`) subscriptions are always enabled. Solana, Tron, and Stellar subscriptions are enabled when the corresponding `networkAssetsSnapsMigrationSolana` / `networkAssetsSnapsMigrationTron` / `networkAssetsSnapsMigrationStellar` feature flag has `stage >= 1`, and disabled otherwise.
1717
- Accounts whose scopes do not match an enabled chain are no longer subscribed at all.
1818
- When the enabled chains change via a feature flag update while the websocket is connected, the service automatically resubscribes the selected account.
19-
- **BREAKING:** `AccountActivityService.subscribe` now requires a `{ addresses: string[] }` argument ([#9531](https://github.com/MetaMask/core/pull/9531))
20-
- Previously, the service only allowed a single address to be subscribed at a time.
19+
- **BREAKING:** `AccountActivityService.subscribe` and `AccountActivityService.unsubscribe` now require a `{ addresses: string[] }` argument ([#9531](https://github.com/MetaMask/core/pull/9531))
20+
- Previously, the service only allowed a single address to be subscribed and unsubscribed at a time.
2121
- Add `@metamask/remote-feature-flag-controller` `^4.2.2` as a dependency ([#9379](https://github.com/MetaMask/core/pull/9379))
2222
- Bump `@metamask/messenger` from `^1.2.0` to `^2.0.0` ([#9392](https://github.com/MetaMask/core/pull/9392))
2323
- Bump `@metamask/profile-sync-controller` from `^28.2.0` to `^28.3.0` ([#9463](https://github.com/MetaMask/core/pull/9463))

packages/core-backend/src/ws/AccountActivityService-method-action-types.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ export type AccountActivityServiceSubscribeAction = {
1919
};
2020

2121
/**
22-
* Unsubscribe from account activity for specified address
23-
* Address should be in CAIP-10 format (e.g., "eip155:0:0x1234..." or "solana:0:ABC123...")
22+
* Unsubscribe from account activity for specified addresses
23+
* Addresses should be in CAIP-10 format (e.g., "eip155:0:0x1234..." or "solana:0:ABC123...")
2424
*
25-
* @param subscription - Account subscription configuration with address to unsubscribe
25+
* @param subscription - The subscription configuration
26+
* @param subscription.addresses - Array of addresses to unsubscribe from, each in CAIP-10 format
2627
*/
2728
export type AccountActivityServiceUnsubscribeAction = {
2829
type: `AccountActivityService:unsubscribe`;

packages/core-backend/src/ws/AccountActivityService.test.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@ import type {
1515
AccountActivityServiceMessenger,
1616
SubscriptionOptions,
1717
} from './AccountActivityService';
18-
import type { ServerNotificationMessage } from './BackendWebSocketService';
18+
import type {
19+
BackendWebSocketService,
20+
ServerNotificationMessage,
21+
} from './BackendWebSocketService';
1922
import { WebSocketState } from './BackendWebSocketService';
23+
import { BackendWebSocketServiceSubscribeAction } from './BackendWebSocketService-method-action-types';
2024

2125
type AllAccountActivityServiceActions =
2226
MessengerActions<AccountActivityServiceMessenger>;
@@ -618,7 +622,7 @@ describe('AccountActivityService', () => {
618622
// =============================================================================
619623
describe('unsubscribe', () => {
620624
const mockSubscription: SubscriptionOptions = {
621-
address: 'eip155:1:0x1234567890123456789012345678901234567890',
625+
addresses: ['eip155:1:0x1234567890123456789012345678901234567890'],
622626
};
623627

624628
it('should handle unsubscribe when not subscribed by returning early without errors', async () => {
@@ -636,6 +640,56 @@ describe('AccountActivityService', () => {
636640
});
637641
});
638642

643+
it('should unsubscribe from active subscriptions for multiple addresses', async () => {
644+
await withService(async ({ service, mocks }) => {
645+
const mockUnsubscribe = jest.fn();
646+
mocks.getSubscriptionsByChannel.mockReturnValueOnce([
647+
{
648+
subscriptionId: 'sub-123',
649+
channels: ['account-activity.v1.eip155:1:0x123abc'],
650+
unsubscribe: mockUnsubscribe,
651+
},
652+
]);
653+
mocks.getSubscriptionsByChannel.mockReturnValueOnce([
654+
{
655+
subscriptionId: 'sub-456',
656+
channels: ['account-activity.v1.eip155:1:0x456def'],
657+
unsubscribe: mockUnsubscribe,
658+
},
659+
]);
660+
661+
await service.unsubscribe({
662+
addresses: ['eip155:1:0x123abc', 'eip155:1:0x456def'],
663+
});
664+
665+
expect(mockUnsubscribe).toHaveBeenCalledTimes(2);
666+
});
667+
});
668+
669+
it('should not unsubscribe multiple times from the same subscription when multiple addresses share the same subscription', async () => {
670+
await withService(async ({ service, mocks }) => {
671+
const mockUnsubscribe = jest.fn().mockReturnValue(undefined);
672+
const subscriptions = [
673+
{
674+
subscriptionId: 'sub-123',
675+
channels: [
676+
'account-activity.v1.eip155:1:0x123abc',
677+
'account-activity.v1.eip155:1:0x456def',
678+
],
679+
unsubscribe: mockUnsubscribe,
680+
},
681+
];
682+
mocks.getSubscriptionsByChannel.mockReturnValue(subscriptions);
683+
684+
await service.unsubscribe({
685+
addresses: ['eip155:1:0x123abc', 'eip155:1:0x456def'],
686+
});
687+
688+
expect(mocks.getSubscriptionsByChannel).toHaveBeenCalledTimes(2);
689+
expect(mockUnsubscribe).toHaveBeenCalledTimes(1);
690+
});
691+
});
692+
639693
it('should handle unsubscribe errors by forcing WebSocket reconnection instead of throwing', async () => {
640694
await withService(
641695
{ accountAddress: '0x1234567890123456789012345678901234567890' },

packages/core-backend/src/ws/AccountActivityService.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ import type {
3131
BackendWebSocketServiceConnectionStateChangedEvent,
3232
ServerNotificationMessage,
3333
} from './BackendWebSocketService';
34-
import { WebSocketState } from './BackendWebSocketService';
34+
import {
35+
WebSocketState,
36+
WebSocketSubscription,
37+
} from './BackendWebSocketService';
3538
import type { BackendWebSocketServiceMethodActions } from './BackendWebSocketService-method-action-types';
3639

3740
// =============================================================================
@@ -335,26 +338,30 @@ export class AccountActivityService {
335338
}
336339

337340
/**
338-
* Unsubscribe from account activity for specified address
339-
* Address should be in CAIP-10 format (e.g., "eip155:0:0x1234..." or "solana:0:ABC123...")
341+
* Unsubscribe from account activity for specified addresses
342+
* Addresses should be in CAIP-10 format (e.g., "eip155:0:0x1234..." or "solana:0:ABC123...")
340343
*
341-
* @param subscription - Account subscription configuration with address to unsubscribe
344+
* @param subscription - The subscription configuration
345+
* @param subscription.addresses - Array of addresses to unsubscribe from, each in CAIP-10 format
342346
*/
343-
async unsubscribe(subscription: SubscriptionOptions): Promise<void> {
344-
const { address } = subscription;
347+
async unsubscribe({ addresses }: SubscriptionOptions): Promise<void> {
345348
try {
346-
// Find channel for the specified address
347-
const channel = `${this.#options.subscriptionNamespace}.${address}`;
348-
const subscriptions = this.#messenger.call(
349-
'BackendWebSocketService:getSubscriptionsByChannel',
350-
channel,
349+
// Find all subscriptions for the specified addresses
350+
const subscriptions = new Set<WebSocketSubscription>(
351+
addresses
352+
.map((address) =>
353+
this.#messenger.call(
354+
'BackendWebSocketService:getSubscriptionsByChannel',
355+
`${this.#options.subscriptionNamespace}.${address}`,
356+
),
357+
)
358+
.flat(),
351359
);
352360

353-
if (subscriptions.length === 0) {
361+
if (subscriptions.size === 0) {
354362
return;
355363
}
356364

357-
// Fast path: Direct unsubscribe using stored unsubscribe function
358365
// Unsubscribe from all matching subscriptions
359366
for (const subscriptionInfo of subscriptions) {
360367
await subscriptionInfo.unsubscribe();

0 commit comments

Comments
 (0)