- 
                Notifications
    You must be signed in to change notification settings 
- Fork 2.2k
feat(api-graphql): add WebSocket health monitoring and manual reconnection #14563
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
785502e
              b3fa5bd
              77e0b95
              6d9f872
              dbcab40
              1eb4414
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -49,6 +49,27 @@ import { | |
| } from './appsyncUrl'; | ||
| import { awsRealTimeHeaderBasedAuth } from './authHeaders'; | ||
|  | ||
| // Platform-safe AsyncStorage import | ||
| let AsyncStorage: any; | ||
| try { | ||
| // Try to import AsyncStorage for React Native (optional dependency) | ||
| // eslint-disable-next-line import/no-extraneous-dependencies | ||
| AsyncStorage = require('@react-native-async-storage/async-storage').default; | ||
| } catch (e) { | ||
| // Fallback for web/other platforms - use localStorage if available | ||
| AsyncStorage = | ||
| typeof localStorage !== 'undefined' | ||
| ? { | ||
| setItem: (key: string, value: string) => { | ||
| localStorage.setItem(key, value); | ||
|  | ||
| return Promise.resolve(); | ||
| }, | ||
| getItem: (key: string) => Promise.resolve(localStorage.getItem(key)), | ||
| } | ||
| : null; | ||
| } | ||
|  | ||
| const dispatchApiEvent = (payload: HubPayload) => { | ||
| Hub.dispatch('api', payload, 'PubSub', AMPLIFY_SYMBOL); | ||
| }; | ||
|  | @@ -106,6 +127,7 @@ export abstract class AWSWebSocketProvider { | |
| /** | ||
| * Mark the socket closed and release all active listeners | ||
| */ | ||
|  | ||
| close() { | ||
| // Mark the socket closed both in status and the connection monitor | ||
| this.socketStatus = SOCKET_STATUS.CLOSED; | ||
|  | @@ -681,6 +703,18 @@ export abstract class AWSWebSocketProvider { | |
| if (type === MESSAGE_TYPES.GQL_CONNECTION_KEEP_ALIVE) { | ||
| this.maintainKeepAlive(); | ||
|  | ||
| // Persist keep-alive timestamp for cross-session tracking | ||
| if (AsyncStorage) { | ||
| try { | ||
| AsyncStorage.setItem( | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is not handling the returned Promise. and as such the catch-path won't be taken. use  | ||
| 'AWS_AMPLIFY_LAST_KEEP_ALIVE', | ||
| JSON.stringify(new Date()), | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. better use  AsyncStorage.setItem('AWS_AMPLIFY_LAST_KEEP_ALIVE', `${Date.now()}`) | ||
| ); | ||
| } catch (error) { | ||
| this.logger.warn('Failed to persist keep-alive timestamp:', error); | ||
| } | ||
| } | ||
|  | ||
| return; | ||
| } | ||
|  | ||
|  | @@ -1025,4 +1059,114 @@ export abstract class AWSWebSocketProvider { | |
| } | ||
| } | ||
| }; | ||
|  | ||
| // WebSocket Health & Control API | ||
|  | ||
| /** | ||
| * Get current WebSocket health state | ||
| */ | ||
| getConnectionHealth(): import('../../types').WebSocketHealthState { | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. better use an import statement at the top of the file and use the type then directly | ||
| const timeSinceLastKeepAlive = this.keepAliveTimestamp | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 
 | ||
| ? Date.now() - this.keepAliveTimestamp | ||
| : undefined; | ||
|  | ||
| const isHealthy = | ||
| this.connectionState === ConnectionState.Connected && | ||
| this.keepAliveTimestamp && | ||
| timeSinceLastKeepAlive !== undefined && | ||
| timeSinceLastKeepAlive < 65000; // 65 second threshold | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this can be shortened: const isHealthy =
			this.connectionState === ConnectionState.Connected &&
			timeSinceLastKeepAlive < 65000; // 65 second threshold | ||
|  | ||
| return { | ||
| isHealthy: Boolean(isHealthy), | ||
| connectionState: this.connectionState || ConnectionState.Disconnected, | ||
| lastKeepAliveTime: this.keepAliveTimestamp, | ||
| timeSinceLastKeepAlive, | ||
| }; | ||
| } | ||
|  | ||
| /** | ||
| * Get persistent WebSocket health state (survives app restarts) | ||
| */ | ||
| async getPersistentConnectionHealth(): Promise< | ||
| import('../../types').WebSocketHealthState | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this would be resolved using direct import | ||
| > { | ||
| let persistentKeepAliveTime: number | undefined; | ||
| let _timeSinceLastPersistentKeepAlive: number | undefined; | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is the use of the  there is no need to mark variables this way and it can just be  | ||
|  | ||
| // Try to get persistent keep-alive timestamp | ||
| if (AsyncStorage) { | ||
| try { | ||
| const persistentKeepAlive = await AsyncStorage.getItem( | ||
| 'AWS_AMPLIFY_LAST_KEEP_ALIVE', | ||
| ); | ||
| if (persistentKeepAlive) { | ||
| const keepAliveDate = new Date(JSON.parse(persistentKeepAlive)); | ||
| persistentKeepAliveTime = keepAliveDate.getTime(); | ||
| _timeSinceLastPersistentKeepAlive = | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if using  timeSinceLastPersistentKeepAlive = Date.now() - persistentKeepAlive | ||
| Date.now() - persistentKeepAliveTime; | ||
| } | ||
| } catch (error) { | ||
| this.logger.warn( | ||
| 'Failed to retrieve persistent keep-alive timestamp:', | ||
| error, | ||
| ); | ||
| } | ||
| } | ||
|  | ||
| // Use the more recent timestamp (in-memory vs persistent) | ||
| const lastKeepAliveTime = | ||
| Math.max(this.keepAliveTimestamp || 0, persistentKeepAliveTime || 0) || | ||
| undefined; | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why the undefined? this simplifies  | ||
|  | ||
| const timeSinceLastKeepAlive = lastKeepAliveTime | ||
| ? Date.now() - lastKeepAliveTime | ||
| : undefined; | ||
|  | ||
| // Health check includes persistent data | ||
| const isHealthy = | ||
| this.connectionState === ConnectionState.Connected && | ||
| lastKeepAliveTime && | ||
| timeSinceLastKeepAlive !== undefined && | ||
| timeSinceLastKeepAlive < 65000; // 65 second threshold | ||
|  | ||
| return { | ||
| isHealthy: Boolean(isHealthy), | ||
| connectionState: this.connectionState || ConnectionState.Disconnected, | ||
| lastKeepAliveTime, | ||
| timeSinceLastKeepAlive, | ||
| }; | ||
| } | ||
|  | ||
| /** | ||
| * Check if WebSocket is currently connected | ||
| */ | ||
| isConnected(): boolean { | ||
| return this.awsRealTimeSocket?.readyState === WebSocket.OPEN; | ||
| } | ||
|  | ||
| /** | ||
| * Manually reconnect WebSocket | ||
| */ | ||
| async reconnect(): Promise<void> { | ||
| this.logger.info('Manual WebSocket reconnection requested'); | ||
|  | ||
| // Close existing connection if any | ||
| if (this.isConnected()) { | ||
| this.close(); | ||
| // Wait briefly for clean disconnect | ||
| await new Promise(resolve => setTimeout(resolve, 100)); | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just await  | ||
| } | ||
|  | ||
| // Reconnect - this would need to be implemented based on how the provider is used | ||
| // For now, log that reconnection was attempted | ||
| this.logger.info('WebSocket reconnection attempted'); | ||
| } | ||
|  | ||
| /** | ||
| * Manually disconnect WebSocket | ||
| */ | ||
| disconnect(): void { | ||
| this.logger.info('Manual WebSocket disconnect requested'); | ||
| this.close(); | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not directly use close? | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have the
KeyValueStorageInterface.It can be (partially) used as a type here. and would be better than
any