@metamask/core-backend 1.0.1 → 2.1.0

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.
@@ -9,7 +9,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _BackendWebSocketService_instances, _BackendWebSocketService_messenger, _BackendWebSocketService_options, _BackendWebSocketService_isEnabled, _BackendWebSocketService_ws, _BackendWebSocketService_state, _BackendWebSocketService_reconnectAttempts, _BackendWebSocketService_reconnectTimer, _BackendWebSocketService_connectionTimeout, _BackendWebSocketService_connectionPromise, _BackendWebSocketService_pendingRequests, _BackendWebSocketService_connectedAt, _BackendWebSocketService_subscriptions, _BackendWebSocketService_channelCallbacks, _BackendWebSocketService_setupAuthentication, _BackendWebSocketService_buildAuthenticatedUrl, _BackendWebSocketService_establishConnection, _BackendWebSocketService_handleMessage, _BackendWebSocketService_isServerResponse, _BackendWebSocketService_isSubscriptionNotification, _BackendWebSocketService_isChannelMessage, _BackendWebSocketService_handleServerResponse, _BackendWebSocketService_handleChannelMessage, _BackendWebSocketService_handleSubscriptionNotification, _BackendWebSocketService_parseMessage, _BackendWebSocketService_handleClose, _BackendWebSocketService_handleError, _BackendWebSocketService_scheduleReconnect, _BackendWebSocketService_clearTimers, _BackendWebSocketService_clearPendingRequests, _BackendWebSocketService_clearSubscriptions, _BackendWebSocketService_setState, _BackendWebSocketService_shouldReconnectOnClose;
12
+ var _BackendWebSocketService_instances, _BackendWebSocketService_messenger, _BackendWebSocketService_options, _BackendWebSocketService_isEnabled, _BackendWebSocketService_trace, _BackendWebSocketService_ws, _BackendWebSocketService_state, _BackendWebSocketService_reconnectAttempts, _BackendWebSocketService_reconnectTimer, _BackendWebSocketService_connectionTimeout, _BackendWebSocketService_connectionPromise, _BackendWebSocketService_pendingRequests, _BackendWebSocketService_connectedAt, _BackendWebSocketService_manualDisconnect, _BackendWebSocketService_subscriptions, _BackendWebSocketService_channelCallbacks, _BackendWebSocketService_subscribeEvents, _BackendWebSocketService_buildAuthenticatedUrl, _BackendWebSocketService_establishConnection, _BackendWebSocketService_handleMessage, _BackendWebSocketService_isServerResponse, _BackendWebSocketService_isSubscriptionNotification, _BackendWebSocketService_isChannelMessage, _BackendWebSocketService_handleServerResponse, _BackendWebSocketService_handleChannelMessage, _BackendWebSocketService_handleSubscriptionNotification, _BackendWebSocketService_parseMessage, _BackendWebSocketService_handleClose, _BackendWebSocketService_handleError, _BackendWebSocketService_scheduleReconnect, _BackendWebSocketService_clearTimers, _BackendWebSocketService_clearPendingRequests, _BackendWebSocketService_clearSubscriptions, _BackendWebSocketService_setState;
13
13
  import { getErrorMessage } from "@metamask/utils";
14
14
  import { v4 as uuidV4 } from "uuid";
15
15
  import { projectLogger, createModuleLogger } from "./logger.mjs";
@@ -105,18 +105,24 @@ export var WebSocketEventType;
105
105
  /**
106
106
  * WebSocket Service with automatic reconnection, session management and direct callback routing
107
107
  *
108
+ * Connection Management:
109
+ * - Automatically subscribes to AuthenticationController:stateChange (sign in/out)
110
+ * - Automatically subscribes to KeyringController:lock/unlock events
111
+ * - Idempotent connect() function safe for multiple rapid calls
112
+ * - Auto-reconnects on unexpected disconnects (manualDisconnect = false)
113
+ *
114
+ * Platform Responsibilities:
115
+ * - Call connect() when app opens/foregrounds
116
+ * - Call disconnect() when app closes/backgrounds
117
+ * - Provide isEnabled() callback (feature flag)
118
+ * - Call destroy() on app termination
119
+ *
108
120
  * Real-Time Performance Optimizations:
109
121
  * - Fast path message routing (zero allocations)
110
122
  * - Production mode removes try-catch overhead
111
123
  * - Optimized JSON parsing with fail-fast
112
124
  * - Direct callback routing bypasses event emitters
113
125
  * - Memory cleanup and resource management
114
- *
115
- * Mobile Integration:
116
- * Mobile apps should handle lifecycle events (background/foreground) by:
117
- * 1. Calling disconnect() when app goes to background
118
- * 2. Calling connect() when app returns to foreground
119
- * 3. Calling destroy() on app termination
120
126
  */
