@metamask-previews/core-backend 1.0.1-preview-5f3688c1 → 1.0.1-preview-0189b42

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,8 @@ 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 ?? ((_request, fn) => fn?.()), "f");
158
169
  __classPrivateFieldSet(this, _BackendWebSocketService_options, {
159
170
  url: options.url,
160
171
  timeout: options.timeout ?? 10000,
@@ -162,8 +173,8 @@ export class BackendWebSocketService {
162
173
  maxReconnectDelay: options.maxReconnectDelay ?? 5000,
163
174
  requestTimeout: options.requestTimeout ?? 30000,
164
175
  }, "f");
165
- // Setup authentication (always enabled)
166
- __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setupAuthentication).call(this);
176
+ // Subscribe to authentication and keyring controller events
177
+ __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_subscribeEvents).call(this);
167
178
  // Register action handlers using the method actions pattern
168
179
  __classPrivateFieldGet(this, _BackendWebSocketService_messenger, "f").registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
169
180
  }
@@ -173,18 +184,23 @@ export class BackendWebSocketService {
173
184
  /**
174
185
  * Establishes WebSocket connection with smart reconnection behavior
175
186
  *
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
187
+ * Connection Requirements (all must be true):
188
+ * 1. Feature enabled (isEnabled() = true)
189
+ * 2. Wallet unlocked (checked by getBearerToken)
190
+ * 3. User signed in (checked by getBearerToken)
191
+ *
192
+ * Platform code should call this when app opens/foregrounds.
193
+ * Automatically called on KeyringController:unlock event.
180
194
  *
181
195
  * @returns Promise that resolves when connection is established
182
196
  */
183
197
  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
198
+ // Reset manual disconnect flag when explicitly connecting
199
+ __classPrivateFieldSet(this, _BackendWebSocketService_manualDisconnect, false, "f");
200
+ // Priority 1: Check if feature is enabled via callback (feature flag check)
201
+ // If feature is disabled, stop all connection attempts
186
202
  if (__classPrivateFieldGet(this, _BackendWebSocketService_isEnabled, "f") && !__classPrivateFieldGet(this, _BackendWebSocketService_isEnabled, "f").call(this)) {
187
- // Clear any pending reconnection attempts since app is disabled
203
+ // Clear any pending reconnection attempts since feature is disabled
188
204
  __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_clearTimers).call(this);
189
205
  __classPrivateFieldSet(this, _BackendWebSocketService_reconnectAttempts, 0, "f");
190
206
  return;
@@ -198,10 +214,9 @@ export class BackendWebSocketService {
198
214
  await __classPrivateFieldGet(this, _BackendWebSocketService_connectionPromise, "f");
199
215
  return;
200
216
  }
201
- // Priority 2: Check authentication requirements (simplified - just check if signed in)
217
+ // Priority 2: Check authentication requirements (signed in)
202
218
  let bearerToken;
203
219
  try {
204
- // AuthenticationController.getBearerToken() handles wallet unlock checks internally
205
220
  const token = await __classPrivateFieldGet(this, _BackendWebSocketService_messenger, "f").call('AuthenticationController:getBearerToken');
206
221
  if (!token) {
207
222
  __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_scheduleReconnect).call(this);
@@ -210,8 +225,10 @@ export class BackendWebSocketService {
210
225
  bearerToken = token;
211
226
  }
212
227
  catch (error) {
213
- log('Failed to check authentication requirements', { error });
214
- // If we can't connect for ANY reason, schedule a retry
228
+ log('Failed to get bearer token (wallet locked or not signed in)', {
229
+ error,
230
+ });
231
+ // Can't connect - schedule retry
215
232
  __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_scheduleReconnect).call(this);
216
233
  return;
217
234
  }
@@ -225,7 +242,8 @@ export class BackendWebSocketService {
225
242
  const errorMessage = getErrorMessage(error);
226
243
  log('Connection attempt failed', { errorMessage, error });
227
244
  __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.ERROR);
228
- throw new Error(`Failed to connect to WebSocket: ${errorMessage}`);
245
+ // Rethrow to propagate error to caller
246
+ throw error;
229
247
  }
