@metamask-previews/core-backend 12.1.0-preview-4fa35a45a → 12.1.0-preview-3d1e571
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +0 -4
- package/README.md +4 -9
- package/dist/ws/BackendWebSocketService-method-action-types.d.ts +3 -10
- package/dist/ws/BackendWebSocketService-method-action-types.d.ts.map +1 -1
- package/dist/ws/BackendWebSocketService-method-action-types.js.map +1 -1
- package/dist/ws/BackendWebSocketService.d.ts +3 -25
- package/dist/ws/BackendWebSocketService.d.ts.map +1 -1
- package/dist/ws/BackendWebSocketService.js +27 -162
- package/dist/ws/BackendWebSocketService.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,10 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
### Added
|
|
11
|
-
|
|
12
|
-
- Add optional `isBasicFunctionality` and `subscribeToBasicFunctionalityChange` options to `BackendWebSocketService`. When basic functionality is off, the service does not connect or reconnect, and an open socket is closed ([#10797](https://github.com/MetaMask/core/pull/10797))
|
|
13
|
-
|
|
14
10
|
## [12.1.0]
|
|
15
11
|
|
|
16
12
|
### Added
|
package/README.md
CHANGED
|
@@ -358,12 +358,11 @@ sequenceDiagram
|
|
|
358
358
|
|
|
359
359
|
### Connection Requirements
|
|
360
360
|
|
|
361
|
-
The WebSocket connects when **ALL
|
|
361
|
+
The WebSocket connects when **ALL 3 conditions are true**:
|
|
362
362
|
|
|
363
363
|
1. ✅ **Feature enabled** - `isEnabled()` callback returns `true` (feature flag)
|
|
364
|
-
2. ✅ **
|
|
365
|
-
3. ✅ **
|
|
366
|
-
4. ✅ **Wallet unlocked** - `KeyringController.isUnlocked = true`
|
|
364
|
+
2. ✅ **User signed in** - `AuthenticationController.isSignedIn = true`
|
|
365
|
+
3. ✅ **Wallet unlocked** - `KeyringController.isUnlocked = true`
|
|
367
366
|
|
|
368
367
|
**Plus:** Platform code must call `connect()` when app opens/foregrounds and `disconnect()` when app closes/backgrounds.
|
|
369
368
|
|
|
@@ -621,11 +620,7 @@ interface BackendWebSocketServiceOptions {
|
|
|
621
620
|
maxReconnectDelay?: number;
|
|
622
621
|
requestTimeout?: number;
|
|
623
622
|
enableAuthentication?: boolean;
|
|
624
|
-
|
|
625
|
-
isBasicFunctionality?: () => boolean;
|
|
626
|
-
subscribeToBasicFunctionalityChange?: (
|
|
627
|
-
onChange: () => void,
|
|
628
|
-
) => void | (() => void);
|
|
623
|
+
enabledCallback?: () => boolean;
|
|
629
624
|
}
|
|
630
625
|
```
|
|
631
626
|
|
|
@@ -8,9 +8,8 @@ import type { BackendWebSocketService } from './BackendWebSocketService.js';
|
|
|
8
8
|
*
|
|
9
9
|
* Connection Requirements (all must be true):
|
|
10
10
|
* 1. Feature enabled (isEnabled() = true)
|
|
11
|
-
* 2.
|
|
12
|
-
* 3.
|
|
13
|
-
* 4. User signed in (checked by getBearerToken)
|
|
11
|
+
* 2. Wallet unlocked (checked by getBearerToken)
|
|
12
|
+
* 3. User signed in (checked by getBearerToken)
|
|
14
13
|
*
|
|
15
14
|
* Platform code should call this when app opens/foregrounds.
|
|
16
15
|
* Automatically called on KeyringController:unlock event.
|
|
@@ -22,13 +21,7 @@ export type BackendWebSocketServiceConnectAction = {
|
|
|
22
21
|
handler: BackendWebSocketService['connect'];
|
|
23
22
|
};
|
|
24
23
|
/**
|
|
25
|
-
* Closes
|
|
26
|
-
*
|
|
27
|
-
* The timer is cleared before the early return. Lock, sign-out, and Basic
|
|
28
|
-
* Functionality off all call this, including when the socket is already gone
|
|
29
|
-
* and `onclose` will not run. `clearTimeout` has to run first: assigning
|
|
30
|
-
* `#reconnectTimer` to null leaves the callback scheduled. A socket still in
|
|
31
|
-
* CONNECTING is `#pendingWebSocket`, so this closes that too.
|
|
24
|
+
* Closes WebSocket connection
|
|
32
25
|
*/
|
|
33
26
|
export type BackendWebSocketServiceDisconnectAction = {
|
|
34
27
|
type: `BackendWebSocketService:disconnect`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BackendWebSocketService-method-action-types.d.ts","sourceRoot":"","sources":["../../src/ws/BackendWebSocketService-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAE5E
|
|
1
|
+
{"version":3,"file":"BackendWebSocketService-method-action-types.d.ts","sourceRoot":"","sources":["../../src/ws/BackendWebSocketService-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAE5E;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,oCAAoC,GAAG;IACjD,IAAI,EAAE,iCAAiC,CAAC;IACxC,OAAO,EAAE,uBAAuB,CAAC,SAAS,CAAC,CAAC;CAC7C,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,uCAAuC,GAAG;IACpD,IAAI,EAAE,oCAAoC,CAAC;IAC3C,OAAO,EAAE,uBAAuB,CAAC,YAAY,CAAC,CAAC;CAChD,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,8CAA8C,GAAG;IAC3D,IAAI,EAAE,2CAA2C,CAAC;IAClD,OAAO,EAAE,uBAAuB,CAAC,mBAAmB,CAAC,CAAC;CACvD,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,wCAAwC,GAAG;IACrD,IAAI,EAAE,qCAAqC,CAAC;IAC5C,OAAO,EAAE,uBAAuB,CAAC,aAAa,CAAC,CAAC;CACjD,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,wCAAwC,GAAG;IACrD,IAAI,EAAE,qCAAqC,CAAC;IAC5C,OAAO,EAAE,uBAAuB,CAAC,aAAa,CAAC,CAAC;CACjD,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,8CAA8C,GAAG;IAC3D,IAAI,EAAE,2CAA2C,CAAC;IAClD,OAAO,EAAE,uBAAuB,CAAC,mBAAmB,CAAC,CAAC;CACvD,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,sDAAsD,GAAG;IACnE,IAAI,EAAE,mDAAmD,CAAC;IAC1D,OAAO,EAAE,uBAAuB,CAAC,2BAA2B,CAAC,CAAC;CAC/D,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,mDAAmD,GAAG;IAChE,IAAI,EAAE,gDAAgD,CAAC;IACvD,OAAO,EAAE,uBAAuB,CAAC,wBAAwB,CAAC,CAAC;CAC5D,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,6DAA6D,GAAG;IAC1E,IAAI,EAAE,0DAA0D,CAAC;IACjE,OAAO,EAAE,uBAAuB,CAAC,kCAAkC,CAAC,CAAC;CACtE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,MAAM,+CAA+C,GAAG;IAC5D,IAAI,EAAE,4CAA4C,CAAC;IACnD,OAAO,EAAE,uBAAuB,CAAC,oBAAoB,CAAC,CAAC;CACxD,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,kDAAkD,GAAG;IAC/D,IAAI,EAAE,+CAA+C,CAAC;IACtD,OAAO,EAAE,uBAAuB,CAAC,uBAAuB,CAAC,CAAC;CAC3D,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,gDAAgD,GAAG;IAC7D,IAAI,EAAE,6CAA6C,CAAC;IACpD,OAAO,EAAE,uBAAuB,CAAC,qBAAqB,CAAC,CAAC;CACzD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,MAAM,MAAM,sCAAsC,GAAG;IACnD,IAAI,EAAE,mCAAmC,CAAC;IAC1C,OAAO,EAAE,uBAAuB,CAAC,WAAW,CAAC,CAAC;CAC/C,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,oCAAoC,GAC5C,oCAAoC,GACpC,uCAAuC,GACvC,8CAA8C,GAC9C,wCAAwC,GACxC,wCAAwC,GACxC,8CAA8C,GAC9C,sDAAsD,GACtD,mDAAmD,GACnD,6DAA6D,GAC7D,+CAA+C,GAC/C,kDAAkD,GAClD,gDAAgD,GAChD,sCAAsC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BackendWebSocketService-method-action-types.js","sourceRoot":"","sources":["../../src/ws/BackendWebSocketService-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * This file is auto generated.\n * Do not edit manually.\n */\n\nimport type { BackendWebSocketService } from './BackendWebSocketService.js';\n\n/**\n * Establishes WebSocket connection with smart reconnection behavior\n *\n * Connection Requirements (all must be true):\n * 1. Feature enabled (isEnabled() = true)\n * 2. Basic functionality enabled (isBasicFunctionality() = true)\n * 3. Wallet unlocked (checked by getBearerToken)\n * 4. User signed in (checked by getBearerToken)\n *\n * Platform code should call this when app opens/foregrounds.\n * Automatically called on KeyringController:unlock event.\n *\n * @returns Promise that resolves when connection is established\n */\nexport type BackendWebSocketServiceConnectAction = {\n type: `BackendWebSocketService:connect`;\n handler: BackendWebSocketService['connect'];\n};\n\n/**\n * Closes the WebSocket and cancels a queued reconnect.\n *\n * The timer is cleared before the early return. Lock, sign-out, and Basic\n * Functionality off all call this, including when the socket is already gone\n * and `onclose` will not run. `clearTimeout` has to run first: assigning\n * `#reconnectTimer` to null leaves the callback scheduled. A socket still in\n * CONNECTING is `#pendingWebSocket`, so this closes that too.\n */\nexport type BackendWebSocketServiceDisconnectAction = {\n type: `BackendWebSocketService:disconnect`;\n handler: BackendWebSocketService['disconnect'];\n};\n\n/**\n * Forces a WebSocket reconnection to clean up subscription state\n *\n * This method is useful when subscription state may be out of sync and needs to be reset.\n * It performs a controlled disconnect-then-reconnect sequence:\n * - Disconnects cleanly to trigger subscription cleanup\n * - Schedules reconnection with exponential backoff to prevent rapid loops\n * - All subscriptions will be cleaned up automatically on disconnect\n *\n * Use cases:\n * - Recovering from subscription/unsubscription issues\n * - Cleaning up orphaned subscriptions\n * - Forcing a fresh subscription state\n *\n * @returns Promise that resolves when disconnection is complete (reconnection is scheduled)\n */\nexport type BackendWebSocketServiceForceReconnectionAction = {\n type: `BackendWebSocketService:forceReconnection`;\n handler: BackendWebSocketService['forceReconnection'];\n};\n\n/**\n * Sends a message through the WebSocket (fire-and-forget, no response expected)\n *\n * This is a low-level method for sending messages without waiting for a response.\n * Most consumers should use `sendRequest()` instead, which handles request-response\n * correlation and provides proper error handling with timeouts.\n *\n * Use this method only when:\n * - You don't need a response from the server\n * - You're implementing custom message protocols\n * - You need fine-grained control over message timing\n *\n * @param message - The message to send\n * @throws Error if WebSocket is not connected or send fails\n *\n * @see sendRequest for request-response pattern with automatic correlation\n */\nexport type BackendWebSocketServiceSendMessageAction = {\n type: `BackendWebSocketService:sendMessage`;\n handler: BackendWebSocketService['sendMessage'];\n};\n\n/**\n * Sends a request and waits for a correlated response (recommended for most use cases)\n *\n * This is the recommended high-level method for request-response communication.\n * It automatically handles:\n * - Request ID generation and correlation\n * - Response matching with timeout protection\n * - Automatic reconnection on timeout\n * - Proper cleanup of pending requests\n *\n * @param message - The request message (can include optional requestId for testing)\n * @returns Promise that resolves with the response data\n * @throws Error if WebSocket is not connected, request times out, or response indicates failure\n *\n * @see sendMessage for fire-and-forget messaging without response handling\n */\nexport type BackendWebSocketServiceSendRequestAction = {\n type: `BackendWebSocketService:sendRequest`;\n handler: BackendWebSocketService['sendRequest'];\n};\n\n/**\n * Gets current connection information\n *\n * @returns Current connection status and details\n */\nexport type BackendWebSocketServiceGetConnectionInfoAction = {\n type: `BackendWebSocketService:getConnectionInfo`;\n handler: BackendWebSocketService['getConnectionInfo'];\n};\n\n/**\n * Gets all subscription information for a specific channel\n *\n * @param channel - The channel name to look up\n * @returns Array of subscription details for all subscriptions containing the channel\n */\nexport type BackendWebSocketServiceGetSubscriptionsByChannelAction = {\n type: `BackendWebSocketService:getSubscriptionsByChannel`;\n handler: BackendWebSocketService['getSubscriptionsByChannel'];\n};\n\n/**\n * Checks if a channel has a subscription\n *\n * @param channel - The channel name to check\n * @returns True if the channel has a subscription, false otherwise\n */\nexport type BackendWebSocketServiceChannelHasSubscriptionAction = {\n type: `BackendWebSocketService:channelHasSubscription`;\n handler: BackendWebSocketService['channelHasSubscription'];\n};\n\n/**\n * Finds all subscriptions that have channels starting with the specified prefix\n *\n * @param channelPrefix - The channel prefix to search for (e.g., \"account-activity.v1\")\n * @returns Array of subscription info for matching subscriptions\n */\nexport type BackendWebSocketServiceFindSubscriptionsByChannelPrefixAction = {\n type: `BackendWebSocketService:findSubscriptionsByChannelPrefix`;\n handler: BackendWebSocketService['findSubscriptionsByChannelPrefix'];\n};\n\n/**\n * Register a callback for specific channels (local callback only, no server subscription)\n *\n * **Key Difference from `subscribe()`:**\n * - `addChannelCallback()`: Registers a local callback without creating a server-side subscription.\n * The callback triggers on ANY message matching the channel name, regardless of subscriptionId.\n * Useful for system-wide notifications or when you don't control the subscription lifecycle.\n *\n * - `subscribe()`: Creates a proper server-side subscription with a subscriptionId.\n * The callback only triggers for messages with the matching subscriptionId.\n * Includes proper lifecycle management (unsubscribe, automatic cleanup on disconnect).\n *\n * **When to use `addChannelCallback()`:**\n * - Listening to system-wide notifications (e.g., 'system-notifications.v1')\n * - Monitoring channels where subscriptions are managed elsewhere\n * - Debug/logging scenarios where you want to observe all channel messages\n *\n * **When to use `subscribe()` instead:**\n * - Creating new subscriptions that need server-side registration\n * - When you need proper cleanup via unsubscribe\n * - Most application use cases (recommended approach)\n *\n * @param options - Channel callback configuration\n * @param options.channelName - Channel name to match exactly\n * @param options.callback - Function to call when channel matches\n *\n * @example\n * ```typescript\n * // Listen to system notifications (no server subscription needed)\n * webSocketService.addChannelCallback({\n * channelName: 'system-notifications.v1',\n * callback: (notification) => {\n * console.log('System notification:', notification.data);\n * }\n * });\n *\n * // For account-specific subscriptions, use subscribe() instead:\n * // const sub = await webSocketService.subscribe({\n * // channels: ['account-activity.v1.eip155:0:0x1234...'],\n * // callback: (notification) => { ... }\n * // });\n * ```\n *\n * @see subscribe for creating proper server-side subscriptions with lifecycle management\n */\nexport type BackendWebSocketServiceAddChannelCallbackAction = {\n type: `BackendWebSocketService:addChannelCallback`;\n handler: BackendWebSocketService['addChannelCallback'];\n};\n\n/**\n * Remove a channel callback\n *\n * @param channelName - The channel name returned from addChannelCallback\n * @returns True if callback was found and removed, false otherwise\n */\nexport type BackendWebSocketServiceRemoveChannelCallbackAction = {\n type: `BackendWebSocketService:removeChannelCallback`;\n handler: BackendWebSocketService['removeChannelCallback'];\n};\n\n/**\n * Get all registered channel callbacks (for debugging)\n *\n * @returns Array of all registered channel callbacks\n */\nexport type BackendWebSocketServiceGetChannelCallbacksAction = {\n type: `BackendWebSocketService:getChannelCallbacks`;\n handler: BackendWebSocketService['getChannelCallbacks'];\n};\n\n/**\n * Create and manage a subscription with server-side registration (recommended for most use cases)\n *\n * This is the recommended subscription API for high-level services. It creates a proper\n * server-side subscription and routes notifications based on subscriptionId.\n *\n * **Key Features:**\n * - Creates server-side subscription with unique subscriptionId\n * - Callback triggered only for messages with matching subscriptionId\n * - Automatic lifecycle management (cleanup on disconnect)\n * - Includes unsubscribe method for proper cleanup\n * - Request-response pattern with error handling\n *\n * **When to use `subscribe()`:**\n * - Creating new subscriptions (account activity, price updates, etc.)\n * - When you need proper cleanup/unsubscribe functionality\n * - Most application use cases\n *\n * **When to use `addChannelCallback()` instead:**\n * - System-wide notifications without server-side subscription\n * - Observing channels managed elsewhere\n * - Debug/logging scenarios\n *\n * @param options - Subscription configuration\n * @param options.channels - Array of channel names to subscribe to\n * @param options.callback - Callback function for handling notifications\n * @param options.requestId - Optional request ID for testing (will generate UUID if not provided)\n * @param options.channelType - Channel type identifier\n * @returns Subscription object with unsubscribe method\n *\n * @example\n * ```typescript\n * // AccountActivityService usage\n * const subscription = await webSocketService.subscribe({\n * channels: ['account-activity.v1.eip155:0:0x1234...'],\n * callback: (notification) => {\n * this.handleAccountActivity(notification.data);\n * }\n * });\n *\n * // Later, clean up\n * await subscription.unsubscribe();\n * ```\n *\n * @see addChannelCallback for local callbacks without server-side subscription\n */\nexport type BackendWebSocketServiceSubscribeAction = {\n type: `BackendWebSocketService:subscribe`;\n handler: BackendWebSocketService['subscribe'];\n};\n\n/**\n * Union of all BackendWebSocketService action types.\n */\nexport type BackendWebSocketServiceMethodActions =\n | BackendWebSocketServiceConnectAction\n | BackendWebSocketServiceDisconnectAction\n | BackendWebSocketServiceForceReconnectionAction\n | BackendWebSocketServiceSendMessageAction\n | BackendWebSocketServiceSendRequestAction\n | BackendWebSocketServiceGetConnectionInfoAction\n | BackendWebSocketServiceGetSubscriptionsByChannelAction\n | BackendWebSocketServiceChannelHasSubscriptionAction\n | BackendWebSocketServiceFindSubscriptionsByChannelPrefixAction\n | BackendWebSocketServiceAddChannelCallbackAction\n | BackendWebSocketServiceRemoveChannelCallbackAction\n | BackendWebSocketServiceGetChannelCallbacksAction\n | BackendWebSocketServiceSubscribeAction;\n"]}
|
|
1
|
+
{"version":3,"file":"BackendWebSocketService-method-action-types.js","sourceRoot":"","sources":["../../src/ws/BackendWebSocketService-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * This file is auto generated.\n * Do not edit manually.\n */\n\nimport type { BackendWebSocketService } from './BackendWebSocketService.js';\n\n/**\n * Establishes WebSocket connection with smart reconnection behavior\n *\n * Connection Requirements (all must be true):\n * 1. Feature enabled (isEnabled() = true)\n * 2. Wallet unlocked (checked by getBearerToken)\n * 3. User signed in (checked by getBearerToken)\n *\n * Platform code should call this when app opens/foregrounds.\n * Automatically called on KeyringController:unlock event.\n *\n * @returns Promise that resolves when connection is established\n */\nexport type BackendWebSocketServiceConnectAction = {\n type: `BackendWebSocketService:connect`;\n handler: BackendWebSocketService['connect'];\n};\n\n/**\n * Closes WebSocket connection\n */\nexport type BackendWebSocketServiceDisconnectAction = {\n type: `BackendWebSocketService:disconnect`;\n handler: BackendWebSocketService['disconnect'];\n};\n\n/**\n * Forces a WebSocket reconnection to clean up subscription state\n *\n * This method is useful when subscription state may be out of sync and needs to be reset.\n * It performs a controlled disconnect-then-reconnect sequence:\n * - Disconnects cleanly to trigger subscription cleanup\n * - Schedules reconnection with exponential backoff to prevent rapid loops\n * - All subscriptions will be cleaned up automatically on disconnect\n *\n * Use cases:\n * - Recovering from subscription/unsubscription issues\n * - Cleaning up orphaned subscriptions\n * - Forcing a fresh subscription state\n *\n * @returns Promise that resolves when disconnection is complete (reconnection is scheduled)\n */\nexport type BackendWebSocketServiceForceReconnectionAction = {\n type: `BackendWebSocketService:forceReconnection`;\n handler: BackendWebSocketService['forceReconnection'];\n};\n\n/**\n * Sends a message through the WebSocket (fire-and-forget, no response expected)\n *\n * This is a low-level method for sending messages without waiting for a response.\n * Most consumers should use `sendRequest()` instead, which handles request-response\n * correlation and provides proper error handling with timeouts.\n *\n * Use this method only when:\n * - You don't need a response from the server\n * - You're implementing custom message protocols\n * - You need fine-grained control over message timing\n *\n * @param message - The message to send\n * @throws Error if WebSocket is not connected or send fails\n *\n * @see sendRequest for request-response pattern with automatic correlation\n */\nexport type BackendWebSocketServiceSendMessageAction = {\n type: `BackendWebSocketService:sendMessage`;\n handler: BackendWebSocketService['sendMessage'];\n};\n\n/**\n * Sends a request and waits for a correlated response (recommended for most use cases)\n *\n * This is the recommended high-level method for request-response communication.\n * It automatically handles:\n * - Request ID generation and correlation\n * - Response matching with timeout protection\n * - Automatic reconnection on timeout\n * - Proper cleanup of pending requests\n *\n * @param message - The request message (can include optional requestId for testing)\n * @returns Promise that resolves with the response data\n * @throws Error if WebSocket is not connected, request times out, or response indicates failure\n *\n * @see sendMessage for fire-and-forget messaging without response handling\n */\nexport type BackendWebSocketServiceSendRequestAction = {\n type: `BackendWebSocketService:sendRequest`;\n handler: BackendWebSocketService['sendRequest'];\n};\n\n/**\n * Gets current connection information\n *\n * @returns Current connection status and details\n */\nexport type BackendWebSocketServiceGetConnectionInfoAction = {\n type: `BackendWebSocketService:getConnectionInfo`;\n handler: BackendWebSocketService['getConnectionInfo'];\n};\n\n/**\n * Gets all subscription information for a specific channel\n *\n * @param channel - The channel name to look up\n * @returns Array of subscription details for all subscriptions containing the channel\n */\nexport type BackendWebSocketServiceGetSubscriptionsByChannelAction = {\n type: `BackendWebSocketService:getSubscriptionsByChannel`;\n handler: BackendWebSocketService['getSubscriptionsByChannel'];\n};\n\n/**\n * Checks if a channel has a subscription\n *\n * @param channel - The channel name to check\n * @returns True if the channel has a subscription, false otherwise\n */\nexport type BackendWebSocketServiceChannelHasSubscriptionAction = {\n type: `BackendWebSocketService:channelHasSubscription`;\n handler: BackendWebSocketService['channelHasSubscription'];\n};\n\n/**\n * Finds all subscriptions that have channels starting with the specified prefix\n *\n * @param channelPrefix - The channel prefix to search for (e.g., \"account-activity.v1\")\n * @returns Array of subscription info for matching subscriptions\n */\nexport type BackendWebSocketServiceFindSubscriptionsByChannelPrefixAction = {\n type: `BackendWebSocketService:findSubscriptionsByChannelPrefix`;\n handler: BackendWebSocketService['findSubscriptionsByChannelPrefix'];\n};\n\n/**\n * Register a callback for specific channels (local callback only, no server subscription)\n *\n * **Key Difference from `subscribe()`:**\n * - `addChannelCallback()`: Registers a local callback without creating a server-side subscription.\n * The callback triggers on ANY message matching the channel name, regardless of subscriptionId.\n * Useful for system-wide notifications or when you don't control the subscription lifecycle.\n *\n * - `subscribe()`: Creates a proper server-side subscription with a subscriptionId.\n * The callback only triggers for messages with the matching subscriptionId.\n * Includes proper lifecycle management (unsubscribe, automatic cleanup on disconnect).\n *\n * **When to use `addChannelCallback()`:**\n * - Listening to system-wide notifications (e.g., 'system-notifications.v1')\n * - Monitoring channels where subscriptions are managed elsewhere\n * - Debug/logging scenarios where you want to observe all channel messages\n *\n * **When to use `subscribe()` instead:**\n * - Creating new subscriptions that need server-side registration\n * - When you need proper cleanup via unsubscribe\n * - Most application use cases (recommended approach)\n *\n * @param options - Channel callback configuration\n * @param options.channelName - Channel name to match exactly\n * @param options.callback - Function to call when channel matches\n *\n * @example\n * ```typescript\n * // Listen to system notifications (no server subscription needed)\n * webSocketService.addChannelCallback({\n * channelName: 'system-notifications.v1',\n * callback: (notification) => {\n * console.log('System notification:', notification.data);\n * }\n * });\n *\n * // For account-specific subscriptions, use subscribe() instead:\n * // const sub = await webSocketService.subscribe({\n * // channels: ['account-activity.v1.eip155:0:0x1234...'],\n * // callback: (notification) => { ... }\n * // });\n * ```\n *\n * @see subscribe for creating proper server-side subscriptions with lifecycle management\n */\nexport type BackendWebSocketServiceAddChannelCallbackAction = {\n type: `BackendWebSocketService:addChannelCallback`;\n handler: BackendWebSocketService['addChannelCallback'];\n};\n\n/**\n * Remove a channel callback\n *\n * @param channelName - The channel name returned from addChannelCallback\n * @returns True if callback was found and removed, false otherwise\n */\nexport type BackendWebSocketServiceRemoveChannelCallbackAction = {\n type: `BackendWebSocketService:removeChannelCallback`;\n handler: BackendWebSocketService['removeChannelCallback'];\n};\n\n/**\n * Get all registered channel callbacks (for debugging)\n *\n * @returns Array of all registered channel callbacks\n */\nexport type BackendWebSocketServiceGetChannelCallbacksAction = {\n type: `BackendWebSocketService:getChannelCallbacks`;\n handler: BackendWebSocketService['getChannelCallbacks'];\n};\n\n/**\n * Create and manage a subscription with server-side registration (recommended for most use cases)\n *\n * This is the recommended subscription API for high-level services. It creates a proper\n * server-side subscription and routes notifications based on subscriptionId.\n *\n * **Key Features:**\n * - Creates server-side subscription with unique subscriptionId\n * - Callback triggered only for messages with matching subscriptionId\n * - Automatic lifecycle management (cleanup on disconnect)\n * - Includes unsubscribe method for proper cleanup\n * - Request-response pattern with error handling\n *\n * **When to use `subscribe()`:**\n * - Creating new subscriptions (account activity, price updates, etc.)\n * - When you need proper cleanup/unsubscribe functionality\n * - Most application use cases\n *\n * **When to use `addChannelCallback()` instead:**\n * - System-wide notifications without server-side subscription\n * - Observing channels managed elsewhere\n * - Debug/logging scenarios\n *\n * @param options - Subscription configuration\n * @param options.channels - Array of channel names to subscribe to\n * @param options.callback - Callback function for handling notifications\n * @param options.requestId - Optional request ID for testing (will generate UUID if not provided)\n * @param options.channelType - Channel type identifier\n * @returns Subscription object with unsubscribe method\n *\n * @example\n * ```typescript\n * // AccountActivityService usage\n * const subscription = await webSocketService.subscribe({\n * channels: ['account-activity.v1.eip155:0:0x1234...'],\n * callback: (notification) => {\n * this.handleAccountActivity(notification.data);\n * }\n * });\n *\n * // Later, clean up\n * await subscription.unsubscribe();\n * ```\n *\n * @see addChannelCallback for local callbacks without server-side subscription\n */\nexport type BackendWebSocketServiceSubscribeAction = {\n type: `BackendWebSocketService:subscribe`;\n handler: BackendWebSocketService['subscribe'];\n};\n\n/**\n * Union of all BackendWebSocketService action types.\n */\nexport type BackendWebSocketServiceMethodActions =\n | BackendWebSocketServiceConnectAction\n | BackendWebSocketServiceDisconnectAction\n | BackendWebSocketServiceForceReconnectionAction\n | BackendWebSocketServiceSendMessageAction\n | BackendWebSocketServiceSendRequestAction\n | BackendWebSocketServiceGetConnectionInfoAction\n | BackendWebSocketServiceGetSubscriptionsByChannelAction\n | BackendWebSocketServiceChannelHasSubscriptionAction\n | BackendWebSocketServiceFindSubscriptionsByChannelPrefixAction\n | BackendWebSocketServiceAddChannelCallbackAction\n | BackendWebSocketServiceRemoveChannelCallbackAction\n | BackendWebSocketServiceGetChannelCallbacksAction\n | BackendWebSocketServiceSubscribeAction;\n"]}
|
|
@@ -52,20 +52,6 @@ export type BackendWebSocketServiceOptions = {
|
|
|
52
52
|
requestTimeout?: number;
|
|
53
53
|
/** Optional callback to determine if connection should be enabled (default: always enabled) */
|
|
54
54
|
isEnabled?: () => boolean;
|
|
55
|
-
/**
|
|
56
|
-
* Getter for basic functionality (matches the "Basic functionality" setting).
|
|
57
|
-
* When it returns true, the backend WebSocket may connect. When false, the
|
|
58
|
-
* service does not connect or reconnect, and an open socket is closed.
|
|
59
|
-
* Defaults to () => true when omitted.
|
|
60
|
-
*/
|
|
61
|
-
isBasicFunctionality?: () => boolean;
|
|
62
|
-
/**
|
|
63
|
-
* Called by the service with an onChange callback. The consumer subscribes to
|
|
64
|
-
* its basic-functionality source and invokes onChange when the value changes.
|
|
65
|
-
* The service re-reads `isBasicFunctionality()` and connects or disconnects.
|
|
66
|
-
* May return an unsubscribe function, which is called on destroy. Optional.
|
|
67
|
-
*/
|
|
68
|
-
subscribeToBasicFunctionalityChange?: (onChange: () => void) => void | (() => void);
|
|
69
55
|
/** Optional callback to trace performance of WebSocket operations (default: no-op) */
|
|
70
56
|
traceFn?: TraceCallback;
|
|
71
57
|
};
|
|
@@ -170,7 +156,6 @@ export type BackendWebSocketServiceMessenger = Messenger<typeof SERVICE_NAME, Ba
|
|
|
170
156
|
* - Call connect() when app opens/foregrounds
|
|
171
157
|
* - Call disconnect() when app closes/backgrounds
|
|
172
158
|
* - Provide isEnabled() callback (feature flag)
|
|
173
|
-
* - Provide isBasicFunctionality() when Basic Functionality can be turned off
|
|
174
159
|
* - Call destroy() on app termination
|
|
175
160
|
*
|
|
176
161
|
* Real-Time Performance Optimizations:
|
|
@@ -197,9 +182,8 @@ export declare class BackendWebSocketService {
|
|
|
197
182
|
*
|
|
198
183
|
* Connection Requirements (all must be true):
|
|
199
184
|
* 1. Feature enabled (isEnabled() = true)
|
|
200
|
-
* 2.
|
|
201
|
-
* 3.
|
|
202
|
-
* 4. User signed in (checked by getBearerToken)
|
|
185
|
+
* 2. Wallet unlocked (checked by getBearerToken)
|
|
186
|
+
* 3. User signed in (checked by getBearerToken)
|
|
203
187
|
*
|
|
204
188
|
* Platform code should call this when app opens/foregrounds.
|
|
205
189
|
* Automatically called on KeyringController:unlock event.
|
|
@@ -208,13 +192,7 @@ export declare class BackendWebSocketService {
|
|
|
208
192
|
*/
|
|
209
193
|
connect(): Promise<void>;
|
|
210
194
|
/**
|
|
211
|
-
* Closes
|
|
212
|
-
*
|
|
213
|
-
* The timer is cleared before the early return. Lock, sign-out, and Basic
|
|
214
|
-
* Functionality off all call this, including when the socket is already gone
|
|
215
|
-
* and `onclose` will not run. `clearTimeout` has to run first: assigning
|
|
216
|
-
* `#reconnectTimer` to null leaves the callback scheduled. A socket still in
|
|
217
|
-
* CONNECTING is `#pendingWebSocket`, so this closes that too.
|
|
195
|
+
* Closes WebSocket connection
|
|
218
196
|
*/
|
|
219
197
|
disconnect(): void;
|
|
220
198
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BackendWebSocketService.d.ts","sourceRoot":"","sources":["../../src/ws/BackendWebSocketService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEhE,OAAO,KAAK,EACV,0BAA0B,EAC1B,4BAA4B,EAC7B,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAKlF,OAAO,KAAK,EAAE,oCAAoC,EAAE,MAAM,kDAAkD,CAAC;AAE7G,QAAA,MAAM,YAAY,EAAG,yBAAkC,CAAC;AA0BxD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA2CnD;AAED;;GAEG;AACH,oBAAY,cAAc;IACxB,UAAU,eAAe;IACzB,SAAS,cAAc;IACvB,wGAAwG;IACxG,aAAa,kBAAkB;IAC/B,YAAY,iBAAiB;IAC7B,yGAAyG;IACzG,KAAK,UAAU;CAChB;AAED;;GAEG;AACH,oBAAY,kBAAkB;IAC5B,SAAS,cAAc;IACvB,YAAY,iBAAiB;IAC7B,OAAO,YAAY;IACnB,KAAK,UAAU;IACf,YAAY,iBAAiB;IAC7B,WAAW,gBAAgB;CAC5B;AAED;;GAEG;AACH,MAAM,MAAM,8BAA8B,GAAG;IAC3C,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAC;IAEZ,oDAAoD;IACpD,SAAS,EAAE,gCAAgC,CAAC;IAE5C,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,kEAAkE;IAClE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B,uDAAuD;IACvD,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,+FAA+F;IAC/F,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC;IAE1B
|
|
1
|
+
{"version":3,"file":"BackendWebSocketService.d.ts","sourceRoot":"","sources":["../../src/ws/BackendWebSocketService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEhE,OAAO,KAAK,EACV,0BAA0B,EAC1B,4BAA4B,EAC7B,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAKlF,OAAO,KAAK,EAAE,oCAAoC,EAAE,MAAM,kDAAkD,CAAC;AAE7G,QAAA,MAAM,YAAY,EAAG,yBAAkC,CAAC;AA0BxD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA2CnD;AAED;;GAEG;AACH,oBAAY,cAAc;IACxB,UAAU,eAAe;IACzB,SAAS,cAAc;IACvB,wGAAwG;IACxG,aAAa,kBAAkB;IAC/B,YAAY,iBAAiB;IAC7B,yGAAyG;IACzG,KAAK,UAAU;CAChB;AAED;;GAEG;AACH,oBAAY,kBAAkB;IAC5B,SAAS,cAAc;IACvB,YAAY,iBAAiB;IAC7B,OAAO,YAAY;IACnB,KAAK,UAAU;IACf,YAAY,iBAAiB;IAC7B,WAAW,gBAAgB;CAC5B;AAED;;GAEG;AACH,MAAM,MAAM,8BAA8B,GAAG;IAC3C,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAC;IAEZ,oDAAoD;IACpD,SAAS,EAAE,gCAAgC,CAAC;IAE5C,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,kEAAkE;IAClE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B,uDAAuD;IACvD,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,+FAA+F;IAC/F,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC;IAE1B,sFAAsF;IACtF,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE;QACJ,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;CACH,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE;QACJ,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;QACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;CACH,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GACxB,oBAAoB,GACpB,qBAAqB,GACrB,yBAAyB,CAAC;AAE9B;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAC;IACpB,wBAAwB;IACxB,QAAQ,EAAE,CAAC,YAAY,EAAE,yBAAyB,KAAK,IAAI,CAAC;CAC7D,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,0CAA0C;IAC1C,cAAc,EAAE,MAAM,CAAC;IACvB,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,2FAA2F;IAC3F,WAAW,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,yBAAyB,KAAK,IAAI,CAAC;IAC7D,2CAA2C;IAC3C,WAAW,EAAE,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACpD,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,cAAc,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAGF,MAAM,MAAM,8BAA8B,GACxC,oCAAoC,CAAC;AAEvC,KAAK,cAAc,GACjB,wBAAwB,CAAC,4CAA4C,CAAC;AAGxE,MAAM,MAAM,kDAAkD,GAAG;IAC/D,IAAI,EAAE,gDAAgD,CAAC;IACvD,OAAO,EAAE,CAAC,uBAAuB,CAAC,CAAC;CACpC,CAAC;AAEF,KAAK,aAAa,GACd,wBAAwB,CAAC,wCAAwC,GACjE,0BAA0B,GAC1B,4BAA4B,CAAC;AAEjC,MAAM,MAAM,6BAA6B,GACvC,kDAAkD,CAAC;AAErD,MAAM,MAAM,gCAAgC,GAAG,SAAS,CACtD,OAAO,YAAY,EACnB,8BAA8B,GAAG,cAAc,EAC/C,6BAA6B,GAAG,aAAa,CAC9C,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,uBAAuB;;IAClC;;OAEG;IACH,QAAQ,CAAC,IAAI,4BAAgB;IAuD7B;;;;OAIG;IACH,YAAY,OAAO,EAAE,8BAA8B,EA4BlD;IA2CD;;;;;;;;;;;;OAYG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CA2D7B;IAED;;OAEG;IACH,UAAU,IAAI,IAAI,CASjB;IAED;;;;;;;;;;;;;;;OAeG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAWvC;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,WAAW,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAY/C;IAED;;;;;;;;;;;;;;;OAeG;IACG,WAAW,CAAC,IAAI,GAAG,qBAAqB,CAAC,MAAM,CAAC,EACpD,OAAO,EAAE,IAAI,CAAC,oBAAoB,EAAE,MAAM,CAAC,GAAG;QAC5C,IAAI,CAAC,EAAE,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC,GAAG;YACvD,SAAS,CAAC,EAAE,MAAM,CAAC;SACpB,CAAC;KACH,GACA,OAAO,CAAC,IAAI,CAAC,CAiDf;IAED;;;;OAIG;IACH,iBAAiB,IAAI,uBAAuB,CAW3C;IAED;;;;;OAKG;IACH,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,EAAE,CAalE;IAED;;;;;OAKG;IACH,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAO/C;IAED;;;;;OAKG;IACH,gCAAgC,CAC9B,aAAa,EAAE,MAAM,GACpB,qBAAqB,EAAE,CAoBzB;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4CG;IACH,kBAAkB,CAAC,OAAO,EAAE;QAC1B,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,CAAC,YAAY,EAAE,yBAAyB,KAAK,IAAI,CAAC;KAC7D,GAAG,IAAI,CAYP;IAED;;;;;OAKG;IACH,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAElD;IAED;;;;OAIG;IACH,mBAAmB,IAAI,eAAe,EAAE,CAEvC;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAUd;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6CG;IACG,SAAS,CAAC,OAAO,EAAE;QACvB,oCAAoC;QACpC,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,yFAAyF;QACzF,WAAW,EAAE,MAAM,CAAC;QACpB,yCAAyC;QACzC,QAAQ,EAAE,CAAC,YAAY,EAAE,yBAAyB,KAAK,IAAI,CAAC;QAC5D,2EAA2E;QAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAsDjC;CA8dF"}
|
|
@@ -112,7 +112,6 @@ export var WebSocketEventType;
|
|
|
112
112
|
* - Call connect() when app opens/foregrounds
|
|
113
113
|
* - Call disconnect() when app closes/backgrounds
|
|
114
114
|
* - Provide isEnabled() callback (feature flag)
|
|
115
|
-
* - Provide isBasicFunctionality() when Basic Functionality can be turned off
|
|
116
115
|
* - Call destroy() on app termination
|
|
117
116
|
*
|
|
118
117
|
* Real-Time Performance Optimizations:
|
|
@@ -130,21 +129,8 @@ export class BackendWebSocketService {
|
|
|
130
129
|
#messenger;
|
|
131
130
|
#options;
|
|
132
131
|
#isEnabled;
|
|
133
|
-
#isBasicFunctionality;
|
|
134
|
-
#unsubscribeBasicFunctionality;
|
|
135
132
|
#trace;
|
|
136
133
|
#ws;
|
|
137
|
-
/**
|
|
138
|
-
* Socket created for the current handshake, before `onopen` assigns `#ws`.
|
|
139
|
-
* `disconnect()` closes this so Basic Functionality can abort CONNECTING.
|
|
140
|
-
*/
|
|
141
|
-
#pendingWebSocket;
|
|
142
|
-
/**
|
|
143
|
-
* True after `disconnect()` asks the socket to close. A handshake still in
|
|
144
|
-
* CONNECTING cannot carry the manual close code: browsers report 1006 and an
|
|
145
|
-
* empty reason instead.
|
|
146
|
-
*/
|
|
147
|
-
#manualClose = false;
|
|
148
134
|
#state = WebSocketState.DISCONNECTED;
|
|
149
135
|
#reconnectAttempts = 0;
|
|
150
136
|
#reconnectTimer = null;
|
|
@@ -175,8 +161,6 @@ export class BackendWebSocketService {
|
|
|
175
161
|
constructor(options) {
|
|
176
162
|
this.#messenger = options.messenger;
|
|
177
163
|
this.#isEnabled = options.isEnabled;
|
|
178
|
-
this.#isBasicFunctionality =
|
|
179
|
-
options.isBasicFunctionality ?? (() => true);
|
|
180
164
|
// Default to no-op trace function to keep core platform-agnostic
|
|
181
165
|
this.#trace =
|
|
182
166
|
options.traceFn ??
|
|
@@ -195,73 +179,6 @@ export class BackendWebSocketService {
|
|
|
195
179
|
this.#subscribeEvents();
|
|
196
180
|
// Register action handlers using the method actions pattern
|
|
197
181
|
this.#messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
|
|
198
|
-
if (options.subscribeToBasicFunctionalityChange) {
|
|
199
|
-
const unsubscribe = options.subscribeToBasicFunctionalityChange(() => {
|
|
200
|
-
this.#handleBasicFunctionalityChange();
|
|
201
|
-
});
|
|
202
|
-
if (typeof unsubscribe === 'function') {
|
|
203
|
-
this.#unsubscribeBasicFunctionality = unsubscribe;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Whether a connection attempt is allowed.
|
|
209
|
-
*
|
|
210
|
-
* Both gates are required when provided: the product feature flag and the
|
|
211
|
-
* user's Basic Functionality setting.
|
|
212
|
-
*
|
|
213
|
-
* @returns True when the socket may connect.
|
|
214
|
-
*/
|
|
215
|
-
#isConnectionAllowed() {
|
|
216
|
-
if (this.#isEnabled && !this.#isEnabled()) {
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
return this.#isBasicFunctionality();
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Drop a queued reconnect and start the backoff over.
|
|
223
|
-
* Does not close an open or in-flight socket.
|
|
224
|
-
*/
|
|
225
|
-
#clearReconnectSchedule() {
|
|
226
|
-
if (this.#reconnectTimer) {
|
|
227
|
-
clearTimeout(this.#reconnectTimer);
|
|
228
|
-
this.#reconnectTimer = null;
|
|
229
|
-
}
|
|
230
|
-
this.#reconnectAttempts = 0;
|
|
231
|
-
this.#newBackoff();
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
|
-
* Stop a connection that the gates no longer allow.
|
|
235
|
-
*
|
|
236
|
-
* Basic Functionality closes the socket. A disabled feature flag only
|
|
237
|
-
* cancels a queued reconnect and leaves an open socket in place.
|
|
238
|
-
*
|
|
239
|
-
* @returns True when the caller should not open a socket.
|
|
240
|
-
*/
|
|
241
|
-
#stopDisallowedConnection() {
|
|
242
|
-
if (this.#isConnectionAllowed()) {
|
|
243
|
-
return false;
|
|
244
|
-
}
|
|
245
|
-
this.#clearReconnectSchedule();
|
|
246
|
-
if (!this.#isBasicFunctionality()) {
|
|
247
|
-
this.disconnect();
|
|
248
|
-
}
|
|
249
|
-
return true;
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Apply a Basic Functionality change. The getter is the source of truth.
|
|
253
|
-
*
|
|
254
|
-
* Turning the setting off uses the same `disconnect()` path as lock and
|
|
255
|
-
* sign-out. `connect()` already refuses to open a socket while the setting
|
|
256
|
-
* is off.
|
|
257
|
-
*/
|
|
258
|
-
#handleBasicFunctionalityChange() {
|
|
259
|
-
if (!this.#isBasicFunctionality()) {
|
|
260
|
-
this.disconnect();
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
264
|
-
this.connect();
|
|
265
182
|
}
|
|
266
183
|
/**
|
|
267
184
|
* Setup event handling for authentication and wallet lock state
|
|
@@ -302,9 +219,8 @@ export class BackendWebSocketService {
|
|
|
302
219
|
*
|
|
303
220
|
* Connection Requirements (all must be true):
|
|
304
221
|
* 1. Feature enabled (isEnabled() = true)
|
|
305
|
-
* 2.
|
|
306
|
-
* 3.
|
|
307
|
-
* 4. User signed in (checked by getBearerToken)
|
|
222
|
+
* 2. Wallet unlocked (checked by getBearerToken)
|
|
223
|
+
* 3. User signed in (checked by getBearerToken)
|
|
308
224
|
*
|
|
309
225
|
* Platform code should call this when app opens/foregrounds.
|
|
310
226
|
* Automatically called on KeyringController:unlock event.
|
|
@@ -312,28 +228,21 @@ export class BackendWebSocketService {
|
|
|
312
228
|
* @returns Promise that resolves when connection is established
|
|
313
229
|
*/
|
|
314
230
|
async connect() {
|
|
315
|
-
// Priority 1:
|
|
316
|
-
// If
|
|
317
|
-
|
|
318
|
-
|
|
231
|
+
// Priority 1: Check if feature is enabled via callback (feature flag check)
|
|
232
|
+
// If feature is disabled, stop all connection attempts
|
|
233
|
+
if (this.#isEnabled && !this.#isEnabled()) {
|
|
234
|
+
// Clear any pending reconnection attempts since feature is disabled
|
|
235
|
+
this.#clearTimers();
|
|
236
|
+
this.#reconnectAttempts = 0;
|
|
319
237
|
return;
|
|
320
238
|
}
|
|
321
239
|
// If already connected, return immediately
|
|
322
240
|
if (this.#state === WebSocketState.CONNECTED) {
|
|
323
241
|
return;
|
|
324
242
|
}
|
|
325
|
-
// If already connecting, wait for the existing attempt
|
|
326
|
-
// abort resolves that attempt without connecting, so start a new handshake
|
|
327
|
-
// when the setting is back on.
|
|
243
|
+
// If already connecting, wait for the existing connection attempt to complete
|
|
328
244
|
if (this.#connectionPromise) {
|
|
329
245
|
await this.#connectionPromise;
|
|
330
|
-
// The attempt that owns the promise clears it. If Basic Functionality
|
|
331
|
-
// turned off while this call waited, do not open another socket.
|
|
332
|
-
if (!this.#isConnectionAllowed()) {
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
// connect() returns immediately when the awaited attempt reached CONNECTED.
|
|
336
|
-
await this.connect();
|
|
337
246
|
return;
|
|
338
247
|
}
|
|
339
248
|
// If a reconnect is already scheduled, defer to it to avoid bypassing exponential backoff
|
|
@@ -343,7 +252,7 @@ export class BackendWebSocketService {
|
|
|
343
252
|
}
|
|
344
253
|
// Create and store the connection promise IMMEDIATELY (before any async operations)
|
|
345
254
|
// This ensures subsequent connect() calls will wait for this promise instead of creating new connections
|
|
346
|
-
|
|
255
|
+
this.#connectionPromise = (async () => {
|
|
347
256
|
// Priority 2: Check authentication requirements (signed in)
|
|
348
257
|
let bearerToken;
|
|
349
258
|
try {
|
|
@@ -357,16 +266,11 @@ export class BackendWebSocketService {
|
|
|
357
266
|
log('Failed to check authentication requirements', { error });
|
|
358
267
|
throw error;
|
|
359
268
|
}
|
|
360
|
-
// Basic Functionality may have turned off while awaiting the token
|
|
361
|
-
if (this.#stopDisallowedConnection()) {
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
269
|
// Establish the actual WebSocket connection
|
|
365
270
|
await this.#establishConnection(bearerToken);
|
|
366
271
|
})();
|
|
367
|
-
this.#connectionPromise = connectionPromise;
|
|
368
272
|
try {
|
|
369
|
-
await connectionPromise;
|
|
273
|
+
await this.#connectionPromise;
|
|
370
274
|
}
|
|
371
275
|
catch {
|
|
372
276
|
// Always schedule reconnect on any failure
|
|
@@ -374,41 +278,19 @@ export class BackendWebSocketService {
|
|
|
374
278
|
this.#scheduleReconnect();
|
|
375
279
|
}
|
|
376
280
|
finally {
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
if (this.#connectionPromise === connectionPromise) {
|
|
380
|
-
this.#connectionPromise = null;
|
|
381
|
-
}
|
|
281
|
+
// Clear the connection promise when done (success or failure)
|
|
282
|
+
this.#connectionPromise = null;
|
|
382
283
|
}
|
|
383
284
|
}
|
|
384
285
|
/**
|
|
385
|
-
* Closes
|
|
386
|
-
*
|
|
387
|
-
* The timer is cleared before the early return. Lock, sign-out, and Basic
|
|
388
|
-
* Functionality off all call this, including when the socket is already gone
|
|
389
|
-
* and `onclose` will not run. `clearTimeout` has to run first: assigning
|
|
390
|
-
* `#reconnectTimer` to null leaves the callback scheduled. A socket still in
|
|
391
|
-
* CONNECTING is `#pendingWebSocket`, so this closes that too.
|
|
286
|
+
* Closes WebSocket connection
|
|
392
287
|
*/
|
|
393
288
|
disconnect() {
|
|
394
|
-
if (this.#
|
|
395
|
-
clearTimeout(this.#reconnectTimer);
|
|
396
|
-
this.#reconnectTimer = null;
|
|
397
|
-
}
|
|
398
|
-
const socket = this.#ws ?? this.#pendingWebSocket;
|
|
399
|
-
if (!socket || this.#state === WebSocketState.DISCONNECTED) {
|
|
289
|
+
if (this.#state === WebSocketState.DISCONNECTED || !this.#ws) {
|
|
400
290
|
return;
|
|
401
291
|
}
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
if (this.#connectionTimeout) {
|
|
405
|
-
clearTimeout(this.#connectionTimeout);
|
|
406
|
-
this.#connectionTimeout = null;
|
|
407
|
-
}
|
|
408
|
-
// Remember the local abort before close(). An open socket reports code 4999.
|
|
409
|
-
// A CONNECTING socket reports 1006, so onclose uses this flag instead.
|
|
410
|
-
this.#manualClose = true;
|
|
411
|
-
socket.close(MANUAL_DISCONNECT_CODE, MANUAL_DISCONNECT_REASON);
|
|
292
|
+
// Close WebSocket with manual disconnect code and reason
|
|
293
|
+
this.#ws.close(MANUAL_DISCONNECT_CODE, MANUAL_DISCONNECT_REASON);
|
|
412
294
|
log('WebSocket manually disconnected');
|
|
413
295
|
}
|
|
414
296
|
/**
|
|
@@ -677,8 +559,6 @@ export class BackendWebSocketService {
|
|
|
677
559
|
* Called when service is being destroyed or app is terminating
|
|
678
560
|
*/
|
|
679
561
|
destroy() {
|
|
680
|
-
this.#unsubscribeBasicFunctionality?.();
|
|
681
|
-
this.#unsubscribeBasicFunctionality = undefined;
|
|
682
562
|
// Always clear timers first to prevent reconnection attempts after destruction
|
|
683
563
|
// This handles the case where destroy() is called while DISCONNECTED with a pending reconnect timer
|
|
684
564
|
this.#clearTimers();
|
|
@@ -817,7 +697,6 @@ export class BackendWebSocketService {
|
|
|
817
697
|
return new Promise((resolve, reject) => {
|
|
818
698
|
// eslint-disable-next-line no-restricted-globals
|
|
819
699
|
const ws = new WebSocket(wsUrl);
|
|
820
|
-
this.#pendingWebSocket = ws;
|
|
821
700
|
this.#connectionTimeout = setTimeout(() => {
|
|
822
701
|
log('WebSocket connection timeout - forcing close', {
|
|
823
702
|
timeout: this.#options.timeout,
|
|
@@ -830,18 +709,9 @@ export class BackendWebSocketService {
|
|
|
830
709
|
clearTimeout(this.#connectionTimeout);
|
|
831
710
|
this.#connectionTimeout = null;
|
|
832
711
|
}
|
|
833
|
-
if (this.#pendingWebSocket === ws) {
|
|
834
|
-
this.#pendingWebSocket = undefined;
|
|
835
|
-
}
|
|
836
712
|
this.#ws = ws;
|
|
837
713
|
this.#setState(WebSocketState.CONNECTED);
|
|
838
714
|
this.#connectedAt = Date.now();
|
|
839
|
-
// Basic Functionality may have turned off during the handshake
|
|
840
|
-
if (!this.#isBasicFunctionality()) {
|
|
841
|
-
this.disconnect();
|
|
842
|
-
resolve();
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
715
|
// Only reset after connection stays stable for a period (10 seconds)
|
|
846
716
|
// This prevents rapid reconnect loops when server accepts then immediately closes
|
|
847
717
|
this.#stableConnectionTimer = setTimeout(() => {
|
|
@@ -859,18 +729,13 @@ export class BackendWebSocketService {
|
|
|
859
729
|
reason: event.reason || getCloseReason(event.code),
|
|
860
730
|
wasClean: event.wasClean,
|
|
861
731
|
});
|
|
862
|
-
// A CONNECTING close() arrives as 1006. The flag records that
|
|
863
|
-
// disconnect() requested it, including when this event is a duplicate.
|
|
864
|
-
const closedByDisconnect = this.#manualClose;
|
|
865
|
-
this.#manualClose = false;
|
|
866
732
|
// Guard against duplicate close events
|
|
867
733
|
if (this.#state === WebSocketState.DISCONNECTED) {
|
|
868
734
|
return;
|
|
869
735
|
}
|
|
870
|
-
//
|
|
871
|
-
const isManualDisconnect =
|
|
872
|
-
|
|
873
|
-
event.reason === MANUAL_DISCONNECT_REASON);
|
|
736
|
+
// Detect if this is a manual disconnect or service cleanup based on close code
|
|
737
|
+
const isManualDisconnect = event.code === MANUAL_DISCONNECT_CODE &&
|
|
738
|
+
event.reason === MANUAL_DISCONNECT_REASON;
|
|
874
739
|
// If connection hasn't been established yet, handle the connection promise
|
|
875
740
|
if (this.#state === WebSocketState.CONNECTING) {
|
|
876
741
|
if (isManualDisconnect) {
|
|
@@ -887,9 +752,6 @@ export class BackendWebSocketService {
|
|
|
887
752
|
// Clear all timers
|
|
888
753
|
this.#clearTimers();
|
|
889
754
|
// Clear WebSocket reference to allow garbage collection
|
|
890
|
-
if (this.#pendingWebSocket === ws) {
|
|
891
|
-
this.#pendingWebSocket = undefined;
|
|
892
|
-
}
|
|
893
755
|
this.#ws = undefined;
|
|
894
756
|
// Clear connection tracking
|
|
895
757
|
this.#connectionPromise = null;
|
|
@@ -1118,12 +980,15 @@ export class BackendWebSocketService {
|
|
|
1118
980
|
delay_ms: delay,
|
|
1119
981
|
});
|
|
1120
982
|
this.#reconnectTimer = setTimeout(() => {
|
|
1121
|
-
//
|
|
1122
|
-
|
|
1123
|
-
if
|
|
983
|
+
// Clear timer reference first
|
|
984
|
+
this.#reconnectTimer = null;
|
|
985
|
+
// Check if connection is still enabled before reconnecting
|
|
986
|
+
if (this.#isEnabled && !this.#isEnabled()) {
|
|
987
|
+
this.#reconnectAttempts = 0;
|
|
988
|
+
// Create new backoff sequence when disabled
|
|
989
|
+
this.#newBackoff();
|
|
1124
990
|
return;
|
|
1125
991
|
}
|
|
1126
|
-
this.#reconnectTimer = null;
|
|
1127
992
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
1128
993
|
this.connect();
|
|
1129
994
|
}, delay);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BackendWebSocketService.js","sourceRoot":"","sources":["../../src/ws/BackendWebSocketService.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TraceCallback } from '@metamask/controller-utils';\nimport { ExponentialBackoff } from '@metamask/controller-utils';\nimport type {\n KeyringControllerLockEvent,\n KeyringControllerUnlockEvent,\n} from '@metamask/keyring-controller';\nimport type { Messenger } from '@metamask/messenger';\nimport type { AuthenticationController } from '@metamask/profile-sync-controller';\nimport { getErrorMessage } from '@metamask/utils';\nimport { v4 as uuidV4 } from 'uuid';\n\nimport { projectLogger, createModuleLogger } from '../logger.js';\nimport type { BackendWebSocketServiceMethodActions } from './BackendWebSocketService-method-action-types.js';\n\nconst SERVICE_NAME = 'BackendWebSocketService' as const;\n\nconst log = createModuleLogger(projectLogger, SERVICE_NAME);\n\n// WebSocket close codes and reasons for internal operations\nconst MANUAL_DISCONNECT_CODE = 4999 as const;\nconst MANUAL_DISCONNECT_REASON = 'Internal: Manual disconnect' as const;\nconst FORCE_RECONNECT_CODE = 4998 as const;\nconst FORCE_RECONNECT_REASON = 'Internal: Force reconnect' as const;\n\nconst MESSENGER_EXPOSED_METHODS = [\n 'connect',\n 'disconnect',\n 'forceReconnection',\n 'sendMessage',\n 'sendRequest',\n 'subscribe',\n 'getConnectionInfo',\n 'getSubscriptionsByChannel',\n 'channelHasSubscription',\n 'findSubscriptionsByChannelPrefix',\n 'addChannelCallback',\n 'removeChannelCallback',\n 'getChannelCallbacks',\n] as const;\n\n/**\n * Gets human-readable close reason from RFC 6455 close code\n *\n * @param code - WebSocket close code\n * @returns Human-readable close reason\n */\nexport function getCloseReason(code: number): string {\n switch (code) {\n case 1000:\n return 'Normal Closure';\n case 1001:\n return 'Going Away';\n case 1002:\n return 'Protocol Error';\n case 1003:\n return 'Unsupported Data';\n case 1004:\n return 'Reserved';\n case 1005:\n return 'No Status Received';\n case 1006:\n return 'Abnormal Closure';\n case 1007:\n return 'Invalid frame payload data';\n case 1008:\n return 'Policy Violation';\n case 1009:\n return 'Message Too Big';\n case 1010:\n return 'Mandatory Extension';\n case 1011:\n return 'Internal Server Error';\n case 1012:\n return 'Service Restart';\n case 1013:\n return 'Try Again Later';\n case 1014:\n return 'Bad Gateway';\n case 1015:\n return 'TLS Handshake';\n default:\n if (code >= 3000 && code <= 3999) {\n return 'Library/Framework Error';\n }\n if (code >= 4000 && code <= 4999) {\n return 'Application Error';\n }\n return 'Unknown';\n }\n}\n\n/**\n * WebSocket connection states\n */\nexport enum WebSocketState {\n CONNECTING = 'connecting',\n CONNECTED = 'connected',\n /** @deprecated This value is no longer used internally and will be removed in a future major release */\n DISCONNECTING = 'disconnecting',\n DISCONNECTED = 'disconnected',\n /** @deprecated TThis value is no longer used internally and will be removed in a future major release */\n ERROR = 'error',\n}\n\n/**\n * WebSocket event types\n */\nexport enum WebSocketEventType {\n CONNECTED = 'connected',\n DISCONNECTED = 'disconnected',\n MESSAGE = 'message',\n ERROR = 'error',\n RECONNECTING = 'reconnecting',\n RECONNECTED = 'reconnected',\n}\n\n/**\n * Configuration options for the WebSocket service\n */\nexport type BackendWebSocketServiceOptions = {\n /** The WebSocket URL to connect to */\n url: string;\n\n /** The messenger for inter-service communication */\n messenger: BackendWebSocketServiceMessenger;\n\n /** Connection timeout in milliseconds (default: 10000) */\n timeout?: number;\n\n /** Initial reconnection delay in milliseconds (default: 10000) */\n reconnectDelay?: number;\n\n /** Maximum reconnection delay in milliseconds (default: 60000) */\n maxReconnectDelay?: number;\n\n /** Request timeout in milliseconds (default: 30000) */\n requestTimeout?: number;\n\n /** Optional callback to determine if connection should be enabled (default: always enabled) */\n isEnabled?: () => boolean;\n\n /**\n * Getter for basic functionality (matches the \"Basic functionality\" setting).\n * When it returns true, the backend WebSocket may connect. When false, the\n * service does not connect or reconnect, and an open socket is closed.\n * Defaults to () => true when omitted.\n */\n isBasicFunctionality?: () => boolean;\n\n /**\n * Called by the service with an onChange callback. The consumer subscribes to\n * its basic-functionality source and invokes onChange when the value changes.\n * The service re-reads `isBasicFunctionality()` and connects or disconnects.\n * May return an unsubscribe function, which is called on destroy. Optional.\n */\n subscribeToBasicFunctionalityChange?: (\n onChange: () => void,\n ) => void | (() => void);\n\n /** Optional callback to trace performance of WebSocket operations (default: no-op) */\n traceFn?: TraceCallback;\n};\n\n/**\n * Client Request message\n * Used when client sends a request to the server\n */\nexport type ClientRequestMessage = {\n event: string;\n data: {\n requestId: string;\n channels?: string[];\n [key: string]: unknown;\n };\n};\n\n/**\n * Server Response message\n * Used when server responds to a client request\n */\nexport type ServerResponseMessage = {\n event: string;\n data: {\n requestId: string;\n subscriptionId?: string;\n succeeded?: string[];\n failed?: string[];\n [key: string]: unknown;\n };\n};\n\n/**\n * Server Notification message\n * Used when server sends unsolicited data to client\n * subscriptionId is optional for system-wide notifications\n */\nexport type ServerNotificationMessage = {\n event: string;\n subscriptionId?: string;\n channel: string;\n data: Record<string, unknown>;\n timestamp: number;\n};\n\n/**\n * Union type for all WebSocket messages\n */\nexport type WebSocketMessage =\n | ClientRequestMessage\n | ServerResponseMessage\n | ServerNotificationMessage;\n\n/**\n * Channel-based callback configuration\n */\nexport type ChannelCallback = {\n /** Channel name to match (also serves as the unique identifier) */\n channelName: string;\n /** Callback function */\n callback: (notification: ServerNotificationMessage) => void;\n};\n\n/**\n * Unified WebSocket subscription object used for both internal storage and external API\n */\nexport type WebSocketSubscription = {\n /** The subscription ID from the server */\n subscriptionId: string;\n /** Channel names for this subscription */\n channels: string[];\n /** Channel type with version (e.g., 'account-activity.v1') extracted from first channel */\n channelType: string;\n /** Callback function for handling notifications (optional for external use) */\n callback?: (notification: ServerNotificationMessage) => void;\n /** Function to unsubscribe and clean up */\n unsubscribe: (requestId?: string) => Promise<void>;\n};\n\n/**\n * WebSocket connection info\n */\nexport type WebSocketConnectionInfo = {\n state: WebSocketState;\n url: string;\n reconnectAttempts: number;\n timeout: number;\n reconnectDelay: number;\n maxReconnectDelay: number;\n requestTimeout: number;\n connectedAt?: number;\n};\n\n// Action types for the messaging system - using generated method actions\nexport type BackendWebSocketServiceActions =\n BackendWebSocketServiceMethodActions;\n\ntype AllowedActions =\n AuthenticationController.AuthenticationControllerGetBearerTokenAction;\n\n// Event types for WebSocket connection state changes\nexport type BackendWebSocketServiceConnectionStateChangedEvent = {\n type: 'BackendWebSocketService:connectionStateChanged';\n payload: [WebSocketConnectionInfo];\n};\n\ntype AllowedEvents =\n | AuthenticationController.AuthenticationControllerStateChangeEvent\n | KeyringControllerLockEvent\n | KeyringControllerUnlockEvent;\n\nexport type BackendWebSocketServiceEvents =\n BackendWebSocketServiceConnectionStateChangedEvent;\n\nexport type BackendWebSocketServiceMessenger = Messenger<\n typeof SERVICE_NAME,\n BackendWebSocketServiceActions | AllowedActions,\n BackendWebSocketServiceEvents | AllowedEvents\n>;\n\n/**\n * WebSocket Service with automatic reconnection, session management and direct callback routing\n *\n * Connection Management:\n * - Automatically subscribes to AuthenticationController:stateChange (sign in/out)\n * - Automatically subscribes to KeyringController:lock/unlock events\n * - Idempotent connect() function safe for multiple rapid calls\n * - Auto-reconnects on unexpected disconnects (manualDisconnect = false)\n *\n * Platform Responsibilities:\n * - Call connect() when app opens/foregrounds\n * - Call disconnect() when app closes/backgrounds\n * - Provide isEnabled() callback (feature flag)\n * - Provide isBasicFunctionality() when Basic Functionality can be turned off\n * - Call destroy() on app termination\n *\n * Real-Time Performance Optimizations:\n * - Fast path message routing (zero allocations)\n * - Production mode removes try-catch overhead\n * - Optimized JSON parsing with fail-fast\n * - Direct callback routing bypasses event emitters\n * - Memory cleanup and resource management\n */\nexport class BackendWebSocketService {\n /**\n * The name of the service.\n */\n readonly name = SERVICE_NAME;\n\n readonly #messenger: BackendWebSocketServiceMessenger;\n\n readonly #options: Required<\n Pick<\n BackendWebSocketServiceOptions,\n | 'url'\n | 'timeout'\n | 'reconnectDelay'\n | 'maxReconnectDelay'\n | 'requestTimeout'\n >\n >;\n\n readonly #isEnabled: (() => boolean) | undefined;\n\n readonly #isBasicFunctionality: () => boolean;\n\n #unsubscribeBasicFunctionality: (() => void) | undefined;\n\n readonly #trace: TraceCallback;\n\n #ws: WebSocket | undefined;\n\n /**\n * Socket created for the current handshake, before `onopen` assigns `#ws`.\n * `disconnect()` closes this so Basic Functionality can abort CONNECTING.\n */\n #pendingWebSocket: WebSocket | undefined;\n\n /**\n * True after `disconnect()` asks the socket to close. A handshake still in\n * CONNECTING cannot carry the manual close code: browsers report 1006 and an\n * empty reason instead.\n */\n #manualClose = false;\n\n #state: WebSocketState = WebSocketState.DISCONNECTED;\n\n #reconnectAttempts = 0;\n\n #reconnectTimer: NodeJS.Timeout | null = null;\n\n #connectionTimeout: NodeJS.Timeout | null = null;\n\n #stableConnectionTimer: NodeJS.Timeout | null = null;\n\n // Track the current connection promise to handle concurrent connection attempts\n #connectionPromise: Promise<void> | null = null;\n\n readonly #pendingRequests = new Map<\n string,\n {\n resolve: (value: unknown) => void;\n reject: (error: Error) => void;\n timeout: NodeJS.Timeout;\n }\n >();\n\n #connectedAt: number = 0;\n\n // Simplified subscription storage (single flat map)\n // Key: subscription ID string (e.g., 'sub_abc123def456')\n // Value: WebSocketSubscription object with channels, callback and metadata\n readonly #subscriptions = new Map<string, WebSocketSubscription>();\n\n // Channel-based callback storage\n // Key: channel name (serves as unique identifier)\n // Value: ChannelCallback configuration\n readonly #channelCallbacks = new Map<string, ChannelCallback>();\n\n // Backoff instance for reconnection delays (reset on stable connection)\n #backoff!: ReturnType<ExponentialBackoff<unknown>['next']>;\n\n // =============================================================================\n // 1. CONSTRUCTOR & INITIALIZATION\n // =============================================================================\n\n /**\n * Creates a new WebSocket service instance\n *\n * @param options - Configuration options for the WebSocket service\n */\n constructor(options: BackendWebSocketServiceOptions) {\n this.#messenger = options.messenger;\n this.#isEnabled = options.isEnabled;\n this.#isBasicFunctionality =\n options.isBasicFunctionality ?? ((): boolean => true);\n // Default to no-op trace function to keep core platform-agnostic\n this.#trace =\n options.traceFn ??\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (((_request: any, fn?: any) => fn?.()) as TraceCallback);\n\n this.#options = {\n url: options.url,\n timeout: options.timeout ?? 10000,\n reconnectDelay: options.reconnectDelay ?? 10000,\n maxReconnectDelay: options.maxReconnectDelay ?? 60000,\n requestTimeout: options.requestTimeout ?? 30000,\n };\n\n // Initialize backoff for reconnection delays\n this.#newBackoff();\n\n // Subscribe to authentication and keyring controller events\n this.#subscribeEvents();\n\n // Register action handlers using the method actions pattern\n this.#messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n\n if (options.subscribeToBasicFunctionalityChange) {\n const unsubscribe = options.subscribeToBasicFunctionalityChange(() => {\n this.#handleBasicFunctionalityChange();\n });\n if (typeof unsubscribe === 'function') {\n this.#unsubscribeBasicFunctionality = unsubscribe;\n }\n }\n }\n\n /**\n * Whether a connection attempt is allowed.\n *\n * Both gates are required when provided: the product feature flag and the\n * user's Basic Functionality setting.\n *\n * @returns True when the socket may connect.\n */\n #isConnectionAllowed(): boolean {\n if (this.#isEnabled && !this.#isEnabled()) {\n return false;\n }\n return this.#isBasicFunctionality();\n }\n\n /**\n * Drop a queued reconnect and start the backoff over.\n * Does not close an open or in-flight socket.\n */\n #clearReconnectSchedule(): void {\n if (this.#reconnectTimer) {\n clearTimeout(this.#reconnectTimer);\n this.#reconnectTimer = null;\n }\n this.#reconnectAttempts = 0;\n this.#newBackoff();\n }\n\n /**\n * Stop a connection that the gates no longer allow.\n *\n * Basic Functionality closes the socket. A disabled feature flag only\n * cancels a queued reconnect and leaves an open socket in place.\n *\n * @returns True when the caller should not open a socket.\n */\n #stopDisallowedConnection(): boolean {\n if (this.#isConnectionAllowed()) {\n return false;\n }\n this.#clearReconnectSchedule();\n if (!this.#isBasicFunctionality()) {\n this.disconnect();\n }\n return true;\n }\n\n /**\n * Apply a Basic Functionality change. The getter is the source of truth.\n *\n * Turning the setting off uses the same `disconnect()` path as lock and\n * sign-out. `connect()` already refuses to open a socket while the setting\n * is off.\n */\n #handleBasicFunctionalityChange(): void {\n if (!this.#isBasicFunctionality()) {\n this.disconnect();\n return;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.connect();\n }\n\n /**\n * Setup event handling for authentication and wallet lock state\n *\n * Three event sources trigger connection/disconnection:\n * 1. AuthenticationController:stateChange (sign in/out)\n * 2. KeyringController:unlock (wallet unlocked)\n * 3. KeyringController:lock (wallet locked)\n *\n * All connect() calls are idempotent and validate all requirements.\n */\n #subscribeEvents(): void {\n // Subscribe to authentication state changes (sign in/out)\n this.#messenger.subscribe(\n 'AuthenticationController:stateChange',\n (state: AuthenticationController.AuthenticationControllerState) => {\n if (state.isSignedIn) {\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.connect();\n } else {\n this.disconnect();\n }\n },\n (state) => ({ isSignedIn: state.isSignedIn }),\n );\n\n // Subscribe to wallet unlock event\n this.#messenger.subscribe('KeyringController:unlock', () => {\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.connect();\n });\n\n // Subscribe to wallet lock event\n this.#messenger.subscribe('KeyringController:lock', () => {\n this.disconnect();\n });\n }\n\n // =============================================================================\n // 2. PUBLIC API METHODS\n // =============================================================================\n\n /**\n * Establishes WebSocket connection with smart reconnection behavior\n *\n * Connection Requirements (all must be true):\n * 1. Feature enabled (isEnabled() = true)\n * 2. Basic functionality enabled (isBasicFunctionality() = true)\n * 3. Wallet unlocked (checked by getBearerToken)\n * 4. User signed in (checked by getBearerToken)\n *\n * Platform code should call this when app opens/foregrounds.\n * Automatically called on KeyringController:unlock event.\n *\n * @returns Promise that resolves when connection is established\n */\n async connect(): Promise<void> {\n // Priority 1: Feature flag and Basic Functionality must both allow the socket.\n // If either gate is closed, stop attempts. Only Basic Functionality also\n // drops an open connection; a disabled feature flag keeps it as before.\n if (this.#stopDisallowedConnection()) {\n return;\n }\n\n // If already connected, return immediately\n if (this.#state === WebSocketState.CONNECTED) {\n return;\n }\n\n // If already connecting, wait for the existing attempt. A Basic Functionality\n // abort resolves that attempt without connecting, so start a new handshake\n // when the setting is back on.\n if (this.#connectionPromise) {\n await this.#connectionPromise;\n // The attempt that owns the promise clears it. If Basic Functionality\n // turned off while this call waited, do not open another socket.\n if (!this.#isConnectionAllowed()) {\n return;\n }\n // connect() returns immediately when the awaited attempt reached CONNECTED.\n await this.connect();\n return;\n }\n\n // If a reconnect is already scheduled, defer to it to avoid bypassing exponential backoff\n // This prevents rapid loops when server accepts then immediately closes connections\n if (this.#reconnectTimer) {\n return;\n }\n\n // Create and store the connection promise IMMEDIATELY (before any async operations)\n // This ensures subsequent connect() calls will wait for this promise instead of creating new connections\n const connectionPromise = (async (): Promise<void> => {\n // Priority 2: Check authentication requirements (signed in)\n let bearerToken: string;\n try {\n const token = await this.#messenger.call(\n 'AuthenticationController:getBearerToken',\n );\n if (!token) {\n throw new Error('Authentication required: user not signed in');\n }\n bearerToken = token;\n } catch (error) {\n log('Failed to check authentication requirements', { error });\n throw error;\n }\n\n // Basic Functionality may have turned off while awaiting the token\n if (this.#stopDisallowedConnection()) {\n return;\n }\n\n // Establish the actual WebSocket connection\n await this.#establishConnection(bearerToken);\n })();\n this.#connectionPromise = connectionPromise;\n\n try {\n await connectionPromise;\n } catch {\n // Always schedule reconnect on any failure\n // Exponential backoff will prevent aggressive retries\n this.#scheduleReconnect();\n } finally {\n // Only clear the attempt this call owns. onclose nulls the field before\n // this runs, and a newer connect() may already have stored its promise.\n if (this.#connectionPromise === connectionPromise) {\n this.#connectionPromise = null;\n }\n }\n }\n\n /**\n * Closes the WebSocket and cancels a queued reconnect.\n *\n * The timer is cleared before the early return. Lock, sign-out, and Basic\n * Functionality off all call this, including when the socket is already gone\n * and `onclose` will not run. `clearTimeout` has to run first: assigning\n * `#reconnectTimer` to null leaves the callback scheduled. A socket still in\n * CONNECTING is `#pendingWebSocket`, so this closes that too.\n */\n disconnect(): void {\n if (this.#reconnectTimer) {\n clearTimeout(this.#reconnectTimer);\n this.#reconnectTimer = null;\n }\n\n const socket = this.#ws ?? this.#pendingWebSocket;\n if (!socket || this.#state === WebSocketState.DISCONNECTED) {\n return;\n }\n\n // This close replaces the handshake timeout. A timeout close would not\n // set the manual-close flag.\n if (this.#connectionTimeout) {\n clearTimeout(this.#connectionTimeout);\n this.#connectionTimeout = null;\n }\n\n // Remember the local abort before close(). An open socket reports code 4999.\n // A CONNECTING socket reports 1006, so onclose uses this flag instead.\n this.#manualClose = true;\n socket.close(MANUAL_DISCONNECT_CODE, MANUAL_DISCONNECT_REASON);\n\n log('WebSocket manually disconnected');\n }\n\n /**\n * Forces a WebSocket reconnection to clean up subscription state\n *\n * This method is useful when subscription state may be out of sync and needs to be reset.\n * It performs a controlled disconnect-then-reconnect sequence:\n * - Disconnects cleanly to trigger subscription cleanup\n * - Schedules reconnection with exponential backoff to prevent rapid loops\n * - All subscriptions will be cleaned up automatically on disconnect\n *\n * Use cases:\n * - Recovering from subscription/unsubscription issues\n * - Cleaning up orphaned subscriptions\n * - Forcing a fresh subscription state\n *\n * @returns Promise that resolves when disconnection is complete (reconnection is scheduled)\n */\n async forceReconnection(): Promise<void> {\n if (this.#state === WebSocketState.DISCONNECTED || !this.#ws) {\n log('WebSocket already disconnected, scheduling reconnect');\n this.#scheduleReconnect();\n return;\n }\n\n log('Forcing WebSocket reconnection to clean up subscription state');\n\n // This ensures ws.onclose will schedule a reconnect (not treat it as manual disconnect)\n this.#ws.close(FORCE_RECONNECT_CODE, FORCE_RECONNECT_REASON);\n }\n\n /**\n * Sends a message through the WebSocket (fire-and-forget, no response expected)\n *\n * This is a low-level method for sending messages without waiting for a response.\n * Most consumers should use `sendRequest()` instead, which handles request-response\n * correlation and provides proper error handling with timeouts.\n *\n * Use this method only when:\n * - You don't need a response from the server\n * - You're implementing custom message protocols\n * - You need fine-grained control over message timing\n *\n * @param message - The message to send\n * @throws Error if WebSocket is not connected or send fails\n *\n * @see sendRequest for request-response pattern with automatic correlation\n */\n sendMessage(message: ClientRequestMessage): void {\n if (this.#state !== WebSocketState.CONNECTED || !this.#ws) {\n throw new Error(`Cannot send message: WebSocket is ${this.#state}`);\n }\n\n try {\n this.#ws.send(JSON.stringify(message));\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n this.#handleError(new Error(errorMessage));\n throw new Error(errorMessage);\n }\n }\n\n /**\n * Sends a request and waits for a correlated response (recommended for most use cases)\n *\n * This is the recommended high-level method for request-response communication.\n * It automatically handles:\n * - Request ID generation and correlation\n * - Response matching with timeout protection\n * - Automatic reconnection on timeout\n * - Proper cleanup of pending requests\n *\n * @param message - The request message (can include optional requestId for testing)\n * @returns Promise that resolves with the response data\n * @throws Error if WebSocket is not connected, request times out, or response indicates failure\n *\n * @see sendMessage for fire-and-forget messaging without response handling\n */\n async sendRequest<Type = ServerResponseMessage['data']>(\n message: Omit<ClientRequestMessage, 'data'> & {\n data?: Omit<ClientRequestMessage['data'], 'requestId'> & {\n requestId?: string;\n };\n },\n ): Promise<Type> {\n if (this.#state !== WebSocketState.CONNECTED) {\n throw new Error(`Cannot send request: WebSocket is ${this.#state}`);\n }\n\n // Use provided requestId if available, otherwise generate a new one\n const requestId = message.data?.requestId ?? uuidV4();\n const requestMessage: ClientRequestMessage = {\n event: message.event,\n data: {\n ...message.data,\n requestId, // Set after spread to ensure it's not overwritten by undefined\n },\n };\n\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n this.#pendingRequests.delete(requestId);\n log('Request timeout - triggering reconnection', {\n timeout: this.#options.requestTimeout,\n });\n\n // Trigger reconnection on request timeout as it may indicate stale connection\n if (this.#state === WebSocketState.CONNECTED && this.#ws) {\n // Force close the current connection to trigger reconnection logic\n this.#ws.close(3000, 'Request timeout - forcing reconnect');\n }\n\n reject(\n new Error(`Request timeout after ${this.#options.requestTimeout}ms`),\n );\n }, this.#options.requestTimeout);\n\n // Store in pending requests for response correlation\n this.#pendingRequests.set(requestId, {\n resolve: resolve as (value: unknown) => void,\n reject,\n timeout,\n });\n\n // Send the request\n try {\n this.sendMessage(requestMessage);\n } catch (error) {\n this.#pendingRequests.delete(requestId);\n clearTimeout(timeout);\n reject(error instanceof Error ? error : new Error(String(error)));\n }\n });\n }\n\n /**\n * Gets current connection information\n *\n * @returns Current connection status and details\n */\n getConnectionInfo(): WebSocketConnectionInfo {\n return {\n state: this.#state,\n url: this.#options.url,\n timeout: this.#options.timeout,\n reconnectDelay: this.#options.reconnectDelay,\n maxReconnectDelay: this.#options.maxReconnectDelay,\n requestTimeout: this.#options.requestTimeout,\n reconnectAttempts: this.#reconnectAttempts,\n connectedAt: this.#connectedAt,\n };\n }\n\n /**\n * Gets all subscription information for a specific channel\n *\n * @param channel - The channel name to look up\n * @returns Array of subscription details for all subscriptions containing the channel\n */\n getSubscriptionsByChannel(channel: string): WebSocketSubscription[] {\n const matchingSubscriptions: WebSocketSubscription[] = [];\n for (const [subscriptionId, subscription] of this.#subscriptions) {\n if (subscription.channels.includes(channel)) {\n matchingSubscriptions.push({\n subscriptionId,\n channels: subscription.channels,\n channelType: subscription.channelType,\n unsubscribe: subscription.unsubscribe,\n });\n }\n }\n return matchingSubscriptions;\n }\n\n /**\n * Checks if a channel has a subscription\n *\n * @param channel - The channel name to check\n * @returns True if the channel has a subscription, false otherwise\n */\n channelHasSubscription(channel: string): boolean {\n for (const subscription of this.#subscriptions.values()) {\n if (subscription.channels.includes(channel)) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Finds all subscriptions that have channels starting with the specified prefix\n *\n * @param channelPrefix - The channel prefix to search for (e.g., \"account-activity.v1\")\n * @returns Array of subscription info for matching subscriptions\n */\n findSubscriptionsByChannelPrefix(\n channelPrefix: string,\n ): WebSocketSubscription[] {\n const matchingSubscriptions: WebSocketSubscription[] = [];\n\n for (const [subscriptionId, subscription] of this.#subscriptions) {\n // Check if any channel in this subscription starts with the prefix\n const hasMatchingChannel = subscription.channels.some((channel) =>\n channel.startsWith(channelPrefix),\n );\n\n if (hasMatchingChannel) {\n matchingSubscriptions.push({\n subscriptionId,\n channels: subscription.channels,\n channelType: subscription.channelType,\n unsubscribe: subscription.unsubscribe,\n });\n }\n }\n\n return matchingSubscriptions;\n }\n\n /**\n * Register a callback for specific channels (local callback only, no server subscription)\n *\n * **Key Difference from `subscribe()`:**\n * - `addChannelCallback()`: Registers a local callback without creating a server-side subscription.\n * The callback triggers on ANY message matching the channel name, regardless of subscriptionId.\n * Useful for system-wide notifications or when you don't control the subscription lifecycle.\n *\n * - `subscribe()`: Creates a proper server-side subscription with a subscriptionId.\n * The callback only triggers for messages with the matching subscriptionId.\n * Includes proper lifecycle management (unsubscribe, automatic cleanup on disconnect).\n *\n * **When to use `addChannelCallback()`:**\n * - Listening to system-wide notifications (e.g., 'system-notifications.v1')\n * - Monitoring channels where subscriptions are managed elsewhere\n * - Debug/logging scenarios where you want to observe all channel messages\n *\n * **When to use `subscribe()` instead:**\n * - Creating new subscriptions that need server-side registration\n * - When you need proper cleanup via unsubscribe\n * - Most application use cases (recommended approach)\n *\n * @param options - Channel callback configuration\n * @param options.channelName - Channel name to match exactly\n * @param options.callback - Function to call when channel matches\n *\n * @example\n * ```typescript\n * // Listen to system notifications (no server subscription needed)\n * webSocketService.addChannelCallback({\n * channelName: 'system-notifications.v1',\n * callback: (notification) => {\n * console.log('System notification:', notification.data);\n * }\n * });\n *\n * // For account-specific subscriptions, use subscribe() instead:\n * // const sub = await webSocketService.subscribe({\n * // channels: ['account-activity.v1.eip155:0:0x1234...'],\n * // callback: (notification) => { ... }\n * // });\n * ```\n *\n * @see subscribe for creating proper server-side subscriptions with lifecycle management\n */\n addChannelCallback(options: {\n channelName: string;\n callback: (notification: ServerNotificationMessage) => void;\n }): void {\n const channelCallback: ChannelCallback = {\n channelName: options.channelName,\n callback: options.callback,\n };\n\n // Check if callback already exists for this channel\n if (this.#channelCallbacks.has(options.channelName)) {\n return;\n }\n\n this.#channelCallbacks.set(options.channelName, channelCallback);\n }\n\n /**\n * Remove a channel callback\n *\n * @param channelName - The channel name returned from addChannelCallback\n * @returns True if callback was found and removed, false otherwise\n */\n removeChannelCallback(channelName: string): boolean {\n return this.#channelCallbacks.delete(channelName);\n }\n\n /**\n * Get all registered channel callbacks (for debugging)\n *\n * @returns Array of all registered channel callbacks\n */\n getChannelCallbacks(): ChannelCallback[] {\n return Array.from(this.#channelCallbacks.values());\n }\n\n /**\n * Destroy the service and clean up resources\n * Called when service is being destroyed or app is terminating\n */\n destroy(): void {\n this.#unsubscribeBasicFunctionality?.();\n this.#unsubscribeBasicFunctionality = undefined;\n\n // Always clear timers first to prevent reconnection attempts after destruction\n // This handles the case where destroy() is called while DISCONNECTED with a pending reconnect timer\n this.#clearTimers();\n\n // Reset reconnect attempts to prevent any future reconnection logic\n this.#reconnectAttempts = 0;\n\n // Disconnect the WebSocket if connected (will be no-op if already disconnected)\n this.disconnect();\n }\n\n /**\n * Create and manage a subscription with server-side registration (recommended for most use cases)\n *\n * This is the recommended subscription API for high-level services. It creates a proper\n * server-side subscription and routes notifications based on subscriptionId.\n *\n * **Key Features:**\n * - Creates server-side subscription with unique subscriptionId\n * - Callback triggered only for messages with matching subscriptionId\n * - Automatic lifecycle management (cleanup on disconnect)\n * - Includes unsubscribe method for proper cleanup\n * - Request-response pattern with error handling\n *\n * **When to use `subscribe()`:**\n * - Creating new subscriptions (account activity, price updates, etc.)\n * - When you need proper cleanup/unsubscribe functionality\n * - Most application use cases\n *\n * **When to use `addChannelCallback()` instead:**\n * - System-wide notifications without server-side subscription\n * - Observing channels managed elsewhere\n * - Debug/logging scenarios\n *\n * @param options - Subscription configuration\n * @param options.channels - Array of channel names to subscribe to\n * @param options.callback - Callback function for handling notifications\n * @param options.requestId - Optional request ID for testing (will generate UUID if not provided)\n * @param options.channelType - Channel type identifier\n * @returns Subscription object with unsubscribe method\n *\n * @example\n * ```typescript\n * // AccountActivityService usage\n * const subscription = await webSocketService.subscribe({\n * channels: ['account-activity.v1.eip155:0:0x1234...'],\n * callback: (notification) => {\n * this.handleAccountActivity(notification.data);\n * }\n * });\n *\n * // Later, clean up\n * await subscription.unsubscribe();\n * ```\n *\n * @see addChannelCallback for local callbacks without server-side subscription\n */\n async subscribe(options: {\n /** Channel names to subscribe to */\n channels: string[];\n /** Channel type with version (e.g., 'account-activity.v1') for tracing and monitoring */\n channelType: string;\n /** Handler for incoming notifications */\n callback: (notification: ServerNotificationMessage) => void;\n /** Optional request ID for testing (will generate UUID if not provided) */\n requestId?: string;\n }): Promise<WebSocketSubscription> {\n const { channels, channelType, callback, requestId } = options;\n\n if (this.#state !== WebSocketState.CONNECTED) {\n throw new Error(\n `Cannot create subscription(s) ${channels.join(', ')}: WebSocket is ${this.#state}`,\n );\n }\n\n // Send subscription request and wait for response\n const subscriptionResponse = await this.sendRequest({\n event: 'subscribe',\n data: { channels, requestId },\n });\n\n if (!subscriptionResponse?.subscriptionId) {\n throw new Error('Invalid subscription response: missing subscription ID');\n }\n\n const { subscriptionId } = subscriptionResponse;\n\n // Create unsubscribe function\n const unsubscribe = async (unsubRequestId?: string): Promise<void> => {\n // Send unsubscribe request first\n await this.sendRequest({\n event: 'unsubscribe',\n data: {\n subscription: subscriptionId,\n channels,\n requestId: unsubRequestId,\n },\n });\n\n // Clean up subscription mapping\n this.#subscriptions.delete(subscriptionId);\n };\n\n const subscription = {\n subscriptionId,\n channels: [...channels],\n channelType,\n unsubscribe,\n };\n\n // Store subscription with subscription ID as key\n this.#subscriptions.set(subscriptionId, {\n subscriptionId,\n channels: [...channels], // Store copy of channels\n channelType,\n callback,\n unsubscribe,\n });\n\n return subscription;\n }\n\n // =============================================================================\n // 3. CONNECTION MANAGEMENT (PRIVATE)\n // =============================================================================\n\n /**\n * Builds an authenticated WebSocket URL with bearer token as query parameter.\n * Uses query parameter for WebSocket authentication since native WebSocket\n * doesn't support custom headers during handshake.\n *\n * @param bearerToken - The bearer token to use for authentication\n * @returns The authenticated WebSocket URL\n */\n #buildAuthenticatedUrl(bearerToken: string): string {\n const baseUrl = this.#options.url;\n\n // Add token as query parameter to the WebSocket URL\n const url = new URL(baseUrl);\n url.searchParams.set('token', bearerToken);\n\n return url.toString();\n }\n\n /**\n * Establishes the actual WebSocket connection\n *\n * @param bearerToken - The bearer token to use for authentication\n * @returns Promise that resolves when connection is established\n */\n async #establishConnection(bearerToken: string): Promise<void> {\n const wsUrl = this.#buildAuthenticatedUrl(bearerToken);\n\n // Transition to CONNECTING state before creating WebSocket\n this.#setState(WebSocketState.CONNECTING);\n return this.#trace(\n {\n name: `${SERVICE_NAME} Connection`,\n data: {\n reconnectAttempt: this.#reconnectAttempts,\n },\n tags: {\n service: SERVICE_NAME,\n },\n },\n () => {\n return new Promise<void>((resolve, reject) => {\n // eslint-disable-next-line no-restricted-globals\n const ws = new WebSocket(wsUrl);\n this.#pendingWebSocket = ws;\n this.#connectionTimeout = setTimeout(() => {\n log('WebSocket connection timeout - forcing close', {\n timeout: this.#options.timeout,\n });\n // Close the WebSocket - onclose will handle rejection and state change\n ws.close();\n }, this.#options.timeout);\n\n ws.onopen = (): void => {\n if (this.#connectionTimeout) {\n clearTimeout(this.#connectionTimeout);\n this.#connectionTimeout = null;\n }\n\n if (this.#pendingWebSocket === ws) {\n this.#pendingWebSocket = undefined;\n }\n this.#ws = ws;\n this.#setState(WebSocketState.CONNECTED);\n this.#connectedAt = Date.now();\n\n // Basic Functionality may have turned off during the handshake\n if (!this.#isBasicFunctionality()) {\n this.disconnect();\n resolve();\n return;\n }\n\n // Only reset after connection stays stable for a period (10 seconds)\n // This prevents rapid reconnect loops when server accepts then immediately closes\n this.#stableConnectionTimer = setTimeout(() => {\n this.#stableConnectionTimer = null;\n this.#reconnectAttempts = 0;\n // Create new backoff sequence for fresh start on next disconnect\n this.#newBackoff();\n log('Connection stable - reset reconnect attempts and backoff');\n }, 10000);\n\n resolve();\n };\n\n ws.onclose = (event: CloseEvent): void => {\n log('WebSocket onclose event triggered', {\n code: event.code,\n reason: event.reason || getCloseReason(event.code),\n wasClean: event.wasClean,\n });\n\n // A CONNECTING close() arrives as 1006. The flag records that\n // disconnect() requested it, including when this event is a duplicate.\n const closedByDisconnect = this.#manualClose;\n this.#manualClose = false;\n\n // Guard against duplicate close events\n if (this.#state === WebSocketState.DISCONNECTED) {\n return;\n }\n\n // An open socket keeps code 4999. A handshake abort does not.\n const isManualDisconnect =\n closedByDisconnect ||\n (event.code === MANUAL_DISCONNECT_CODE &&\n event.reason === MANUAL_DISCONNECT_REASON);\n\n // If connection hasn't been established yet, handle the connection promise\n if (this.#state === WebSocketState.CONNECTING) {\n if (isManualDisconnect) {\n // Manual disconnect during connection - resolve to prevent reconnection\n resolve();\n } else {\n // Failed connection attempt - reject to trigger reconnection\n reject(\n new Error(\n `WebSocket connection closed during connection: ${event.code} ${event.reason}`,\n ),\n );\n }\n }\n\n // Calculate connection duration before we clear state (only if we were connected)\n const connectionDurationMs =\n this.#connectedAt > 0 ? Date.now() - this.#connectedAt : 0;\n\n // Clear all timers\n this.#clearTimers();\n\n // Clear WebSocket reference to allow garbage collection\n if (this.#pendingWebSocket === ws) {\n this.#pendingWebSocket = undefined;\n }\n this.#ws = undefined;\n\n // Clear connection tracking\n this.#connectionPromise = null;\n this.#connectedAt = 0;\n\n this.#clearPendingRequests(\n new Error(\n `WebSocket connection closed: ${event.code} ${event.reason || getCloseReason(event.code)}`,\n ),\n );\n this.#clearSubscriptions();\n\n // Update state to disconnected\n this.#setState(WebSocketState.DISCONNECTED);\n\n // Check if this was a manual disconnect\n if (isManualDisconnect) {\n // Manual disconnect - reset attempts and don't reconnect\n this.#reconnectAttempts = 0;\n } else {\n // Unexpected disconnect - schedule reconnection\n this.#scheduleReconnect();\n }\n\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.#trace({\n name: `${SERVICE_NAME} Disconnection`,\n data: {\n code: event.code,\n reason: event.reason || getCloseReason(event.code),\n wasClean: event.wasClean,\n reconnectAttempts: this.#reconnectAttempts,\n ...(connectionDurationMs > 0 && {\n connectionDuration_ms: connectionDurationMs,\n }),\n },\n tags: {\n service: SERVICE_NAME,\n },\n });\n };\n\n // Set up message handler immediately - no need to wait for connection\n ws.onmessage = (event: MessageEvent): void => {\n try {\n const rawData =\n typeof event.data === 'string'\n ? event.data\n : String(event.data);\n this.#handleMessage(this.#parseMessage(rawData));\n } catch {\n // Silently ignore invalid JSON messages\n }\n };\n });\n },\n );\n }\n\n // =============================================================================\n // 4. MESSAGE HANDLING (PRIVATE)\n // =============================================================================\n\n /**\n * Handles incoming WebSocket messages\n *\n * @param message - The WebSocket message to handle\n */\n #handleMessage(message: WebSocketMessage): void {\n if (this.#isServerResponse(message)) {\n this.#handleServerResponse(message);\n return;\n }\n\n if (\n this.#isSubscriptionNotification(message) &&\n this.#handleSubscriptionNotification(message)\n ) {\n return;\n }\n\n if (this.#isChannelMessage(message)) {\n this.#handleChannelMessage(message);\n }\n }\n\n /**\n * Checks if a message is a server response (correlated with client requests)\n *\n * @param message - The message to check\n * @returns True if the message is a server response\n */\n #isServerResponse(\n message: WebSocketMessage,\n ): message is ServerResponseMessage {\n return (\n 'data' in message &&\n message.data &&\n typeof message.data === 'object' &&\n 'requestId' in message.data\n );\n }\n\n /**\n * Checks if a message is a subscription notification (has subscriptionId)\n *\n * @param message - The message to check\n * @returns True if the message is a subscription notification with subscriptionId\n */\n #isSubscriptionNotification(\n message: WebSocketMessage,\n ): message is ServerNotificationMessage {\n return 'subscriptionId' in message;\n }\n\n /**\n * Checks if a message has a channel property (system or subscription notification)\n *\n * @param message - The message to check\n * @returns True if the message has a channel property\n */\n #isChannelMessage(\n message: WebSocketMessage,\n ): message is ServerNotificationMessage {\n return 'channel' in message;\n }\n\n /**\n * Handles server response messages (correlated with client requests)\n *\n * @param message - The server response message to handle\n */\n #handleServerResponse(message: ServerResponseMessage): void {\n const { requestId } = message.data;\n\n const request = this.#pendingRequests.get(requestId);\n if (!request) {\n return;\n }\n\n this.#pendingRequests.delete(requestId);\n clearTimeout(request.timeout);\n\n // Check if the response indicates failure\n if (message.data.failed && message.data.failed.length > 0) {\n request.reject(\n new Error(`Request failed: ${message.data.failed.join(', ')}`),\n );\n } else {\n request.resolve(message.data);\n }\n }\n\n /**\n * Handles messages with channel properties by triggering channel callbacks\n *\n * @param message - The message with channel property to handle\n */\n #handleChannelMessage(message: ServerNotificationMessage): void {\n this.#channelCallbacks.get(message.channel)?.callback(message);\n }\n\n /**\n * Handles server notifications with subscription IDs\n *\n * @param message - The server notification message to handle\n * @returns True if the message was handled, false if it should fall through to channel handling\n */\n #handleSubscriptionNotification(message: ServerNotificationMessage): boolean {\n const { subscriptionId, timestamp, channel } = message;\n\n // Only handle if subscriptionId is defined and not null (allows \"0\" as valid ID)\n if (subscriptionId === null || subscriptionId === undefined) {\n return false;\n }\n\n const subscription = this.#subscriptions.get(subscriptionId);\n if (!subscription?.callback) {\n return false;\n }\n\n // Calculate notification latency: time from server sent to client received\n const receivedAt = Date.now();\n const latency = receivedAt - timestamp;\n\n // Trace notification processing with latency data\n // Use stored channelType instead of parsing each time\n // Promise result intentionally not awaited\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.#trace(\n {\n name: `${SERVICE_NAME} Notification`,\n data: {\n channel,\n latency_ms: latency,\n subscriptionId,\n },\n tags: {\n service: SERVICE_NAME,\n notification_type: subscription.channelType,\n },\n },\n () => {\n subscription.callback?.(message);\n },\n );\n return true;\n }\n\n /**\n * Parse WebSocket message data\n *\n * @param data - The raw message data to parse\n * @returns Parsed message\n */\n #parseMessage(data: string): WebSocketMessage {\n return JSON.parse(data);\n }\n\n // =============================================================================\n // 5. EVENT HANDLERS (PRIVATE)\n // =============================================================================\n\n /**\n * Handles WebSocket errors\n *\n * @param _error - Error that occurred (unused)\n */\n #handleError(_error: Error): void {\n // Placeholder for future error handling logic\n }\n\n // =============================================================================\n // 6. STATE MANAGEMENT (PRIVATE)\n // =============================================================================\n\n /**\n * Schedules a connection attempt with exponential backoff and jitter\n *\n * This method is used for automatic reconnection with Cockatiel's exponential backoff:\n * - Prevents duplicate reconnection timers (idempotent)\n * - Applies exponential backoff with jitter based on previous failures\n * - Jitter uses decorrelated formula to prevent thundering herd problem\n * - Used ONLY for automatic retries, not user-initiated actions\n *\n * Call this from:\n * - connect() catch block (on connection failure)\n * - ws.onclose handler (on unexpected disconnect)\n *\n * For user-initiated actions (sign in, unlock), call connect() directly instead.\n *\n * If a reconnect is already scheduled, this is a no-op to prevent:\n * - Orphaned timers (memory leak)\n * - Inflated reconnect attempts counter\n * - Prematurely long delays\n */\n #scheduleReconnect(): void {\n // If a reconnect is already scheduled, don't schedule another one\n if (this.#reconnectTimer) {\n return;\n }\n\n // Increment attempts BEFORE calculating delay so backoff grows properly\n this.#reconnectAttempts += 1;\n\n // Use Cockatiel's exponential backoff to get delay with jitter\n const delay = this.#backoff.duration;\n\n // Progress to next backoff state for future reconnect attempts\n // Pass attempt number as context (though ExponentialBackoff doesn't use it)\n this.#backoff = this.#backoff.next({ attempt: this.#reconnectAttempts });\n\n log('Scheduling reconnect', {\n attempt: this.#reconnectAttempts,\n delay_ms: delay,\n });\n\n this.#reconnectTimer = setTimeout(() => {\n // A closed gate drops this timer and resets the attempt count.\n // Leave the timer set until then so that clear still runs.\n if (this.#stopDisallowedConnection()) {\n return;\n }\n\n this.#reconnectTimer = null;\n\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.connect();\n }, delay);\n }\n\n /**\n * Creates a new exponential backoff sequence\n */\n #newBackoff(): void {\n this.#backoff = new ExponentialBackoff({\n initialDelay: this.#options.reconnectDelay,\n maxDelay: this.#options.maxReconnectDelay,\n }).next();\n }\n\n #clearTimers(): void {\n if (this.#reconnectTimer) {\n clearTimeout(this.#reconnectTimer);\n this.#reconnectTimer = null;\n }\n if (this.#connectionTimeout) {\n clearTimeout(this.#connectionTimeout);\n this.#connectionTimeout = null;\n }\n if (this.#stableConnectionTimer) {\n clearTimeout(this.#stableConnectionTimer);\n this.#stableConnectionTimer = null;\n }\n }\n\n /**\n * Clears all pending requests and rejects them with the given error\n *\n * @param error - Error to reject with\n */\n #clearPendingRequests(error: Error): void {\n for (const [, request] of this.#pendingRequests) {\n clearTimeout(request.timeout);\n request.reject(error);\n }\n this.#pendingRequests.clear();\n }\n\n /**\n * Clears all active subscriptions\n */\n #clearSubscriptions(): void {\n this.#subscriptions.clear();\n }\n\n /**\n * Sets the connection state and emits state change events\n *\n * @param newState - The new WebSocket state\n */\n #setState(newState: WebSocketState): void {\n const oldState = this.#state;\n this.#state = newState;\n\n if (oldState !== newState) {\n // Publish connection state change event\n // Messenger handles listener errors internally, no need for try-catch\n this.#messenger.publish(\n 'BackendWebSocketService:connectionStateChanged',\n this.getConnectionInfo(),\n );\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"BackendWebSocketService.js","sourceRoot":"","sources":["../../src/ws/BackendWebSocketService.ts"],"names":[],"mappings":"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","sourcesContent":["import type { TraceCallback } from '@metamask/controller-utils';\nimport { ExponentialBackoff } from '@metamask/controller-utils';\nimport type {\n KeyringControllerLockEvent,\n KeyringControllerUnlockEvent,\n} from '@metamask/keyring-controller';\nimport type { Messenger } from '@metamask/messenger';\nimport type { AuthenticationController } from '@metamask/profile-sync-controller';\nimport { getErrorMessage } from '@metamask/utils';\nimport { v4 as uuidV4 } from 'uuid';\n\nimport { projectLogger, createModuleLogger } from '../logger.js';\nimport type { BackendWebSocketServiceMethodActions } from './BackendWebSocketService-method-action-types.js';\n\nconst SERVICE_NAME = 'BackendWebSocketService' as const;\n\nconst log = createModuleLogger(projectLogger, SERVICE_NAME);\n\n// WebSocket close codes and reasons for internal operations\nconst MANUAL_DISCONNECT_CODE = 4999 as const;\nconst MANUAL_DISCONNECT_REASON = 'Internal: Manual disconnect' as const;\nconst FORCE_RECONNECT_CODE = 4998 as const;\nconst FORCE_RECONNECT_REASON = 'Internal: Force reconnect' as const;\n\nconst MESSENGER_EXPOSED_METHODS = [\n 'connect',\n 'disconnect',\n 'forceReconnection',\n 'sendMessage',\n 'sendRequest',\n 'subscribe',\n 'getConnectionInfo',\n 'getSubscriptionsByChannel',\n 'channelHasSubscription',\n 'findSubscriptionsByChannelPrefix',\n 'addChannelCallback',\n 'removeChannelCallback',\n 'getChannelCallbacks',\n] as const;\n\n/**\n * Gets human-readable close reason from RFC 6455 close code\n *\n * @param code - WebSocket close code\n * @returns Human-readable close reason\n */\nexport function getCloseReason(code: number): string {\n switch (code) {\n case 1000:\n return 'Normal Closure';\n case 1001:\n return 'Going Away';\n case 1002:\n return 'Protocol Error';\n case 1003:\n return 'Unsupported Data';\n case 1004:\n return 'Reserved';\n case 1005:\n return 'No Status Received';\n case 1006:\n return 'Abnormal Closure';\n case 1007:\n return 'Invalid frame payload data';\n case 1008:\n return 'Policy Violation';\n case 1009:\n return 'Message Too Big';\n case 1010:\n return 'Mandatory Extension';\n case 1011:\n return 'Internal Server Error';\n case 1012:\n return 'Service Restart';\n case 1013:\n return 'Try Again Later';\n case 1014:\n return 'Bad Gateway';\n case 1015:\n return 'TLS Handshake';\n default:\n if (code >= 3000 && code <= 3999) {\n return 'Library/Framework Error';\n }\n if (code >= 4000 && code <= 4999) {\n return 'Application Error';\n }\n return 'Unknown';\n }\n}\n\n/**\n * WebSocket connection states\n */\nexport enum WebSocketState {\n CONNECTING = 'connecting',\n CONNECTED = 'connected',\n /** @deprecated This value is no longer used internally and will be removed in a future major release */\n DISCONNECTING = 'disconnecting',\n DISCONNECTED = 'disconnected',\n /** @deprecated TThis value is no longer used internally and will be removed in a future major release */\n ERROR = 'error',\n}\n\n/**\n * WebSocket event types\n */\nexport enum WebSocketEventType {\n CONNECTED = 'connected',\n DISCONNECTED = 'disconnected',\n MESSAGE = 'message',\n ERROR = 'error',\n RECONNECTING = 'reconnecting',\n RECONNECTED = 'reconnected',\n}\n\n/**\n * Configuration options for the WebSocket service\n */\nexport type BackendWebSocketServiceOptions = {\n /** The WebSocket URL to connect to */\n url: string;\n\n /** The messenger for inter-service communication */\n messenger: BackendWebSocketServiceMessenger;\n\n /** Connection timeout in milliseconds (default: 10000) */\n timeout?: number;\n\n /** Initial reconnection delay in milliseconds (default: 10000) */\n reconnectDelay?: number;\n\n /** Maximum reconnection delay in milliseconds (default: 60000) */\n maxReconnectDelay?: number;\n\n /** Request timeout in milliseconds (default: 30000) */\n requestTimeout?: number;\n\n /** Optional callback to determine if connection should be enabled (default: always enabled) */\n isEnabled?: () => boolean;\n\n /** Optional callback to trace performance of WebSocket operations (default: no-op) */\n traceFn?: TraceCallback;\n};\n\n/**\n * Client Request message\n * Used when client sends a request to the server\n */\nexport type ClientRequestMessage = {\n event: string;\n data: {\n requestId: string;\n channels?: string[];\n [key: string]: unknown;\n };\n};\n\n/**\n * Server Response message\n * Used when server responds to a client request\n */\nexport type ServerResponseMessage = {\n event: string;\n data: {\n requestId: string;\n subscriptionId?: string;\n succeeded?: string[];\n failed?: string[];\n [key: string]: unknown;\n };\n};\n\n/**\n * Server Notification message\n * Used when server sends unsolicited data to client\n * subscriptionId is optional for system-wide notifications\n */\nexport type ServerNotificationMessage = {\n event: string;\n subscriptionId?: string;\n channel: string;\n data: Record<string, unknown>;\n timestamp: number;\n};\n\n/**\n * Union type for all WebSocket messages\n */\nexport type WebSocketMessage =\n | ClientRequestMessage\n | ServerResponseMessage\n | ServerNotificationMessage;\n\n/**\n * Channel-based callback configuration\n */\nexport type ChannelCallback = {\n /** Channel name to match (also serves as the unique identifier) */\n channelName: string;\n /** Callback function */\n callback: (notification: ServerNotificationMessage) => void;\n};\n\n/**\n * Unified WebSocket subscription object used for both internal storage and external API\n */\nexport type WebSocketSubscription = {\n /** The subscription ID from the server */\n subscriptionId: string;\n /** Channel names for this subscription */\n channels: string[];\n /** Channel type with version (e.g., 'account-activity.v1') extracted from first channel */\n channelType: string;\n /** Callback function for handling notifications (optional for external use) */\n callback?: (notification: ServerNotificationMessage) => void;\n /** Function to unsubscribe and clean up */\n unsubscribe: (requestId?: string) => Promise<void>;\n};\n\n/**\n * WebSocket connection info\n */\nexport type WebSocketConnectionInfo = {\n state: WebSocketState;\n url: string;\n reconnectAttempts: number;\n timeout: number;\n reconnectDelay: number;\n maxReconnectDelay: number;\n requestTimeout: number;\n connectedAt?: number;\n};\n\n// Action types for the messaging system - using generated method actions\nexport type BackendWebSocketServiceActions =\n BackendWebSocketServiceMethodActions;\n\ntype AllowedActions =\n AuthenticationController.AuthenticationControllerGetBearerTokenAction;\n\n// Event types for WebSocket connection state changes\nexport type BackendWebSocketServiceConnectionStateChangedEvent = {\n type: 'BackendWebSocketService:connectionStateChanged';\n payload: [WebSocketConnectionInfo];\n};\n\ntype AllowedEvents =\n | AuthenticationController.AuthenticationControllerStateChangeEvent\n | KeyringControllerLockEvent\n | KeyringControllerUnlockEvent;\n\nexport type BackendWebSocketServiceEvents =\n BackendWebSocketServiceConnectionStateChangedEvent;\n\nexport type BackendWebSocketServiceMessenger = Messenger<\n typeof SERVICE_NAME,\n BackendWebSocketServiceActions | AllowedActions,\n BackendWebSocketServiceEvents | AllowedEvents\n>;\n\n/**\n * WebSocket Service with automatic reconnection, session management and direct callback routing\n *\n * Connection Management:\n * - Automatically subscribes to AuthenticationController:stateChange (sign in/out)\n * - Automatically subscribes to KeyringController:lock/unlock events\n * - Idempotent connect() function safe for multiple rapid calls\n * - Auto-reconnects on unexpected disconnects (manualDisconnect = false)\n *\n * Platform Responsibilities:\n * - Call connect() when app opens/foregrounds\n * - Call disconnect() when app closes/backgrounds\n * - Provide isEnabled() callback (feature flag)\n * - Call destroy() on app termination\n *\n * Real-Time Performance Optimizations:\n * - Fast path message routing (zero allocations)\n * - Production mode removes try-catch overhead\n * - Optimized JSON parsing with fail-fast\n * - Direct callback routing bypasses event emitters\n * - Memory cleanup and resource management\n */\nexport class BackendWebSocketService {\n /**\n * The name of the service.\n */\n readonly name = SERVICE_NAME;\n\n readonly #messenger: BackendWebSocketServiceMessenger;\n\n readonly #options: Required<\n Omit<BackendWebSocketServiceOptions, 'messenger' | 'isEnabled' | 'traceFn'>\n >;\n\n readonly #isEnabled: (() => boolean) | undefined;\n\n readonly #trace: TraceCallback;\n\n #ws: WebSocket | undefined;\n\n #state: WebSocketState = WebSocketState.DISCONNECTED;\n\n #reconnectAttempts = 0;\n\n #reconnectTimer: NodeJS.Timeout | null = null;\n\n #connectionTimeout: NodeJS.Timeout | null = null;\n\n #stableConnectionTimer: NodeJS.Timeout | null = null;\n\n // Track the current connection promise to handle concurrent connection attempts\n #connectionPromise: Promise<void> | null = null;\n\n readonly #pendingRequests = new Map<\n string,\n {\n resolve: (value: unknown) => void;\n reject: (error: Error) => void;\n timeout: NodeJS.Timeout;\n }\n >();\n\n #connectedAt: number = 0;\n\n // Simplified subscription storage (single flat map)\n // Key: subscription ID string (e.g., 'sub_abc123def456')\n // Value: WebSocketSubscription object with channels, callback and metadata\n readonly #subscriptions = new Map<string, WebSocketSubscription>();\n\n // Channel-based callback storage\n // Key: channel name (serves as unique identifier)\n // Value: ChannelCallback configuration\n readonly #channelCallbacks = new Map<string, ChannelCallback>();\n\n // Backoff instance for reconnection delays (reset on stable connection)\n #backoff!: ReturnType<ExponentialBackoff<unknown>['next']>;\n\n // =============================================================================\n // 1. CONSTRUCTOR & INITIALIZATION\n // =============================================================================\n\n /**\n * Creates a new WebSocket service instance\n *\n * @param options - Configuration options for the WebSocket service\n */\n constructor(options: BackendWebSocketServiceOptions) {\n this.#messenger = options.messenger;\n this.#isEnabled = options.isEnabled;\n // Default to no-op trace function to keep core platform-agnostic\n this.#trace =\n options.traceFn ??\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (((_request: any, fn?: any) => fn?.()) as TraceCallback);\n\n this.#options = {\n url: options.url,\n timeout: options.timeout ?? 10000,\n reconnectDelay: options.reconnectDelay ?? 10000,\n maxReconnectDelay: options.maxReconnectDelay ?? 60000,\n requestTimeout: options.requestTimeout ?? 30000,\n };\n\n // Initialize backoff for reconnection delays\n this.#newBackoff();\n\n // Subscribe to authentication and keyring controller events\n this.#subscribeEvents();\n\n // Register action handlers using the method actions pattern\n this.#messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n }\n\n /**\n * Setup event handling for authentication and wallet lock state\n *\n * Three event sources trigger connection/disconnection:\n * 1. AuthenticationController:stateChange (sign in/out)\n * 2. KeyringController:unlock (wallet unlocked)\n * 3. KeyringController:lock (wallet locked)\n *\n * All connect() calls are idempotent and validate all requirements.\n */\n #subscribeEvents(): void {\n // Subscribe to authentication state changes (sign in/out)\n this.#messenger.subscribe(\n 'AuthenticationController:stateChange',\n (state: AuthenticationController.AuthenticationControllerState) => {\n if (state.isSignedIn) {\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.connect();\n } else {\n this.disconnect();\n }\n },\n (state) => ({ isSignedIn: state.isSignedIn }),\n );\n\n // Subscribe to wallet unlock event\n this.#messenger.subscribe('KeyringController:unlock', () => {\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.connect();\n });\n\n // Subscribe to wallet lock event\n this.#messenger.subscribe('KeyringController:lock', () => {\n this.disconnect();\n });\n }\n\n // =============================================================================\n // 2. PUBLIC API METHODS\n // =============================================================================\n\n /**\n * Establishes WebSocket connection with smart reconnection behavior\n *\n * Connection Requirements (all must be true):\n * 1. Feature enabled (isEnabled() = true)\n * 2. Wallet unlocked (checked by getBearerToken)\n * 3. User signed in (checked by getBearerToken)\n *\n * Platform code should call this when app opens/foregrounds.\n * Automatically called on KeyringController:unlock event.\n *\n * @returns Promise that resolves when connection is established\n */\n async connect(): Promise<void> {\n // Priority 1: Check if feature is enabled via callback (feature flag check)\n // If feature is disabled, stop all connection attempts\n if (this.#isEnabled && !this.#isEnabled()) {\n // Clear any pending reconnection attempts since feature is disabled\n this.#clearTimers();\n this.#reconnectAttempts = 0;\n return;\n }\n\n // If already connected, return immediately\n if (this.#state === WebSocketState.CONNECTED) {\n return;\n }\n\n // If already connecting, wait for the existing connection attempt to complete\n if (this.#connectionPromise) {\n await this.#connectionPromise;\n return;\n }\n\n // If a reconnect is already scheduled, defer to it to avoid bypassing exponential backoff\n // This prevents rapid loops when server accepts then immediately closes connections\n if (this.#reconnectTimer) {\n return;\n }\n\n // Create and store the connection promise IMMEDIATELY (before any async operations)\n // This ensures subsequent connect() calls will wait for this promise instead of creating new connections\n this.#connectionPromise = (async (): Promise<void> => {\n // Priority 2: Check authentication requirements (signed in)\n let bearerToken: string;\n try {\n const token = await this.#messenger.call(\n 'AuthenticationController:getBearerToken',\n );\n if (!token) {\n throw new Error('Authentication required: user not signed in');\n }\n bearerToken = token;\n } catch (error) {\n log('Failed to check authentication requirements', { error });\n throw error;\n }\n\n // Establish the actual WebSocket connection\n await this.#establishConnection(bearerToken);\n })();\n\n try {\n await this.#connectionPromise;\n } catch {\n // Always schedule reconnect on any failure\n // Exponential backoff will prevent aggressive retries\n this.#scheduleReconnect();\n } finally {\n // Clear the connection promise when done (success or failure)\n this.#connectionPromise = null;\n }\n }\n\n /**\n * Closes WebSocket connection\n */\n disconnect(): void {\n if (this.#state === WebSocketState.DISCONNECTED || !this.#ws) {\n return;\n }\n\n // Close WebSocket with manual disconnect code and reason\n this.#ws.close(MANUAL_DISCONNECT_CODE, MANUAL_DISCONNECT_REASON);\n\n log('WebSocket manually disconnected');\n }\n\n /**\n * Forces a WebSocket reconnection to clean up subscription state\n *\n * This method is useful when subscription state may be out of sync and needs to be reset.\n * It performs a controlled disconnect-then-reconnect sequence:\n * - Disconnects cleanly to trigger subscription cleanup\n * - Schedules reconnection with exponential backoff to prevent rapid loops\n * - All subscriptions will be cleaned up automatically on disconnect\n *\n * Use cases:\n * - Recovering from subscription/unsubscription issues\n * - Cleaning up orphaned subscriptions\n * - Forcing a fresh subscription state\n *\n * @returns Promise that resolves when disconnection is complete (reconnection is scheduled)\n */\n async forceReconnection(): Promise<void> {\n if (this.#state === WebSocketState.DISCONNECTED || !this.#ws) {\n log('WebSocket already disconnected, scheduling reconnect');\n this.#scheduleReconnect();\n return;\n }\n\n log('Forcing WebSocket reconnection to clean up subscription state');\n\n // This ensures ws.onclose will schedule a reconnect (not treat it as manual disconnect)\n this.#ws.close(FORCE_RECONNECT_CODE, FORCE_RECONNECT_REASON);\n }\n\n /**\n * Sends a message through the WebSocket (fire-and-forget, no response expected)\n *\n * This is a low-level method for sending messages without waiting for a response.\n * Most consumers should use `sendRequest()` instead, which handles request-response\n * correlation and provides proper error handling with timeouts.\n *\n * Use this method only when:\n * - You don't need a response from the server\n * - You're implementing custom message protocols\n * - You need fine-grained control over message timing\n *\n * @param message - The message to send\n * @throws Error if WebSocket is not connected or send fails\n *\n * @see sendRequest for request-response pattern with automatic correlation\n */\n sendMessage(message: ClientRequestMessage): void {\n if (this.#state !== WebSocketState.CONNECTED || !this.#ws) {\n throw new Error(`Cannot send message: WebSocket is ${this.#state}`);\n }\n\n try {\n this.#ws.send(JSON.stringify(message));\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n this.#handleError(new Error(errorMessage));\n throw new Error(errorMessage);\n }\n }\n\n /**\n * Sends a request and waits for a correlated response (recommended for most use cases)\n *\n * This is the recommended high-level method for request-response communication.\n * It automatically handles:\n * - Request ID generation and correlation\n * - Response matching with timeout protection\n * - Automatic reconnection on timeout\n * - Proper cleanup of pending requests\n *\n * @param message - The request message (can include optional requestId for testing)\n * @returns Promise that resolves with the response data\n * @throws Error if WebSocket is not connected, request times out, or response indicates failure\n *\n * @see sendMessage for fire-and-forget messaging without response handling\n */\n async sendRequest<Type = ServerResponseMessage['data']>(\n message: Omit<ClientRequestMessage, 'data'> & {\n data?: Omit<ClientRequestMessage['data'], 'requestId'> & {\n requestId?: string;\n };\n },\n ): Promise<Type> {\n if (this.#state !== WebSocketState.CONNECTED) {\n throw new Error(`Cannot send request: WebSocket is ${this.#state}`);\n }\n\n // Use provided requestId if available, otherwise generate a new one\n const requestId = message.data?.requestId ?? uuidV4();\n const requestMessage: ClientRequestMessage = {\n event: message.event,\n data: {\n ...message.data,\n requestId, // Set after spread to ensure it's not overwritten by undefined\n },\n };\n\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n this.#pendingRequests.delete(requestId);\n log('Request timeout - triggering reconnection', {\n timeout: this.#options.requestTimeout,\n });\n\n // Trigger reconnection on request timeout as it may indicate stale connection\n if (this.#state === WebSocketState.CONNECTED && this.#ws) {\n // Force close the current connection to trigger reconnection logic\n this.#ws.close(3000, 'Request timeout - forcing reconnect');\n }\n\n reject(\n new Error(`Request timeout after ${this.#options.requestTimeout}ms`),\n );\n }, this.#options.requestTimeout);\n\n // Store in pending requests for response correlation\n this.#pendingRequests.set(requestId, {\n resolve: resolve as (value: unknown) => void,\n reject,\n timeout,\n });\n\n // Send the request\n try {\n this.sendMessage(requestMessage);\n } catch (error) {\n this.#pendingRequests.delete(requestId);\n clearTimeout(timeout);\n reject(error instanceof Error ? error : new Error(String(error)));\n }\n });\n }\n\n /**\n * Gets current connection information\n *\n * @returns Current connection status and details\n */\n getConnectionInfo(): WebSocketConnectionInfo {\n return {\n state: this.#state,\n url: this.#options.url,\n timeout: this.#options.timeout,\n reconnectDelay: this.#options.reconnectDelay,\n maxReconnectDelay: this.#options.maxReconnectDelay,\n requestTimeout: this.#options.requestTimeout,\n reconnectAttempts: this.#reconnectAttempts,\n connectedAt: this.#connectedAt,\n };\n }\n\n /**\n * Gets all subscription information for a specific channel\n *\n * @param channel - The channel name to look up\n * @returns Array of subscription details for all subscriptions containing the channel\n */\n getSubscriptionsByChannel(channel: string): WebSocketSubscription[] {\n const matchingSubscriptions: WebSocketSubscription[] = [];\n for (const [subscriptionId, subscription] of this.#subscriptions) {\n if (subscription.channels.includes(channel)) {\n matchingSubscriptions.push({\n subscriptionId,\n channels: subscription.channels,\n channelType: subscription.channelType,\n unsubscribe: subscription.unsubscribe,\n });\n }\n }\n return matchingSubscriptions;\n }\n\n /**\n * Checks if a channel has a subscription\n *\n * @param channel - The channel name to check\n * @returns True if the channel has a subscription, false otherwise\n */\n channelHasSubscription(channel: string): boolean {\n for (const subscription of this.#subscriptions.values()) {\n if (subscription.channels.includes(channel)) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Finds all subscriptions that have channels starting with the specified prefix\n *\n * @param channelPrefix - The channel prefix to search for (e.g., \"account-activity.v1\")\n * @returns Array of subscription info for matching subscriptions\n */\n findSubscriptionsByChannelPrefix(\n channelPrefix: string,\n ): WebSocketSubscription[] {\n const matchingSubscriptions: WebSocketSubscription[] = [];\n\n for (const [subscriptionId, subscription] of this.#subscriptions) {\n // Check if any channel in this subscription starts with the prefix\n const hasMatchingChannel = subscription.channels.some((channel) =>\n channel.startsWith(channelPrefix),\n );\n\n if (hasMatchingChannel) {\n matchingSubscriptions.push({\n subscriptionId,\n channels: subscription.channels,\n channelType: subscription.channelType,\n unsubscribe: subscription.unsubscribe,\n });\n }\n }\n\n return matchingSubscriptions;\n }\n\n /**\n * Register a callback for specific channels (local callback only, no server subscription)\n *\n * **Key Difference from `subscribe()`:**\n * - `addChannelCallback()`: Registers a local callback without creating a server-side subscription.\n * The callback triggers on ANY message matching the channel name, regardless of subscriptionId.\n * Useful for system-wide notifications or when you don't control the subscription lifecycle.\n *\n * - `subscribe()`: Creates a proper server-side subscription with a subscriptionId.\n * The callback only triggers for messages with the matching subscriptionId.\n * Includes proper lifecycle management (unsubscribe, automatic cleanup on disconnect).\n *\n * **When to use `addChannelCallback()`:**\n * - Listening to system-wide notifications (e.g., 'system-notifications.v1')\n * - Monitoring channels where subscriptions are managed elsewhere\n * - Debug/logging scenarios where you want to observe all channel messages\n *\n * **When to use `subscribe()` instead:**\n * - Creating new subscriptions that need server-side registration\n * - When you need proper cleanup via unsubscribe\n * - Most application use cases (recommended approach)\n *\n * @param options - Channel callback configuration\n * @param options.channelName - Channel name to match exactly\n * @param options.callback - Function to call when channel matches\n *\n * @example\n * ```typescript\n * // Listen to system notifications (no server subscription needed)\n * webSocketService.addChannelCallback({\n * channelName: 'system-notifications.v1',\n * callback: (notification) => {\n * console.log('System notification:', notification.data);\n * }\n * });\n *\n * // For account-specific subscriptions, use subscribe() instead:\n * // const sub = await webSocketService.subscribe({\n * // channels: ['account-activity.v1.eip155:0:0x1234...'],\n * // callback: (notification) => { ... }\n * // });\n * ```\n *\n * @see subscribe for creating proper server-side subscriptions with lifecycle management\n */\n addChannelCallback(options: {\n channelName: string;\n callback: (notification: ServerNotificationMessage) => void;\n }): void {\n const channelCallback: ChannelCallback = {\n channelName: options.channelName,\n callback: options.callback,\n };\n\n // Check if callback already exists for this channel\n if (this.#channelCallbacks.has(options.channelName)) {\n return;\n }\n\n this.#channelCallbacks.set(options.channelName, channelCallback);\n }\n\n /**\n * Remove a channel callback\n *\n * @param channelName - The channel name returned from addChannelCallback\n * @returns True if callback was found and removed, false otherwise\n */\n removeChannelCallback(channelName: string): boolean {\n return this.#channelCallbacks.delete(channelName);\n }\n\n /**\n * Get all registered channel callbacks (for debugging)\n *\n * @returns Array of all registered channel callbacks\n */\n getChannelCallbacks(): ChannelCallback[] {\n return Array.from(this.#channelCallbacks.values());\n }\n\n /**\n * Destroy the service and clean up resources\n * Called when service is being destroyed or app is terminating\n */\n destroy(): void {\n // Always clear timers first to prevent reconnection attempts after destruction\n // This handles the case where destroy() is called while DISCONNECTED with a pending reconnect timer\n this.#clearTimers();\n\n // Reset reconnect attempts to prevent any future reconnection logic\n this.#reconnectAttempts = 0;\n\n // Disconnect the WebSocket if connected (will be no-op if already disconnected)\n this.disconnect();\n }\n\n /**\n * Create and manage a subscription with server-side registration (recommended for most use cases)\n *\n * This is the recommended subscription API for high-level services. It creates a proper\n * server-side subscription and routes notifications based on subscriptionId.\n *\n * **Key Features:**\n * - Creates server-side subscription with unique subscriptionId\n * - Callback triggered only for messages with matching subscriptionId\n * - Automatic lifecycle management (cleanup on disconnect)\n * - Includes unsubscribe method for proper cleanup\n * - Request-response pattern with error handling\n *\n * **When to use `subscribe()`:**\n * - Creating new subscriptions (account activity, price updates, etc.)\n * - When you need proper cleanup/unsubscribe functionality\n * - Most application use cases\n *\n * **When to use `addChannelCallback()` instead:**\n * - System-wide notifications without server-side subscription\n * - Observing channels managed elsewhere\n * - Debug/logging scenarios\n *\n * @param options - Subscription configuration\n * @param options.channels - Array of channel names to subscribe to\n * @param options.callback - Callback function for handling notifications\n * @param options.requestId - Optional request ID for testing (will generate UUID if not provided)\n * @param options.channelType - Channel type identifier\n * @returns Subscription object with unsubscribe method\n *\n * @example\n * ```typescript\n * // AccountActivityService usage\n * const subscription = await webSocketService.subscribe({\n * channels: ['account-activity.v1.eip155:0:0x1234...'],\n * callback: (notification) => {\n * this.handleAccountActivity(notification.data);\n * }\n * });\n *\n * // Later, clean up\n * await subscription.unsubscribe();\n * ```\n *\n * @see addChannelCallback for local callbacks without server-side subscription\n */\n async subscribe(options: {\n /** Channel names to subscribe to */\n channels: string[];\n /** Channel type with version (e.g., 'account-activity.v1') for tracing and monitoring */\n channelType: string;\n /** Handler for incoming notifications */\n callback: (notification: ServerNotificationMessage) => void;\n /** Optional request ID for testing (will generate UUID if not provided) */\n requestId?: string;\n }): Promise<WebSocketSubscription> {\n const { channels, channelType, callback, requestId } = options;\n\n if (this.#state !== WebSocketState.CONNECTED) {\n throw new Error(\n `Cannot create subscription(s) ${channels.join(', ')}: WebSocket is ${this.#state}`,\n );\n }\n\n // Send subscription request and wait for response\n const subscriptionResponse = await this.sendRequest({\n event: 'subscribe',\n data: { channels, requestId },\n });\n\n if (!subscriptionResponse?.subscriptionId) {\n throw new Error('Invalid subscription response: missing subscription ID');\n }\n\n const { subscriptionId } = subscriptionResponse;\n\n // Create unsubscribe function\n const unsubscribe = async (unsubRequestId?: string): Promise<void> => {\n // Send unsubscribe request first\n await this.sendRequest({\n event: 'unsubscribe',\n data: {\n subscription: subscriptionId,\n channels,\n requestId: unsubRequestId,\n },\n });\n\n // Clean up subscription mapping\n this.#subscriptions.delete(subscriptionId);\n };\n\n const subscription = {\n subscriptionId,\n channels: [...channels],\n channelType,\n unsubscribe,\n };\n\n // Store subscription with subscription ID as key\n this.#subscriptions.set(subscriptionId, {\n subscriptionId,\n channels: [...channels], // Store copy of channels\n channelType,\n callback,\n unsubscribe,\n });\n\n return subscription;\n }\n\n // =============================================================================\n // 3. CONNECTION MANAGEMENT (PRIVATE)\n // =============================================================================\n\n /**\n * Builds an authenticated WebSocket URL with bearer token as query parameter.\n * Uses query parameter for WebSocket authentication since native WebSocket\n * doesn't support custom headers during handshake.\n *\n * @param bearerToken - The bearer token to use for authentication\n * @returns The authenticated WebSocket URL\n */\n #buildAuthenticatedUrl(bearerToken: string): string {\n const baseUrl = this.#options.url;\n\n // Add token as query parameter to the WebSocket URL\n const url = new URL(baseUrl);\n url.searchParams.set('token', bearerToken);\n\n return url.toString();\n }\n\n /**\n * Establishes the actual WebSocket connection\n *\n * @param bearerToken - The bearer token to use for authentication\n * @returns Promise that resolves when connection is established\n */\n async #establishConnection(bearerToken: string): Promise<void> {\n const wsUrl = this.#buildAuthenticatedUrl(bearerToken);\n\n // Transition to CONNECTING state before creating WebSocket\n this.#setState(WebSocketState.CONNECTING);\n return this.#trace(\n {\n name: `${SERVICE_NAME} Connection`,\n data: {\n reconnectAttempt: this.#reconnectAttempts,\n },\n tags: {\n service: SERVICE_NAME,\n },\n },\n () => {\n return new Promise<void>((resolve, reject) => {\n // eslint-disable-next-line no-restricted-globals\n const ws = new WebSocket(wsUrl);\n this.#connectionTimeout = setTimeout(() => {\n log('WebSocket connection timeout - forcing close', {\n timeout: this.#options.timeout,\n });\n // Close the WebSocket - onclose will handle rejection and state change\n ws.close();\n }, this.#options.timeout);\n\n ws.onopen = (): void => {\n if (this.#connectionTimeout) {\n clearTimeout(this.#connectionTimeout);\n this.#connectionTimeout = null;\n }\n\n this.#ws = ws;\n this.#setState(WebSocketState.CONNECTED);\n this.#connectedAt = Date.now();\n\n // Only reset after connection stays stable for a period (10 seconds)\n // This prevents rapid reconnect loops when server accepts then immediately closes\n this.#stableConnectionTimer = setTimeout(() => {\n this.#stableConnectionTimer = null;\n this.#reconnectAttempts = 0;\n // Create new backoff sequence for fresh start on next disconnect\n this.#newBackoff();\n log('Connection stable - reset reconnect attempts and backoff');\n }, 10000);\n\n resolve();\n };\n\n ws.onclose = (event: CloseEvent): void => {\n log('WebSocket onclose event triggered', {\n code: event.code,\n reason: event.reason || getCloseReason(event.code),\n wasClean: event.wasClean,\n });\n\n // Guard against duplicate close events\n if (this.#state === WebSocketState.DISCONNECTED) {\n return;\n }\n\n // Detect if this is a manual disconnect or service cleanup based on close code\n const isManualDisconnect =\n event.code === MANUAL_DISCONNECT_CODE &&\n event.reason === MANUAL_DISCONNECT_REASON;\n\n // If connection hasn't been established yet, handle the connection promise\n if (this.#state === WebSocketState.CONNECTING) {\n if (isManualDisconnect) {\n // Manual disconnect during connection - resolve to prevent reconnection\n resolve();\n } else {\n // Failed connection attempt - reject to trigger reconnection\n reject(\n new Error(\n `WebSocket connection closed during connection: ${event.code} ${event.reason}`,\n ),\n );\n }\n }\n\n // Calculate connection duration before we clear state (only if we were connected)\n const connectionDurationMs =\n this.#connectedAt > 0 ? Date.now() - this.#connectedAt : 0;\n\n // Clear all timers\n this.#clearTimers();\n\n // Clear WebSocket reference to allow garbage collection\n this.#ws = undefined;\n\n // Clear connection tracking\n this.#connectionPromise = null;\n this.#connectedAt = 0;\n\n this.#clearPendingRequests(\n new Error(\n `WebSocket connection closed: ${event.code} ${event.reason || getCloseReason(event.code)}`,\n ),\n );\n this.#clearSubscriptions();\n\n // Update state to disconnected\n this.#setState(WebSocketState.DISCONNECTED);\n\n // Check if this was a manual disconnect\n if (isManualDisconnect) {\n // Manual disconnect - reset attempts and don't reconnect\n this.#reconnectAttempts = 0;\n } else {\n // Unexpected disconnect - schedule reconnection\n this.#scheduleReconnect();\n }\n\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.#trace({\n name: `${SERVICE_NAME} Disconnection`,\n data: {\n code: event.code,\n reason: event.reason || getCloseReason(event.code),\n wasClean: event.wasClean,\n reconnectAttempts: this.#reconnectAttempts,\n ...(connectionDurationMs > 0 && {\n connectionDuration_ms: connectionDurationMs,\n }),\n },\n tags: {\n service: SERVICE_NAME,\n },\n });\n };\n\n // Set up message handler immediately - no need to wait for connection\n ws.onmessage = (event: MessageEvent): void => {\n try {\n const rawData =\n typeof event.data === 'string'\n ? event.data\n : String(event.data);\n this.#handleMessage(this.#parseMessage(rawData));\n } catch {\n // Silently ignore invalid JSON messages\n }\n };\n });\n },\n );\n }\n\n // =============================================================================\n // 4. MESSAGE HANDLING (PRIVATE)\n // =============================================================================\n\n /**\n * Handles incoming WebSocket messages\n *\n * @param message - The WebSocket message to handle\n */\n #handleMessage(message: WebSocketMessage): void {\n if (this.#isServerResponse(message)) {\n this.#handleServerResponse(message);\n return;\n }\n\n if (\n this.#isSubscriptionNotification(message) &&\n this.#handleSubscriptionNotification(message)\n ) {\n return;\n }\n\n if (this.#isChannelMessage(message)) {\n this.#handleChannelMessage(message);\n }\n }\n\n /**\n * Checks if a message is a server response (correlated with client requests)\n *\n * @param message - The message to check\n * @returns True if the message is a server response\n */\n #isServerResponse(\n message: WebSocketMessage,\n ): message is ServerResponseMessage {\n return (\n 'data' in message &&\n message.data &&\n typeof message.data === 'object' &&\n 'requestId' in message.data\n );\n }\n\n /**\n * Checks if a message is a subscription notification (has subscriptionId)\n *\n * @param message - The message to check\n * @returns True if the message is a subscription notification with subscriptionId\n */\n #isSubscriptionNotification(\n message: WebSocketMessage,\n ): message is ServerNotificationMessage {\n return 'subscriptionId' in message;\n }\n\n /**\n * Checks if a message has a channel property (system or subscription notification)\n *\n * @param message - The message to check\n * @returns True if the message has a channel property\n */\n #isChannelMessage(\n message: WebSocketMessage,\n ): message is ServerNotificationMessage {\n return 'channel' in message;\n }\n\n /**\n * Handles server response messages (correlated with client requests)\n *\n * @param message - The server response message to handle\n */\n #handleServerResponse(message: ServerResponseMessage): void {\n const { requestId } = message.data;\n\n const request = this.#pendingRequests.get(requestId);\n if (!request) {\n return;\n }\n\n this.#pendingRequests.delete(requestId);\n clearTimeout(request.timeout);\n\n // Check if the response indicates failure\n if (message.data.failed && message.data.failed.length > 0) {\n request.reject(\n new Error(`Request failed: ${message.data.failed.join(', ')}`),\n );\n } else {\n request.resolve(message.data);\n }\n }\n\n /**\n * Handles messages with channel properties by triggering channel callbacks\n *\n * @param message - The message with channel property to handle\n */\n #handleChannelMessage(message: ServerNotificationMessage): void {\n this.#channelCallbacks.get(message.channel)?.callback(message);\n }\n\n /**\n * Handles server notifications with subscription IDs\n *\n * @param message - The server notification message to handle\n * @returns True if the message was handled, false if it should fall through to channel handling\n */\n #handleSubscriptionNotification(message: ServerNotificationMessage): boolean {\n const { subscriptionId, timestamp, channel } = message;\n\n // Only handle if subscriptionId is defined and not null (allows \"0\" as valid ID)\n if (subscriptionId === null || subscriptionId === undefined) {\n return false;\n }\n\n const subscription = this.#subscriptions.get(subscriptionId);\n if (!subscription?.callback) {\n return false;\n }\n\n // Calculate notification latency: time from server sent to client received\n const receivedAt = Date.now();\n const latency = receivedAt - timestamp;\n\n // Trace notification processing with latency data\n // Use stored channelType instead of parsing each time\n // Promise result intentionally not awaited\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.#trace(\n {\n name: `${SERVICE_NAME} Notification`,\n data: {\n channel,\n latency_ms: latency,\n subscriptionId,\n },\n tags: {\n service: SERVICE_NAME,\n notification_type: subscription.channelType,\n },\n },\n () => {\n subscription.callback?.(message);\n },\n );\n return true;\n }\n\n /**\n * Parse WebSocket message data\n *\n * @param data - The raw message data to parse\n * @returns Parsed message\n */\n #parseMessage(data: string): WebSocketMessage {\n return JSON.parse(data);\n }\n\n // =============================================================================\n // 5. EVENT HANDLERS (PRIVATE)\n // =============================================================================\n\n /**\n * Handles WebSocket errors\n *\n * @param _error - Error that occurred (unused)\n */\n #handleError(_error: Error): void {\n // Placeholder for future error handling logic\n }\n\n // =============================================================================\n // 6. STATE MANAGEMENT (PRIVATE)\n // =============================================================================\n\n /**\n * Schedules a connection attempt with exponential backoff and jitter\n *\n * This method is used for automatic reconnection with Cockatiel's exponential backoff:\n * - Prevents duplicate reconnection timers (idempotent)\n * - Applies exponential backoff with jitter based on previous failures\n * - Jitter uses decorrelated formula to prevent thundering herd problem\n * - Used ONLY for automatic retries, not user-initiated actions\n *\n * Call this from:\n * - connect() catch block (on connection failure)\n * - ws.onclose handler (on unexpected disconnect)\n *\n * For user-initiated actions (sign in, unlock), call connect() directly instead.\n *\n * If a reconnect is already scheduled, this is a no-op to prevent:\n * - Orphaned timers (memory leak)\n * - Inflated reconnect attempts counter\n * - Prematurely long delays\n */\n #scheduleReconnect(): void {\n // If a reconnect is already scheduled, don't schedule another one\n if (this.#reconnectTimer) {\n return;\n }\n\n // Increment attempts BEFORE calculating delay so backoff grows properly\n this.#reconnectAttempts += 1;\n\n // Use Cockatiel's exponential backoff to get delay with jitter\n const delay = this.#backoff.duration;\n\n // Progress to next backoff state for future reconnect attempts\n // Pass attempt number as context (though ExponentialBackoff doesn't use it)\n this.#backoff = this.#backoff.next({ attempt: this.#reconnectAttempts });\n\n log('Scheduling reconnect', {\n attempt: this.#reconnectAttempts,\n delay_ms: delay,\n });\n\n this.#reconnectTimer = setTimeout(() => {\n // Clear timer reference first\n this.#reconnectTimer = null;\n\n // Check if connection is still enabled before reconnecting\n if (this.#isEnabled && !this.#isEnabled()) {\n this.#reconnectAttempts = 0;\n // Create new backoff sequence when disabled\n this.#newBackoff();\n return;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.connect();\n }, delay);\n }\n\n /**\n * Creates a new exponential backoff sequence\n */\n #newBackoff(): void {\n this.#backoff = new ExponentialBackoff({\n initialDelay: this.#options.reconnectDelay,\n maxDelay: this.#options.maxReconnectDelay,\n }).next();\n }\n\n #clearTimers(): void {\n if (this.#reconnectTimer) {\n clearTimeout(this.#reconnectTimer);\n this.#reconnectTimer = null;\n }\n if (this.#connectionTimeout) {\n clearTimeout(this.#connectionTimeout);\n this.#connectionTimeout = null;\n }\n if (this.#stableConnectionTimer) {\n clearTimeout(this.#stableConnectionTimer);\n this.#stableConnectionTimer = null;\n }\n }\n\n /**\n * Clears all pending requests and rejects them with the given error\n *\n * @param error - Error to reject with\n */\n #clearPendingRequests(error: Error): void {\n for (const [, request] of this.#pendingRequests) {\n clearTimeout(request.timeout);\n request.reject(error);\n }\n this.#pendingRequests.clear();\n }\n\n /**\n * Clears all active subscriptions\n */\n #clearSubscriptions(): void {\n this.#subscriptions.clear();\n }\n\n /**\n * Sets the connection state and emits state change events\n *\n * @param newState - The new WebSocket state\n */\n #setState(newState: WebSocketState): void {\n const oldState = this.#state;\n this.#state = newState;\n\n if (oldState !== newState) {\n // Publish connection state change event\n // Messenger handles listener errors internally, no need for try-catch\n this.#messenger.publish(\n 'BackendWebSocketService:connectionStateChanged',\n this.getConnectionInfo(),\n );\n }\n }\n}\n"]}
|