121
127
  export class BackendWebSocketService {
122
128
  // =============================================================================
@@ -136,6 +142,7 @@ export class BackendWebSocketService {
136
142
  _BackendWebSocketService_messenger.set(this, void 0);
137
143
  _BackendWebSocketService_options.set(this, void 0);
138
144
  _BackendWebSocketService_isEnabled.set(this, void 0);
145
+ _BackendWebSocketService_trace.set(this, void 0);
139
146
  _BackendWebSocketService_ws.set(this, void 0);
140
147
  _BackendWebSocketService_state.set(this, WebSocketState.DISCONNECTED);
141
148
  _BackendWebSocketService_reconnectAttempts.set(this, 0);
@@ -144,7 +151,9 @@ export class BackendWebSocketService {
144
151
  // Track the current connection promise to handle concurrent connection attempts
145
152
  _BackendWebSocketService_connectionPromise.set(this, null);
146
153
  _BackendWebSocketService_pendingRequests.set(this, new Map());
147
- _BackendWebSocketService_connectedAt.set(this, null);
154
+ _BackendWebSocketService_connectedAt.set(this, 0);
155
+ // Track manual disconnects to prevent automatic reconnection
156
+ _BackendWebSocketService_manualDisconnect.set(this, false);
148
157
  // Simplified subscription storage (single flat map)
149
158
  // Key: subscription ID string (e.g., 'sub_abc123def456')
150
159
  // Value: WebSocketSubscription object with channels, callback and metadata
@@ -155,6 +164,10 @@ export class BackendWebSocketService {
155
164
  _BackendWebSocketService_channelCallbacks.set(this, new Map());
156
165
  __classPrivateFieldSet(this, _BackendWebSocketService_messenger, options.messenger, "f");
157
166
  __classPrivateFieldSet(this, _BackendWebSocketService_isEnabled, options.isEnabled, "f");
167
+ // Default to no-op trace function to keep core platform-agnostic
168
+ __classPrivateFieldSet(this, _BackendWebSocketService_trace, options.traceFn ??
169
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
170
+ ((_request, fn) => fn?.()), "f");
158
171
  __classPrivateFieldSet(this, _BackendWebSocketService_options, {
159
172
  url: options.url,
160
173
  timeout: options.timeout ?? 10000,
@@ -162,8 +175,8 @@ export class BackendWebSocketService {
162
175
  maxReconnectDelay: options.maxReconnectDelay ?? 5000,
163
176
  requestTimeout: options.requestTimeout ?? 30000,
164
177
  }, "f");
165
- // Setup authentication (always enabled)
166
- __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setupAuthentication).call(this);
178
+ // Subscribe to authentication and keyring controller events
179
+ __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_subscribeEvents).call(this);
167
180
  // Register action handlers using the method actions pattern
168
181
  __classPrivateFieldGet(this, _BackendWebSocketService_messenger, "f").registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
169
182
  }
@@ -173,18 +186,23 @@ export class BackendWebSocketService {
173
186
  /**
174
187
  * Establishes WebSocket connection with smart reconnection behavior
175
188
  *
176
- * Simplified Priority System (using AuthenticationController):
177
- * 1. App closed/backgrounded Stop all attempts (save resources)
178
- * 2. User not signed in (wallet locked OR not authenticated) → Keep retrying
179
- * 3. User signed in (wallet unlocked + authenticated) → Connect successfully
189
+ * Connection Requirements (all must be true):
190
+ * 1. Feature enabled (isEnabled() = true)
191
+ * 2. Wallet unlocked (checked by getBearerToken)
192
+ * 3. User signed in (checked by getBearerToken)
193
+ *
194
+ * Platform code should call this when app opens/foregrounds.
195
+ * Automatically called on KeyringController:unlock event.
180
196
  *
181
197
  * @returns Promise that resolves when connection is established
182
198
  */
183
199
  async connect() {
184
- // Priority 1: Check if connection is enabled via callback (app lifecycle check)
185
- // If app is closed/backgrounded, stop all connection attempts to save resources
200
+ // Reset manual disconnect flag when explicitly connecting
201
+ __classPrivateFieldSet(this, _BackendWebSocketService_manualDisconnect, false, "f");
202
+ // Priority 1: Check if feature is enabled via callback (feature flag check)
203
+ // If feature is disabled, stop all connection attempts
186
204
  if (__classPrivateFieldGet(this, _BackendWebSocketService_isEnabled, "f") && !__classPrivateFieldGet(this, _BackendWebSocketService_isEnabled, "f").call(this)) {
187
- // Clear any pending reconnection attempts since app is disabled
205
+ // Clear any pending reconnection attempts since feature is disabled
188
206
  __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_clearTimers).call(this);
189
207
  __classPrivateFieldSet(this, _BackendWebSocketService_reconnectAttempts, 0, "f");
190
208
  return;
@@ -194,39 +212,45 @@ export class BackendWebSocketService {
194
212
  return;
195
213
  }
196
214
  // If already connecting, wait for the existing connection attempt to complete
197
- if (__classPrivateFieldGet(this, _BackendWebSocketService_state, "f") === WebSocketState.CONNECTING && __classPrivateFieldGet(this, _BackendWebSocketService_connectionPromise, "f")) {
215
+ if (__classPrivateFieldGet(this, _BackendWebSocketService_connectionPromise, "f")) {
198
216
  await __classPrivateFieldGet(this, _BackendWebSocketService_connectionPromise, "f");
199
217
  return;
200
218
  }
201
- // Priority 2: Check authentication requirements (simplified - just check if signed in)
202
- let bearerToken;
203
- try {
204
- // AuthenticationController.getBearerToken() handles wallet unlock checks internally
205
- const token = await __classPrivateFieldGet(this, _BackendWebSocketService_messenger, "f").call('AuthenticationController:getBearerToken');
206
- if (!token) {
219
+ // Create and store the connection promise IMMEDIATELY (before any async operations)
220
+ // This ensures subsequent connect() calls will wait for this promise instead of creating new connections
221
+ __classPrivateFieldSet(this, _BackendWebSocketService_connectionPromise, (async () => {
222
+ // Priority 2: Check authentication requirements (signed in)
223
+ let bearerToken;
224
+ try {
225
+ const token = await __classPrivateFieldGet(this, _BackendWebSocketService_messenger, "f").call('AuthenticationController:getBearerToken');
226
+ if (!token) {
227
+ __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_scheduleReconnect).call(this);
228
+ throw new Error('Authentication required: user not signed in');
229
+ }
230
+ bearerToken = token;
231
+ }
232
+ catch (error) {
233
+ log('Failed to check authentication requirements', { error });
234
+ // Can't connect - schedule retry
207
235
  __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_scheduleReconnect).call(this);
208
- return;
236
+ throw error;
209
237
  }
210
- bearerToken = token;
211
- }
212
- catch (error) {
213
- log('Failed to check authentication requirements', { error });
214
- // If we can't connect for ANY reason, schedule a retry
215
- __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_scheduleReconnect).call(this);
216
- return;
217
- }
218
- __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.CONNECTING);
219
- // Create and store the connection promise
220
- __classPrivateFieldSet(this, _BackendWebSocketService_connectionPromise, __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_establishConnection).call(this, bearerToken), "f");
238
+ __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.CONNECTING);
239
+ // Establish the actual WebSocket connection
240
+ try {
241
+ await __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_establishConnection).call(this, bearerToken);
242
+ }
243
+ catch (error) {
244
+ const errorMessage = getErrorMessage(error);
245
+ log('Connection attempt failed', { errorMessage, error });
246
+ __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.ERROR);
247
+ // Rethrow to propagate error to caller
248
+ throw error;
249
+ }
250
+ })(), "f");
221
251
  try {
222
252
  await __classPrivateFieldGet(this, _BackendWebSocketService_connectionPromise, "f");
223
253
  }
