Skip to content

Commit e856aaf

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

4 files changed

Lines changed: 77 additions & 19 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: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,7 @@ describe('AccountActivityService', () => {
618618
// =============================================================================
619619
describe('unsubscribe', () => {
620620
const mockSubscription: SubscriptionOptions = {
621-
address: 'eip155:1:0x1234567890123456789012345678901234567890',
621+
addresses: ['eip155:1:0x1234567890123456789012345678901234567890'],
622622
};
623623

624624
it('should handle unsubscribe when not subscribed by returning early without errors', async () => {
@@ -636,6 +636,56 @@ describe('AccountActivityService', () => {
636636
});
637637
});
638638

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