230
248
  finally {
231
249
  // Clear the connection promise when done (success or failure)
@@ -242,6 +260,8 @@ export class BackendWebSocketService {
242
260
  __classPrivateFieldGet(this, _BackendWebSocketService_state, "f") === WebSocketState.DISCONNECTING) {
243
261
  return;
244
262
  }
263
+ // Mark this as a manual disconnect to prevent automatic reconnection
264
+ __classPrivateFieldSet(this, _BackendWebSocketService_manualDisconnect, true, "f");
245
265
  __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.DISCONNECTING);
246
266
  __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_clearTimers).call(this);
247
267
  __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_clearPendingRequests).call(this, new Error('WebSocket disconnected'));
@@ -250,7 +270,6 @@ export class BackendWebSocketService {
250
270
  if (__classPrivateFieldGet(this, _BackendWebSocketService_ws, "f")) {
251
271
  __classPrivateFieldGet(this, _BackendWebSocketService_ws, "f").close(1000, 'Normal closure');
252
272
  }
253
- __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.DISCONNECTED);
254
273
  log('WebSocket manually disconnected');
255
274
  }
256
275
  /**
@@ -352,7 +371,7 @@ export class BackendWebSocketService {
352
371
  state: __classPrivateFieldGet(this, _BackendWebSocketService_state, "f"),
353
372
  url: __classPrivateFieldGet(this, _BackendWebSocketService_options, "f").url,
354
373
  reconnectAttempts: __classPrivateFieldGet(this, _BackendWebSocketService_reconnectAttempts, "f"),
355
- connectedAt: __classPrivateFieldGet(this, _BackendWebSocketService_connectedAt, "f") ?? undefined,
374
+ connectedAt: __classPrivateFieldGet(this, _BackendWebSocketService_connectedAt, "f"),
356
375
  };
357
376
  }
358
377
  /**
@@ -368,6 +387,7 @@ export class BackendWebSocketService {
368
387
  matchingSubscriptions.push({
369
388
  subscriptionId,
370
389
  channels: subscription.channels,
390
+ channelType: subscription.channelType,
371
391
  unsubscribe: subscription.unsubscribe,
372
392
  });
373
393
  }
@@ -403,6 +423,7 @@ export class BackendWebSocketService {
403
423
  matchingSubscriptions.push({
404
424
  subscriptionId,
405
425
  channels: subscription.channels,
426
+ channelType: subscription.channelType,
406
427
  unsubscribe: subscription.unsubscribe,
407
428
  });
408
429
  }
@@ -496,6 +517,8 @@ export class BackendWebSocketService {
496
517
  if (__classPrivateFieldGet(this, _BackendWebSocketService_ws, "f") && __classPrivateFieldGet(this, _BackendWebSocketService_ws, "f").readyState === WebSocket.OPEN) {
497
518
  __classPrivateFieldGet(this, _BackendWebSocketService_ws, "f").close(1000, 'Service cleanup');
498
519
  }
520
+ // Set state to disconnected immediately
521
+ __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.DISCONNECTED);
499
522
  }
500
523
  /**
501
524
  * Create and manage a subscription with server-side registration (recommended for most use cases)
@@ -524,6 +547,7 @@ export class BackendWebSocketService {
524
547
  * @param options.channels - Array of channel names to subscribe to
525
548
  * @param options.callback - Callback function for handling notifications
526
549
  * @param options.requestId - Optional request ID for testing (will generate UUID if not provided)
550
+ * @param options.channelType - Channel type identifier
527
551
  * @returns Subscription object with unsubscribe method
528
552
  *
529
553
  * @example
@@ -543,7 +567,7 @@ export class BackendWebSocketService {
543
567
  * @see addChannelCallback for local callbacks without server-side subscription
544
568
  */