224
- catch (error) {
225
- const errorMessage = getErrorMessage(error);
226
- log('Connection attempt failed', { errorMessage, error });
227
- __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.ERROR);
228
- throw new Error(`Failed to connect to WebSocket: ${errorMessage}`);
229
- }
230
254
  finally {
231
255
  // Clear the connection promise when done (success or failure)
232
256
  __classPrivateFieldSet(this, _BackendWebSocketService_connectionPromise, null, "f");
@@ -242,6 +266,8 @@ export class BackendWebSocketService {
242
266
  __classPrivateFieldGet(this, _BackendWebSocketService_state, "f") === WebSocketState.DISCONNECTING) {
243
267
  return;
244
268
  }
269
+ // Mark this as a manual disconnect to prevent automatic reconnection
270
+ __classPrivateFieldSet(this, _BackendWebSocketService_manualDisconnect, true, "f");
245
271
  __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.DISCONNECTING);
246
272
  __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_clearTimers).call(this);
247
273
  __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_clearPendingRequests).call(this, new Error('WebSocket disconnected'));
@@ -250,7 +276,6 @@ export class BackendWebSocketService {
250
276
  if (__classPrivateFieldGet(this, _BackendWebSocketService_ws, "f")) {
251
277
  __classPrivateFieldGet(this, _BackendWebSocketService_ws, "f").close(1000, 'Normal closure');
252
278
  }
253
- __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.DISCONNECTED);
254
279
  log('WebSocket manually disconnected');
255
280
  }
