33import { Observable , Subscription , SubscriptionLike } from 'rxjs' ;
44import { GraphQLError } from 'graphql' ;
55import { ConsoleLogger , Hub , HubPayload } from '@aws-amplify/core' ;
6+ import type { KeyValueStorageInterface } from '@aws-amplify/core' ;
67import {
78 CustomUserAgentDetails ,
89 DocumentType ,
@@ -18,6 +19,7 @@ import {
1819 ConnectionState ,
1920 PubSubContentObserver ,
2021} from '../../types/PubSub' ;
22+ import type { WebSocketHealthState } from '../../types' ;
2123import {
2224 AMPLIFY_SYMBOL ,
2325 CONNECTION_INIT_TIMEOUT ,
@@ -49,6 +51,35 @@ import {
4951} from './appsyncUrl' ;
5052import { awsRealTimeHeaderBasedAuth } from './authHeaders' ;
5153
54+ // Storage key for persistent keep-alive tracking
55+ const KEEP_ALIVE_STORAGE_KEY = 'AWS_AMPLIFY_LAST_KEEP_ALIVE' ;
56+
57+ // Platform-safe storage implementation
58+ let platformStorage : Pick <
59+ KeyValueStorageInterface ,
60+ 'setItem' | 'getItem'
61+ > | null = null ;
62+
63+ try {
64+ // Try to import AsyncStorage for React Native (optional dependency)
65+ const AsyncStorage =
66+ // eslint-disable-next-line import/no-extraneous-dependencies
67+ require ( '@react-native-async-storage/async-storage' ) . default ;
68+ platformStorage = AsyncStorage ;
69+ } catch ( e ) {
70+ // Fallback for web/other platforms - use localStorage if available
71+ if ( typeof localStorage !== 'undefined' ) {
72+ platformStorage = {
73+ setItem : ( key : string , value : string ) => {
74+ localStorage . setItem ( key , value ) ;
75+
76+ return Promise . resolve ( ) ;
77+ } ,
78+ getItem : ( key : string ) => Promise . resolve ( localStorage . getItem ( key ) ) ,
79+ } ;
80+ }
81+ }
82+
5283const dispatchApiEvent = ( payload : HubPayload ) => {
5384 Hub . dispatch ( 'api' , payload , 'PubSub' , AMPLIFY_SYMBOL ) ;
5485} ;
@@ -106,6 +137,7 @@ export abstract class AWSWebSocketProvider {
106137 /**
107138 * Mark the socket closed and release all active listeners
108139 */
140+
109141 close ( ) {
110142 // Mark the socket closed both in status and the connection monitor
111143 this . socketStatus = SOCKET_STATUS . CLOSED ;
@@ -681,6 +713,15 @@ export abstract class AWSWebSocketProvider {
681713 if ( type === MESSAGE_TYPES . GQL_CONNECTION_KEEP_ALIVE ) {
682714 this . maintainKeepAlive ( ) ;
683715
716+ // Persist keep-alive timestamp for cross-session tracking
717+ if ( platformStorage ) {
718+ platformStorage
719+ . setItem ( KEEP_ALIVE_STORAGE_KEY , `${ Date . now ( ) } ` )
720+ . catch ( error => {
721+ this . logger . warn ( 'Failed to persist keep-alive timestamp:' , error ) ;
722+ } ) ;
723+ }
724+
684725 return ;
685726 }
686727
@@ -1025,4 +1066,92 @@ export abstract class AWSWebSocketProvider {
10251066 }
10261067 }
10271068 } ;
1069+
1070+ // WebSocket Health & Control API
1071+
1072+ /**
1073+ * Get current WebSocket health state
1074+ */
1075+ getConnectionHealth ( ) : WebSocketHealthState {
1076+ const timeSinceLastKeepAlive = Date . now ( ) - this . keepAliveTimestamp ;
1077+
1078+ const isHealthy =
1079+ this . connectionState === ConnectionState . Connected &&
1080+ timeSinceLastKeepAlive < DEFAULT_KEEP_ALIVE_ALERT_TIMEOUT ;
1081+
1082+ return {
1083+ isHealthy,
1084+ connectionState : this . connectionState || ConnectionState . Disconnected ,
1085+ lastKeepAliveTime : this . keepAliveTimestamp ,
1086+ timeSinceLastKeepAlive,
1087+ } ;
1088+ }
1089+
1090+ /**
1091+ * Get persistent WebSocket health state (survives app restarts)
1092+ */
1093+ async getPersistentConnectionHealth ( ) : Promise < WebSocketHealthState > {
1094+ let persistentKeepAliveTime = 0 ;
1095+
1096+ // Try to get persistent keep-alive timestamp
1097+ if ( platformStorage ) {
1098+ try {
1099+ const persistentKeepAlive = await platformStorage . getItem (
1100+ KEEP_ALIVE_STORAGE_KEY ,
1101+ ) ;
1102+ if ( persistentKeepAlive ) {
1103+ persistentKeepAliveTime = Number ( persistentKeepAlive ) || 0 ;
1104+ }
1105+ } catch ( error ) {
1106+ this . logger . warn (
1107+ 'Failed to retrieve persistent keep-alive timestamp:' ,
1108+ error ,
1109+ ) ;
1110+ }
1111+ }
1112+
1113+ // Use the more recent timestamp (in-memory vs persistent)
1114+ const lastKeepAliveTime = Math . max (
1115+ this . keepAliveTimestamp ,
1116+ persistentKeepAliveTime ,
1117+ ) ;
1118+
1119+ const timeSinceLastKeepAlive =
1120+ lastKeepAliveTime > 0 ? Date . now ( ) - lastKeepAliveTime : Infinity ; // If no keep-alive has been received, treat as unhealthy
1121+
1122+ // Health check includes persistent data
1123+ const isHealthy =
1124+ this . connectionState === ConnectionState . Connected &&
1125+ timeSinceLastKeepAlive < DEFAULT_KEEP_ALIVE_ALERT_TIMEOUT ;
1126+
1127+ return {
1128+ isHealthy,
1129+ connectionState : this . connectionState || ConnectionState . Disconnected ,
1130+ lastKeepAliveTime : lastKeepAliveTime > 0 ? lastKeepAliveTime : undefined ,
1131+ timeSinceLastKeepAlive :
1132+ lastKeepAliveTime > 0 ? timeSinceLastKeepAlive : undefined ,
1133+ } ;
1134+ }
1135+
1136+ /**
1137+ * Check if WebSocket is currently connected
1138+ */
1139+ isConnected ( ) : boolean {
1140+ return this . awsRealTimeSocket ?. readyState === WebSocket . OPEN ;
1141+ }
1142+
1143+ /**
1144+ * Manually reconnect WebSocket
1145+ */
1146+ async reconnect ( ) : Promise < void > {
1147+ this . logger . info ( 'Manual WebSocket reconnection requested' ) ;
1148+
1149+ // Close existing connection if any
1150+ if ( this . isConnected ( ) ) {
1151+ await this . close ( ) ;
1152+ }
1153+
1154+ // Trigger reconnection through the reconnection monitor
1155+ this . reconnectionMonitor . record ( ReconnectEvent . START_RECONNECT ) ;
1156+ }
10281157}
0 commit comments