545
569
  async subscribe(options) {
546
- const { channels, callback, requestId } = options;
570
+ const { channels, channelType, callback, requestId } = options;
547
571
  if (__classPrivateFieldGet(this, _BackendWebSocketService_state, "f") !== WebSocketState.CONNECTED) {
548
572
  throw new Error(`Cannot create subscription(s) ${channels.join(', ')}: WebSocket is ${__classPrivateFieldGet(this, _BackendWebSocketService_state, "f")}`);
549
573
  }
@@ -556,10 +580,6 @@ export class BackendWebSocketService {
556
580
  throw new Error('Invalid subscription response: missing subscription ID');
557
581
  }
558
582
  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
583
  // Create unsubscribe function
564
584
  const unsubscribe = async (unsubRequestId) => {
565
585
  // Send unsubscribe request first
@@ -577,45 +597,42 @@ export class BackendWebSocketService {
577
597
  const subscription = {
578
598
  subscriptionId,
579
599
  channels: [...channels],
600
+ channelType,
580
601
  unsubscribe,
581
602
  };
582
603
  // Store subscription with subscription ID as key
583
604
  __classPrivateFieldGet(this, _BackendWebSocketService_subscriptions, "f").set(subscriptionId, {
584
605
  subscriptionId,
585
606
  channels: [...channels],
607
+ channelType,
586
608
  callback,
587
609
  unsubscribe,
588
610
  });
589
611
  return subscription;
590
612
  }
591
613
  }
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
- }
614
+ _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() {
615
+ // Subscribe to authentication state changes (sign in/out)
616
+ __classPrivateFieldGet(this, _BackendWebSocketService_messenger, "f").subscribe('AuthenticationController:stateChange', (state) => {
617
+ if (state.isSignedIn) {
618
+ // eslint-disable-next-line no-void
619
+ void this.connect();
620
+ }
621
+ else {
622
+ // eslint-disable-next-line no-void
623
+ void this.disconnect();
624
+ }
625
+ });
626
+ // Subscribe to wallet unlock event
627
+ __classPrivateFieldGet(this, _BackendWebSocketService_messenger, "f").subscribe('KeyringController:unlock', () => {
628
+ // eslint-disable-next-line no-void
629
+ void this.connect();
630
+ });
631
+ // Subscribe to wallet lock event
632
+ __classPrivateFieldGet(this, _BackendWebSocketService_messenger, "f").subscribe('KeyringController:lock', () => {
633
+ // eslint-disable-next-line no-void
634
+ void this.disconnect();
635
+ });
619
636
  }, _BackendWebSocketService_buildAuthenticatedUrl = function _BackendWebSocketService_buildAuthenticatedUrl(bearerToken) {
620
637
  const baseUrl = __classPrivateFieldGet(this, _BackendWebSocketService_options, "f").url;
621
638
  // Add token as query parameter to the WebSocket URL
@@ -631,6 +648,7 @@ _BackendWebSocketService_messenger = new WeakMap(), _BackendWebSocketService_opt
631
648
  */
632
649
  async function _BackendWebSocketService_establishConnection(bearerToken) {
633
650
  const wsUrl = __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_buildAuthenticatedUrl).call(this, bearerToken);
651
+ const connectionStartTime = Date.now();
634
652
  return new Promise((resolve, reject) => {
635
653
  const ws = new WebSocket(wsUrl);
636
654
  __classPrivateFieldSet(this, _BackendWebSocketService_connectionTimeout, setTimeout(() => {
@@ -638,40 +656,48 @@ async function _BackendWebSocketService_establishConnection(bearerToken) {
638
656
  timeout: __classPrivateFieldGet(this, _BackendWebSocketService_options, "f").timeout,
639
657
  });
640
658
  ws.close();
641
- reject(new Error(`Connection timeout after ${__classPrivateFieldGet(this, _BackendWebSocketService_options, "f").timeout}ms`));
659
+ reject(new Error(`Failed to connect to WebSocket: Connection timeout after ${__classPrivateFieldGet(this, _BackendWebSocketService_options, "f").timeout}ms`));
642
660
  }, __classPrivateFieldGet(this, _BackendWebSocketService_options, "f").timeout), "f");
643
661
  ws.onopen = () => {
644
662
  if (__classPrivateFieldGet(this, _BackendWebSocketService_connectionTimeout, "f")) {
645
663
  clearTimeout(__classPrivateFieldGet(this, _BackendWebSocketService_connectionTimeout, "f"));
646
664
  __classPrivateFieldSet(this, _BackendWebSocketService_connectionTimeout, null, "f");
647
665
  }
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();
666
+ // Calculate connection latency
667
+ const connectionLatency = Date.now() - connectionStartTime;
668
+ // Trace successful connection with latency
669
+ __classPrivateFieldGet(this, _BackendWebSocketService_trace, "f").call(this, {
670
+ name: `${SERVICE_NAME} Connection`,
671
+ data: {
672
+ reconnectAttempt: __classPrivateFieldGet(this, _BackendWebSocketService_reconnectAttempts, "f"),
673
+ latency_ms: connectionLatency,
674
+ },
675
+ tags: {
676
+ service: SERVICE_NAME,
677
+ },
678
+ }, () => {
679
+ __classPrivateFieldSet(this, _BackendWebSocketService_ws, ws, "f");
680
+ __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.CONNECTED);
681
+ __classPrivateFieldSet(this, _BackendWebSocketService_connectedAt, Date.now(), "f");
682
+ // Reset reconnect attempts on successful connection
683
+ __classPrivateFieldSet(this, _BackendWebSocketService_reconnectAttempts, 0, "f");
684
+ resolve();
685
+ });
654
686
  };
655
687
  ws.onerror = (event) => {
656
688
  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}`));
689
+ // Handle connection-phase errors
690
+ if (__classPrivateFieldGet(this, _BackendWebSocketService_connectionTimeout, "f")) {
691
+ clearTimeout(__classPrivateFieldGet(this, _BackendWebSocketService_connectionTimeout, "f"));
692
+ __classPrivateFieldSet(this, _BackendWebSocketService_connectionTimeout, null, "f");
669
693
  }
694
+ const error = new Error(`WebSocket connection error to ${wsUrl}`);
695
+ reject(error);
670
696
  };
671
697
  ws.onclose = (event) => {
672
698
  log('WebSocket onclose event triggered', {
673
699
  code: event.code,
674
- reason: event.reason || 'none',
700
+ reason: event.reason,
675
701
  wasClean: event.wasClean,
676
702
  });
677
703
  if (__classPrivateFieldGet(this, _BackendWebSocketService_state, "f") === WebSocketState.CONNECTING) {
@@ -743,42 +769,98 @@ async function _BackendWebSocketService_establishConnection(bearerToken) {
743
769
  if (__classPrivateFieldGet(this, _BackendWebSocketService_channelCallbacks, "f").size === 0) {
744
770
  return;
745
771
  }
746
- // Direct lookup for exact channel match
747
- __classPrivateFieldGet(this, _BackendWebSocketService_channelCallbacks, "f").get(message.channel)?.callback(message);
772
+ // Calculate notification latency: time from server sent to client received
773
+ const receivedAt = Date.now();
774
+ const latency = receivedAt - message.timestamp;
775
+ // Trace channel message processing with latency data
776
+ __classPrivateFieldGet(this, _BackendWebSocketService_trace, "f").call(this, {
777
+ name: `${SERVICE_NAME} Channel Message`,
778
+ data: {
779
+ channel: message.channel,
780
+ latency_ms: latency,
781
+ event: message.event,
782
+ },
783
+ tags: {
784
+ service: SERVICE_NAME,
785
+ channel_type: message.channel,
786
+ },
787
+ }, () => {
788
+ // Direct lookup for exact channel match
789
+ __classPrivateFieldGet(this, _BackendWebSocketService_channelCallbacks, "f").get(message.channel)?.callback(message);
790
+ });
748
791
  }, _BackendWebSocketService_handleSubscriptionNotification = function _BackendWebSocketService_handleSubscriptionNotification(message) {
749
- const { subscriptionId } = message;
792
+ const { subscriptionId, timestamp, channel } = message;
750
793
  // Only handle if subscriptionId is defined and not null (allows "0" as valid ID)
751
794
  if (subscriptionId !== null && subscriptionId !== undefined) {
752
- __classPrivateFieldGet(this, _BackendWebSocketService_subscriptions, "f").get(subscriptionId)?.callback?.(message);
795
+ const subscription = __classPrivateFieldGet(this, _BackendWebSocketService_subscriptions, "f").get(subscriptionId);
796
+ if (!subscription) {
797
+ return false;
798
+ }
799
+ // Calculate notification latency: time from server sent to client received
800
+ const receivedAt = Date.now();
801
+ const latency = receivedAt - timestamp;
802
+ // Trace notification processing with latency data
803
+ // Use stored channelType instead of parsing each time
804
+ __classPrivateFieldGet(this, _BackendWebSocketService_trace, "f").call(this, {
805
+ name: `${SERVICE_NAME} Notification`,
806
+ data: {
807
+ channel,
808
+ latency_ms: latency,
809
+ subscriptionId,
810
+ },
811
+ tags: {
812
+ service: SERVICE_NAME,
813
+ notification_type: subscription.channelType,
814
+ },
815
+ }, () => {
816
+ subscription.callback?.(message);
817
+ });
753
818
  return true;
754
819
  }
755
820
  return false;
756
821
  }, _BackendWebSocketService_parseMessage = function _BackendWebSocketService_parseMessage(data) {
757
822
  return JSON.parse(data);
758
823
  }, _BackendWebSocketService_handleClose = function _BackendWebSocketService_handleClose(event) {
824
+ // Calculate connection duration before we clear state
825
+ const connectionDuration = Date.now() - __classPrivateFieldGet(this, _BackendWebSocketService_connectedAt, "f");
759
826
  __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_clearTimers).call(this);
760
- __classPrivateFieldSet(this, _BackendWebSocketService_connectedAt, null, "f");
827
+ __classPrivateFieldSet(this, _BackendWebSocketService_connectedAt, 0, "f");
761
828
  // Clear any pending connection promise
762
829
  __classPrivateFieldSet(this, _BackendWebSocketService_connectionPromise, null, "f");
763
830
  // Clear subscriptions and pending requests on any disconnect
764
831
  // This ensures clean state for reconnection
765
832
  __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_clearPendingRequests).call(this, new Error('WebSocket connection closed'));
766
833
  __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);
834
+ // Update state to disconnected
835
+ __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_setState).call(this, WebSocketState.DISCONNECTED);
836
+ // Check if this was a manual disconnect
837
+ if (__classPrivateFieldGet(this, _BackendWebSocketService_manualDisconnect, "f")) {
838
+ // Manual disconnect - don't reconnect
839
+ log('WebSocket closed due to manual disconnect, not reconnecting');
770
840
  return;
771
841
  }
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', {
842
+ // Trace unexpected disconnect with details
843
+ __classPrivateFieldGet(this, _BackendWebSocketService_trace, "f").call(this, {
844
+ name: `${SERVICE_NAME} Disconnect`,
845
+ data: {
778
846
  code: event.code,
779
- });
780
- __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_scheduleReconnect).call(this);
781
- }
847
+ reason: event.reason || getCloseReason(event.code),
848
+ connectionDuration_ms: connectionDuration,
849
+ },
850
+ tags: {
851
+ service: SERVICE_NAME,
852
+ disconnect_type: 'unexpected',
853
+ },
854
+ }, () => {
855
+ // Empty trace callback - just measuring the event
856
+ });
857
+ // For any unexpected disconnects, attempt reconnection
858
+ // The manualDisconnect flag is the only gate - if it's false, we reconnect
859
+ log('Connection lost unexpectedly, will attempt reconnection', {
860
+ code: event.code,
861
+ reason: event.reason,
862
+ });
863
+ __classPrivateFieldGet(this, _BackendWebSocketService_instances, "m", _BackendWebSocketService_scheduleReconnect).call(this);
782
864
  }, _BackendWebSocketService_handleError = function _BackendWebSocketService_handleError(_error) {
783
865
  // Placeholder for future error handling logic
784
866
  }, _BackendWebSocketService_scheduleReconnect = function _BackendWebSocketService_scheduleReconnect() {
@@ -829,8 +911,5 @@ async function _BackendWebSocketService_establishConnection(bearerToken) {
829
911
  // Messenger handles listener errors internally, no need for try-catch
830
912
  __classPrivateFieldGet(this, _BackendWebSocketService_messenger, "f").publish('BackendWebSocketService:connectionStateChanged', this.getConnectionInfo());
831
913
  }
832
- }, _BackendWebSocketService_shouldReconnectOnClose = function _BackendWebSocketService_shouldReconnectOnClose(code) {
833
- // Don't reconnect only on normal closure (manual disconnect)
834
- return code !== 1000;
835
914
  };
836
915
  //# sourceMappingURL=BackendWebSocketService.mjs.map