256
281
  /**
@@ -351,8 +376,12 @@ export class BackendWebSocketService {
351
376
  return {
352
377
  state: __classPrivateFieldGet(this, _BackendWebSocketService_state, "f"),
353
378
  url: __classPrivateFieldGet(this, _BackendWebSocketService_options, "f").url,
379
+ timeout: __classPrivateFieldGet(this, _BackendWebSocketService_options, "f").timeout,
380
+ reconnectDelay: __classPrivateFieldGet(this, _BackendWebSocketService_options, "f").reconnectDelay,
381
+ maxReconnectDelay: __classPrivateFieldGet(this, _BackendWebSocketService_options, "f").maxReconnectDelay,
382
+ requestTimeout: __classPrivateFieldGet(this, _BackendWebSocketService_options, "f").requestTimeout,
354
383
  reconnectAttempts: __classPrivateFieldGet(this, _BackendWebSocketService_reconnectAttempts, "f"),
355
- connectedAt: __classPrivateFieldGet(this, _BackendWebSocketService_connectedAt, "f") ?? undefined,
384
+ connectedAt: __classPrivateFieldGet(this, _BackendWebSocketService_connectedAt, "f"),
356
385
  };
357
386
  }
358
387
  /**
@@ -368,6 +397,7 @@ export class BackendWebSocketService {
368
397
  matchingSubscriptions.push({
369
398
  subscriptionId,
370
399
  channels: subscription.channels,
400
+ channelType: subscription.channelType,
371
401
  unsubscribe: subscription.unsubscribe,
372
402
  });
373
403
  }
@@ -403,6 +433,7 @@ export class BackendWebSocketService {
403
433
  matchingSubscriptions.push({
404
434
  subscriptionId,
405
435
  channels: subscription.channels,
436
+ channelType: subscription.channelType,
406
437
  unsubscribe: subscription.unsubscribe,
407
438
  });
408
439
  }
@@ -496,6 +527,8 @@ export class BackendWebSocketService {
496
527
  if (__classPrivateFieldGet(this, _BackendWebSocketService_ws, "f") && __classPrivateFieldGet(this, _BackendWebSocketService_ws, "f").readyState === WebSocket.OPEN) {
497
528
  __classPrivateFieldGet(this, _BackendWebSocketService_ws, "f").close(1000, 'Service cleanup');
498
529
  }
530
+ // Set state to disconnected immediately
531
+ __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.DISCONNECTED);
499
532
  }
500
533
  /**
501
534
  * Create and manage a subscription with server-side registration (recommended for most use cases)
@@ -524,6 +557,7 @@ export class BackendWebSocketService {
524
557
  * @param options.channels - Array of channel names to subscribe to
525
558
  * @param options.callback - Callback function for handling notifications
526
559
  * @param options.requestId - Optional request ID for testing (will generate UUID if not provided)
560
+ * @param options.channelType - Channel type identifier
527
561
  * @returns Subscription object with unsubscribe method
528
562
  *
529
563
  * @example
@@ -543,7 +577,7 @@ export class BackendWebSocketService {
543
577
  * @see addChannelCallback for local callbacks without server-side subscription
544
578
  */
