Skip to content

Commit 253b44c

Browse files
committed
feat(api-graphql): add WebSocket connection health monitoring
Add getConnectionHealth() and isConnected() to AWSWebSocketProvider for real-time connection diagnostics. getConnectionHealth() reports whether the connection is healthy based on connection state and keep-alive staleness (65s threshold). isConnected() checks raw WebSocket readyState. Removed broken reconnect(), over-engineered persistent storage, and unused WebSocketControl interface from original implementation.
1 parent 24c0a0b commit 253b44c

File tree

4 files changed

+147
-0
lines changed

4 files changed

+147
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@aws-amplify/api-graphql': minor
3+
---
4+
5+
feat(api-graphql): add WebSocket connection health monitoring
6+
7+
Add `getConnectionHealth()` and `isConnected()` methods to the WebSocket provider,
8+
enabling consumers to check real-time connection health status and keep-alive staleness.
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { AWSAppSyncRealTimeProvider } from '../src/Providers/AWSAppSyncRealTimeProvider';
2+
import { ConnectionState as CS } from '../src/types/PubSub';
3+
4+
describe('WebSocket Health Monitoring', () => {
5+
let provider: AWSAppSyncRealTimeProvider;
6+
7+
beforeEach(() => {
8+
provider = new AWSAppSyncRealTimeProvider();
9+
});
10+
11+
afterEach(() => {
12+
jest.restoreAllMocks();
13+
});
14+
15+
describe('getConnectionHealth', () => {
16+
test('returns healthy when connected with recent keep-alive', () => {
17+
(provider as any).connectionState = CS.Connected;
18+
(provider as any).keepAliveTimestamp = Date.now();
19+
20+
const health = provider.getConnectionHealth();
21+
22+
expect(health.isHealthy).toBe(true);
23+
expect(health.connectionState).toBe(CS.Connected);
24+
expect(health.lastKeepAliveTime).toBeGreaterThan(0);
25+
expect(health.timeSinceLastKeepAlive).toBeLessThan(1000);
26+
});
27+
28+
test('returns unhealthy when not connected', () => {
29+
(provider as any).connectionState = CS.Disconnected;
30+
(provider as any).keepAliveTimestamp = Date.now();
31+
32+
const health = provider.getConnectionHealth();
33+
34+
expect(health.isHealthy).toBe(false);
35+
expect(health.connectionState).toBe(CS.Disconnected);
36+
});
37+
38+
test('returns unhealthy when keep-alive is stale (>65s)', () => {
39+
(provider as any).connectionState = CS.Connected;
40+
(provider as any).keepAliveTimestamp = Date.now() - 66_000;
41+
42+
const health = provider.getConnectionHealth();
43+
44+
expect(health.isHealthy).toBe(false);
45+
expect(health.connectionState).toBe(CS.Connected);
46+
expect(health.timeSinceLastKeepAlive).toBeGreaterThan(65_000);
47+
});
48+
49+
test('returns unhealthy during connection disruption', () => {
50+
(provider as any).connectionState = CS.ConnectionDisrupted;
51+
(provider as any).keepAliveTimestamp = Date.now();
52+
53+
const health = provider.getConnectionHealth();
54+
55+
expect(health.isHealthy).toBe(false);
56+
expect(health.connectionState).toBe(CS.ConnectionDisrupted);
57+
});
58+
59+
test('defaults connectionState to Disconnected when undefined', () => {
60+
(provider as any).connectionState = undefined;
61+
62+
const health = provider.getConnectionHealth();
63+
64+
expect(health.connectionState).toBe(CS.Disconnected);
65+
});
66+
});
67+
68+
describe('isConnected', () => {
69+
test('returns true when WebSocket readyState is OPEN', () => {
70+
(provider as any).awsRealTimeSocket = {
71+
readyState: WebSocket.OPEN,
72+
};
73+
74+
expect(provider.isConnected()).toBe(true);
75+
});
76+
77+
test('returns false when WebSocket is undefined', () => {
78+
(provider as any).awsRealTimeSocket = undefined;
79+
80+
expect(provider.isConnected()).toBe(false);
81+
});
82+
83+
test('returns false when WebSocket is CONNECTING', () => {
84+
(provider as any).awsRealTimeSocket = {
85+
readyState: WebSocket.CONNECTING,
86+
};
87+
88+
expect(provider.isConnected()).toBe(false);
89+
});
90+
91+
test('returns false when WebSocket is CLOSED', () => {
92+
(provider as any).awsRealTimeSocket = {
93+
readyState: WebSocket.CLOSED,
94+
};
95+
96+
expect(provider.isConnected()).toBe(false);
97+
});
98+
99+
test('returns false when WebSocket is CLOSING', () => {
100+
(provider as any).awsRealTimeSocket = {
101+
readyState: WebSocket.CLOSING,
102+
};
103+
104+
expect(provider.isConnected()).toBe(false);
105+
});
106+
});
107+
});

packages/api-graphql/src/Providers/AWSWebSocketProvider/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
ConnectionState,
1919
PubSubContentObserver,
2020
} from '../../types/PubSub';
21+
import type { WebSocketHealthState } from '../../types';
2122
import {
2223
AMPLIFY_SYMBOL,
2324
CONNECTION_INIT_TIMEOUT,
@@ -1025,4 +1026,28 @@ export abstract class AWSWebSocketProvider {
10251026
}
10261027
}
10271028
};
1029+
1030+
/**
1031+
* Get current WebSocket health state
1032+
*/
1033+
getConnectionHealth(): WebSocketHealthState {
1034+
const timeSinceLastKeepAlive = Date.now() - this.keepAliveTimestamp;
1035+
const isHealthy =
1036+
this.connectionState === ConnectionState.Connected &&
1037+
timeSinceLastKeepAlive < DEFAULT_KEEP_ALIVE_ALERT_TIMEOUT;
1038+
1039+
return {
1040+
isHealthy,
1041+
connectionState: this.connectionState || ConnectionState.Disconnected,
1042+
lastKeepAliveTime: this.keepAliveTimestamp,
1043+
timeSinceLastKeepAlive,
1044+
};
1045+
}
1046+
1047+
/**
1048+
* Check if WebSocket is currently connected
1049+
*/
1050+
isConnected(): boolean {
1051+
return this.awsRealTimeSocket?.readyState === WebSocket.OPEN;
1052+
}
10281053
}

packages/api-graphql/src/types/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,3 +522,10 @@ export interface AuthModeParams extends Record<string, unknown> {
522522
export type GenerateServerClientParams = {
523523
config: ResourcesConfig;
524524
} & CommonPublicClientOptions;
525+
526+
export interface WebSocketHealthState {
527+
isHealthy: boolean;
528+
connectionState: import('./PubSub').ConnectionState;
529+
lastKeepAliveTime: number;
530+
timeSinceLastKeepAlive: number;
531+
}

0 commit comments

Comments
 (0)