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