545
579
  async subscribe(options) {
546
- const { channels, callback, requestId } = options;
580
+ const { channels, channelType, callback, requestId } = options;
547
581
  if (__classPrivateFieldGet(this, _BackendWebSocketService_state, "f") !== WebSocketState.CONNECTED) {
548
582
  throw new Error(`Cannot create subscription(s) ${channels.join(', ')}: WebSocket is ${__classPrivateFieldGet(this, _BackendWebSocketService_state, "f")}`);
549
583
  }
@@ -556,10 +590,6 @@ export class BackendWebSocketService {
556
590
  throw new Error('Invalid subscription response: missing subscription ID');
557
591
  }
558
592
  const { subscriptionId } = subscriptionResponse;
559
- // Check for failures
560
- if (subscriptionResponse.failed && subscriptionResponse.failed.length > 0) {
561
- throw new Error(`Subscription failed for channels: ${subscriptionResponse.failed.join(', ')}`);
562
- }
563
593
  // Create unsubscribe function
564
594
  const unsubscribe = async (unsubRequestId) => {
565
595
  // Send unsubscribe request first
@@ -577,45 +607,42 @@ export class BackendWebSocketService {
577
607
  const subscription = {
578
608
  subscriptionId,
579
609
  channels: [...channels],
610
+ channelType,
580
611
  unsubscribe,
581
612
  };
582
613
  // Store subscription with subscription ID as key
583
614
  __classPrivateFieldGet(this, _BackendWebSocketService_subscriptions, "f").set(subscriptionId, {
584
615
  subscriptionId,
585
616
  channels: [...channels],
617
+ channelType,
586
618
  callback,
587
619
  unsubscribe,
588
620
  });
589
621
  return subscription;
590
622
  }
591
623
  }
592
- _BackendWebSocketService_messenger = new WeakMap(), _BackendWebSocketService_options = new WeakMap(), _BackendWebSocketService_isEnabled = new WeakMap(), _BackendWebSocketService_ws = new WeakMap(), _BackendWebSocketService_state = new WeakMap(), _BackendWebSocketService_reconnectAttempts = new WeakMap(), _BackendWebSocketService_reconnectTimer = new WeakMap(), _BackendWebSocketService_connectionTimeout = new WeakMap(), _BackendWebSocketService_connectionPromise = new WeakMap(), _BackendWebSocketService_pendingRequests = new WeakMap(), _BackendWebSocketService_connectedAt = new WeakMap(), _BackendWebSocketService_subscriptions = new WeakMap(), _BackendWebSocketService_channelCallbacks = new WeakMap(), _BackendWebSocketService_instances = new WeakSet(), _BackendWebSocketService_setupAuthentication = function _BackendWebSocketService_setupAuthentication() {
593
- try {
594
- // Subscribe to authentication state changes - this includes wallet unlock state
595
- // AuthenticationController can only be signed in if wallet is unlocked
596
- // Using selector to only listen for isSignedIn property changes for better performance
597
- __classPrivateFieldGet(this, _BackendWebSocketService_messenger, "f").subscribe('AuthenticationController:stateChange', (isSignedIn) => {
598
- if (isSignedIn) {
599
- // User signed in (wallet unlocked + authenticated) - try to connect
600
- // Clear any pending reconnection timer since we're attempting connection
601
- __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_clearTimers).call(this);
602
- this.connect().catch((error) => {
603
- log('Failed to connect after sign-in', { error });
604
- });
605
- }
606
- else {
607
- // User signed out (wallet locked OR signed out) - disconnect and stop reconnection attempts
608
- __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_clearTimers).call(this);
609
- __classPrivateFieldSet(this, _BackendWebSocketService_reconnectAttempts, 0, "f");
610
- this.disconnect().catch((error) => {
611
- log('Failed to disconnect after sign-out', { error });
612
- });
613
- }
614
- }, (state) => state.isSignedIn);
615
- }
616
- catch (error) {
617
- throw new Error(`Authentication setup failed: ${getErrorMessage(error)}`);
618
- }
624
+ _BackendWebSocketService_messenger = new WeakMap(), _BackendWebSocketService_options = new WeakMap(), _BackendWebSocketService_isEnabled = new WeakMap(), _BackendWebSocketService_trace = new WeakMap(), _BackendWebSocketService_ws = new WeakMap(), _BackendWebSocketService_state = new WeakMap(), _BackendWebSocketService_reconnectAttempts = new WeakMap(), _BackendWebSocketService_reconnectTimer = new WeakMap(), _BackendWebSocketService_connectionTimeout = new WeakMap(), _BackendWebSocketService_connectionPromise = new WeakMap(), _BackendWebSocketService_pendingRequests = new WeakMap(), _BackendWebSocketService_connectedAt = new WeakMap(), _BackendWebSocketService_manualDisconnect = new WeakMap(), _BackendWebSocketService_subscriptions = new WeakMap(), _BackendWebSocketService_channelCallbacks = new WeakMap(), _BackendWebSocketService_instances = new WeakSet(), _BackendWebSocketService_subscribeEvents = function _BackendWebSocketService_subscribeEvents() {
625
+ // Subscribe to authentication state changes (sign in/out)
626
+ __classPrivateFieldGet(this, _BackendWebSocketService_messenger, "f").subscribe('AuthenticationController:stateChange', (state) => {
627
+ if (state.isSignedIn) {
628
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
629
+ this.connect();
630
+ }
631
+ else {
632
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
633
+ this.disconnect();
634
+ }
635
+ }, (state) => ({ isSignedIn: state.isSignedIn }));
636
+ // Subscribe to wallet unlock event
637
+ __classPrivateFieldGet(this, _BackendWebSocketService_messenger, "f").subscribe('KeyringController:unlock', () => {
638
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
639
+ this.connect();
640
+ });
641
+ // Subscribe to wallet lock event
642
+ __classPrivateFieldGet(this, _BackendWebSocketService_messenger, "f").subscribe('KeyringController:lock', () => {
643
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
644
+ this.disconnect();
645
+ });
619
646
  }, _BackendWebSocketService_buildAuthenticatedUrl = function _BackendWebSocketService_buildAuthenticatedUrl(bearerToken) {
620
647
  const baseUrl = __classPrivateFieldGet(this, _BackendWebSocketService_options, "f").url;
621
648
  // Add token as query parameter to the WebSocket URL
@@ -631,6 +658,7 @@ _BackendWebSocketService_messenger = new WeakMap(), _BackendWebSocketService_opt
631
658
  */
632
659
  async function _BackendWebSocketService_establishConnection(bearerToken) {
633
660
  const wsUrl = __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_buildAuthenticatedUrl).call(this, bearerToken);
661
+ const connectionStartTime = Date.now();
634
662
  return new Promise((resolve, reject) => {
635
663
  const ws = new WebSocket(wsUrl);
636
664
  __classPrivateFieldSet(this, _BackendWebSocketService_connectionTimeout, setTimeout(() => {
@@ -638,40 +666,47 @@ async function _BackendWebSocketService_establishConnection(bearerToken) {
638
666
  timeout: __classPrivateFieldGet(this, _BackendWebSocketService_options, "f").timeout,
639
667
  });
640
668
  ws.close();
641
- reject(new Error(`Connection timeout after ${__classPrivateFieldGet(this, _BackendWebSocketService_options, "f").timeout}ms`));
669
+ reject(new Error(`Failed to connect to WebSocket: Connection timeout after ${__classPrivateFieldGet(this, _BackendWebSocketService_options, "f").timeout}ms`));
642
670
  }, __classPrivateFieldGet(this, _BackendWebSocketService_options, "f").timeout), "f");
643
671
  ws.onopen = () => {
644
672
  if (__classPrivateFieldGet(this, _BackendWebSocketService_connectionTimeout, "f")) {
645
673
  clearTimeout(__classPrivateFieldGet(this, _BackendWebSocketService_connectionTimeout, "f"));
646
674
  __classPrivateFieldSet(this, _BackendWebSocketService_connectionTimeout, null, "f");
647
675
  }
648
- __classPrivateFieldSet(this, _BackendWebSocketService_ws, ws, "f");
649
- __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.CONNECTED);
650
- __classPrivateFieldSet(this, _BackendWebSocketService_connectedAt, Date.now(), "f");
651
- // Reset reconnect attempts on successful connection
652
- __classPrivateFieldSet(this, _BackendWebSocketService_reconnectAttempts, 0, "f");
653
- resolve();
676
+ // Calculate connection latency
677
+ const connectionLatency = Date.now() - connectionStartTime;
678
+ // Trace successful connection with latency
679
+ __classPrivateFieldGet(this, _BackendWebSocketService_trace, "f").call(this, {
680
+ name: `${SERVICE_NAME} Connection`,
681
+ data: {
682
+ reconnectAttempt: __classPrivateFieldGet(this, _BackendWebSocketService_reconnectAttempts, "f"),
683
+ latency_ms: connectionLatency,
684
+ },
685
+ tags: {
686
+ service: SERVICE_NAME,
687
+ },
688
+ }, () => {
689
+ __classPrivateFieldSet(this, _BackendWebSocketService_ws, ws, "f");
690
+ __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.CONNECTED);
691
+ __classPrivateFieldSet(this, _BackendWebSocketService_connectedAt, Date.now(), "f");
692
+ // Reset reconnect attempts on successful connection
693
+ __classPrivateFieldSet(this, _BackendWebSocketService_reconnectAttempts, 0, "f");
694
+ resolve();
695
+ });
654
696
  };
655
697
  ws.onerror = (event) => {
656
698
  log('WebSocket onerror event triggered', { event });
657
- if (__classPrivateFieldGet(this, _BackendWebSocketService_state, "f") === WebSocketState.CONNECTING) {
658
- // Handle connection-phase errors
659
- if (__classPrivateFieldGet(this, _BackendWebSocketService_connectionTimeout, "f")) {
660
- clearTimeout(__classPrivateFieldGet(this, _BackendWebSocketService_connectionTimeout, "f"));
661
- __classPrivateFieldSet(this, _BackendWebSocketService_connectionTimeout, null, "f");
662
- }
663
- const error = new Error(`WebSocket connection error to ${wsUrl}`);
664
- reject(error);
665
- }
666
- else {
667
- // Handle runtime errors
668
- __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_handleError).call(this, new Error(`WebSocket error: ${event.type}`));
699
+ if (__classPrivateFieldGet(this, _BackendWebSocketService_connectionTimeout, "f")) {
700
+ clearTimeout(__classPrivateFieldGet(this, _BackendWebSocketService_connectionTimeout, "f"));
701
+ __classPrivateFieldSet(this, _BackendWebSocketService_connectionTimeout, null, "f");
669
702
  }
703
+ const error = new Error(`WebSocket connection error to ${wsUrl}`);
704
+ reject(error);
670
705
  };
671
706
  ws.onclose = (event) => {
672
707
  log('WebSocket onclose event triggered', {
673
708
  code: event.code,
674
- reason: event.reason || 'none',
709
+ reason: event.reason,
675
710
  wasClean: event.wasClean,
676
711
  });
677
712
  if (__classPrivateFieldGet(this, _BackendWebSocketService_state, "f") === WebSocketState.CONNECTING) {
@@ -705,7 +740,8 @@ async function _BackendWebSocketService_establishConnection(bearerToken) {
705
740
  }
706
741
  // Handle subscription notifications with valid subscriptionId
707
742
  if (__classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_isSubscriptionNotification).call(this, message)) {
708
- const handled = __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_handleSubscriptionNotification).call(this, message);
743
+ const notificationMsg = message;
744
+ const handled = __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_handleSubscriptionNotification).call(this, notificationMsg);
709
745
  // If subscription notification wasn't handled (falsy subscriptionId), fall through to channel handling
710
746
  if (handled) {
711
747
  return;
@@ -713,7 +749,8 @@ async function _BackendWebSocketService_establishConnection(bearerToken) {
713
749
  }
714
750
  // Trigger channel callbacks for any message with a channel property
715
751
  if (__classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_isChannelMessage).call(this, message)) {
716
- __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_handleChannelMessage).call(this, message);
752
+ const channelMsg = message;
753
+ __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_handleChannelMessage).call(this, channelMsg);
717
754
  }
718
755
  }, _BackendWebSocketService_isServerResponse = function _BackendWebSocketService_isServerResponse(message) {
719
756
  return ('data' in message &&
@@ -743,58 +780,102 @@ async function _BackendWebSocketService_establishConnection(bearerToken) {
743
780
  if (__classPrivateFieldGet(this, _BackendWebSocketService_channelCallbacks, "f").size === 0) {
744
781
  return;
745
782
  }
746
- // Direct lookup for exact channel match
747
- __classPrivateFieldGet(this, _BackendWebSocketService_channelCallbacks, "f").get(message.channel)?.callback(message);
783
+ // Calculate notification latency: time from server sent to client received
784
+ const receivedAt = Date.now();
785
+ const latency = receivedAt - message.timestamp;
786
+ // Trace channel message processing with latency data
787
+ __classPrivateFieldGet(this, _BackendWebSocketService_trace, "f").call(this, {
788
+ name: `${SERVICE_NAME} Channel Message`,
789
+ data: {
790
+ latency_ms: latency,
791
+ event: message.event,
792
+ },
793
+ tags: {
794
+ service: SERVICE_NAME,
795
+ },
796
+ }, () => {
797
+ // Direct lookup for exact channel match
798
+ __classPrivateFieldGet(this, _BackendWebSocketService_channelCallbacks, "f").get(message.channel)?.callback(message);
799
+ });
748
800
  }, _BackendWebSocketService_handleSubscriptionNotification = function _BackendWebSocketService_handleSubscriptionNotification(message) {
749
- const { subscriptionId } = message;
801
+ const { subscriptionId, timestamp, channel } = message;
750
802
  // Only handle if subscriptionId is defined and not null (allows "0" as valid ID)
751
803
  if (subscriptionId !== null && subscriptionId !== undefined) {
752
- __classPrivateFieldGet(this, _BackendWebSocketService_subscriptions, "f").get(subscriptionId)?.callback?.(message);
804
+ const subscription = __classPrivateFieldGet(this, _BackendWebSocketService_subscriptions, "f").get(subscriptionId);
805
+ if (!subscription) {
806
+ return false;
807
+ }
808
+ // Calculate notification latency: time from server sent to client received
809
+ const receivedAt = Date.now();
810
+ const latency = receivedAt - timestamp;
811
+ // Trace notification processing wi th latency data
812
+ // Use stored channelType instead of parsing each time
813
+ __classPrivateFieldGet(this, _BackendWebSocketService_trace, "f").call(this, {
814
+ name: `${SERVICE_NAME} Notification`,
815
+ data: {
816
+ channel,
817
+ latency_ms: latency,
818
+ subscriptionId,
819
+ },
820
+ tags: {
821
+ service: SERVICE_NAME,
822
+ notification_type: subscription.channelType,
823
+ },
824
+ }, () => {
825
+ subscription.callback?.(message);
826
+ });
753
827
  return true;
754
828
  }
755
829
  return false;
756
830
  }, _BackendWebSocketService_parseMessage = function _BackendWebSocketService_parseMessage(data) {
757
831
  return JSON.parse(data);
758
832
  }, _BackendWebSocketService_handleClose = function _BackendWebSocketService_handleClose(event) {
833
+ // Calculate connection duration before we clear state
834
+ const connectionDuration = Date.now() - __classPrivateFieldGet(this, _BackendWebSocketService_connectedAt, "f");
759
835
  __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_clearTimers).call(this);
760
- __classPrivateFieldSet(this, _BackendWebSocketService_connectedAt, null, "f");
836
+ __classPrivateFieldSet(this, _BackendWebSocketService_connectedAt, 0, "f");
761
837
  // Clear any pending connection promise
762
838
  __classPrivateFieldSet(this, _BackendWebSocketService_connectionPromise, null, "f");
763
839
  // Clear subscriptions and pending requests on any disconnect
764
840
  // This ensures clean state for reconnection
765
841
  __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_clearPendingRequests).call(this, new Error('WebSocket connection closed'));
766
842
  __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_clearSubscriptions).call(this);
767
- if (__classPrivateFieldGet(this, _BackendWebSocketService_state, "f") === WebSocketState.DISCONNECTING) {
768
- // Manual disconnect
769
- __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.DISCONNECTED);
843
+ // Update state to disconnected
844
+ __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.DISCONNECTED);
845
+ // Check if this was a manual disconnect
846
+ if (__classPrivateFieldGet(this, _BackendWebSocketService_manualDisconnect, "f")) {
847
+ // Manual disconnect - don't reconnect
770
848
  return;
771
849
  }
772
- // For unexpected disconnects, update the state to reflect that we're disconnected
773
- __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.DISCONNECTED);
774
- // Check if we should attempt reconnection based on close code
775
- const shouldReconnect = __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_shouldReconnectOnClose).call(this, event.code);
776
- if (shouldReconnect) {
777
- log('Connection lost unexpectedly, will attempt reconnection', {
850
+ // Trace unexpected disconnect with details
851
+ __classPrivateFieldGet(this, _BackendWebSocketService_trace, "f").call(this, {
852
+ name: `${SERVICE_NAME} Disconnect`,
853
+ data: {
778
854
  code: event.code,
779
- });
780
- __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_scheduleReconnect).call(this);
781
- }
855
+ reason: event.reason || getCloseReason(event.code),
856
+ connectionDuration_ms: connectionDuration,
857
+ },
858
+ tags: {
859
+ service: SERVICE_NAME,
860
+ disconnect_type: 'unexpected',
861
+ },
862
+ }, () => {
863
+ // Empty trace callback - just measuring the event
864
+ });
865
+ // For any unexpected disconnects, attempt reconnection
866
+ // The manualDisconnect flag is the only gate - if it's false, we reconnect
867
+ __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_scheduleReconnect).call(this);
782
868
  }, _BackendWebSocketService_handleError = function _BackendWebSocketService_handleError(_error) {
783
869
  // Placeholder for future error handling logic
784
870
  }, _BackendWebSocketService_scheduleReconnect = function _BackendWebSocketService_scheduleReconnect() {
785
871
  __classPrivateFieldSet(this, _BackendWebSocketService_reconnectAttempts, __classPrivateFieldGet(this, _BackendWebSocketService_reconnectAttempts, "f") + 1, "f");
786
872
  const rawDelay = __classPrivateFieldGet(this, _BackendWebSocketService_options, "f").reconnectDelay * Math.pow(1.5, __classPrivateFieldGet(this, _BackendWebSocketService_reconnectAttempts, "f") - 1);
787
873
  const delay = Math.min(rawDelay, __classPrivateFieldGet(this, _BackendWebSocketService_options, "f").maxReconnectDelay);
788
- log('Scheduling reconnection attempt', {
789
- attempt: __classPrivateFieldGet(this, _BackendWebSocketService_reconnectAttempts, "f"),
790
- delayMs: delay,
791
- });
792
874
  __classPrivateFieldSet(this, _BackendWebSocketService_reconnectTimer, setTimeout(() => {
793
875
  // Clear timer reference first
794
876
  __classPrivateFieldSet(this, _BackendWebSocketService_reconnectTimer, null, "f");
795
877
  // Check if connection is still enabled before reconnecting
796
878
  if (__classPrivateFieldGet(this, _BackendWebSocketService_isEnabled, "f") && !__classPrivateFieldGet(this, _BackendWebSocketService_isEnabled, "f").call(this)) {
797
- log('Reconnection disabled by isEnabled - stopping all attempts');
798
879
  __classPrivateFieldSet(this, _BackendWebSocketService_reconnectAttempts, 0, "f");
799
880
  return;
800
881
  }
@@ -824,13 +905,9 @@ async function _BackendWebSocketService_establishConnection(bearerToken) {
824
905
  const oldState = __classPrivateFieldGet(this, _BackendWebSocketService_state, "f");
825
906
  __classPrivateFieldSet(this, _BackendWebSocketService_state, newState, "f");
826
907
  if (oldState !== newState) {
827
- log('WebSocket state changed', { oldState, newState });
828
908
  // Publish connection state change event
829
909
  // Messenger handles listener errors internally, no need for try-catch
830
910
  __classPrivateFieldGet(this, _BackendWebSocketService_messenger, "f").publish('BackendWebSocketService:connectionStateChanged', this.getConnectionInfo());
831
911
  }
832
- }, _BackendWebSocketService_shouldReconnectOnClose = function _BackendWebSocketService_shouldReconnectOnClose(code) {
833
- // Don't reconnect only on normal closure (manual disconnect)
834
- return code !== 1000;
835
912
  };
836
913
  //# sourceMappingURL=BackendWebSocketService.mjs.map