Skip to content

Commit 9b286a2

Browse files
committed
feat: add network detector that uses notices endpoint
1 parent 90d2b20 commit 9b286a2

File tree

2 files changed

+162
-0
lines changed

2 files changed

+162
-0
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { Agent } from 'https';
2+
import { request } from 'https';
3+
4+
/**
5+
* Detects internet connectivity by making a lightweight request to the notices endpoint
6+
*/
7+
export class NetworkDetector {
8+
private static readonly CACHE_DURATION_MS = 30_000; // 30 seconds
9+
private static readonly TIMEOUT_MS = 500;
10+
11+
private static cachedResult: boolean | undefined;
12+
private static cacheExpiry: number = 0;
13+
14+
/**
15+
* Check if internet connectivity is available
16+
*/
17+
public static async hasConnectivity(agent?: Agent): Promise<boolean> {
18+
const now = Date.now();
19+
20+
// Return cached result if still valid
21+
if (this.cachedResult !== undefined && now < this.cacheExpiry) {
22+
return this.cachedResult;
23+
}
24+
25+
try {
26+
const connected = await this.ping(agent);
27+
this.cachedResult = connected;
28+
this.cacheExpiry = now + this.CACHE_DURATION_MS;
29+
return connected;
30+
} catch {
31+
this.cachedResult = false;
32+
this.cacheExpiry = now + this.CACHE_DURATION_MS;
33+
return false;
34+
}
35+
}
36+
37+
private static ping(agent?: Agent): Promise<boolean> {
38+
return new Promise((resolve) => {
39+
const req = request({
40+
hostname: 'cli.cdk.dev-tools.aws.dev',
41+
path: '/notices.json',
42+
method: 'HEAD',
43+
agent,
44+
timeout: this.TIMEOUT_MS,
45+
}, (res) => {
46+
resolve(res.statusCode !== undefined && res.statusCode < 500);
47+
});
48+
49+
req.on('error', () => resolve(false));
50+
req.on('timeout', () => {
51+
req.destroy();
52+
resolve(false);
53+
});
54+
55+
req.end();
56+
});
57+
}
58+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import * as https from 'https';
2+
import { NetworkDetector } from '../../lib/util/network-detector';
3+
4+
// Mock the https module
5+
jest.mock('https');
6+
const mockHttps = https as jest.Mocked<typeof https>;
7+
8+
describe('NetworkDetector', () => {
9+
let mockRequest: jest.Mock;
10+
11+
beforeEach(() => {
12+
jest.clearAllMocks();
13+
mockRequest = jest.fn();
14+
mockHttps.request.mockImplementation(mockRequest);
15+
16+
// Clear static cache between tests
17+
(NetworkDetector as any).cachedResult = undefined;
18+
(NetworkDetector as any).cacheExpiry = 0;
19+
});
20+
21+
test('returns true when server responds with success status', async () => {
22+
const mockReq = {
23+
on: jest.fn(),
24+
end: jest.fn(),
25+
destroy: jest.fn(),
26+
};
27+
28+
mockRequest.mockImplementation((_, callback) => {
29+
callback({ statusCode: 200 });
30+
return mockReq;
31+
});
32+
33+
const result = await NetworkDetector.hasConnectivity();
34+
expect(result).toBe(true);
35+
});
36+
37+
test('returns false when server responds with server error', async () => {
38+
const mockReq = {
39+
on: jest.fn(),
40+
end: jest.fn(),
41+
destroy: jest.fn(),
42+
};
43+
44+
mockRequest.mockImplementation((_, callback) => {
45+
callback({ statusCode: 500 });
46+
return mockReq;
47+
});
48+
49+
const result = await NetworkDetector.hasConnectivity();
50+
expect(result).toBe(false);
51+
});
52+
53+
test('returns false on network error', async () => {
54+
const mockReq = {
55+
on: jest.fn((event, handler) => {
56+
if (event === 'error') {
57+
setTimeout(() => handler(new Error('Network error')), 0);
58+
}
59+
}),
60+
end: jest.fn(),
61+
destroy: jest.fn(),
62+
};
63+
64+
mockRequest.mockReturnValue(mockReq);
65+
66+
const result = await NetworkDetector.hasConnectivity();
67+
expect(result).toBe(false);
68+
});
69+
70+
test('returns false on timeout', async () => {
71+
const mockReq = {
72+
on: jest.fn((event, handler) => {
73+
if (event === 'timeout') {
74+
setTimeout(() => handler(), 0);
75+
}
76+
}),
77+
end: jest.fn(),
78+
destroy: jest.fn(),
79+
};
80+
81+
mockRequest.mockReturnValue(mockReq);
82+
83+
const result = await NetworkDetector.hasConnectivity();
84+
expect(result).toBe(false);
85+
});
86+
87+
test('caches result for subsequent calls', async () => {
88+
const mockReq = {
89+
on: jest.fn(),
90+
end: jest.fn(),
91+
destroy: jest.fn(),
92+
};
93+
94+
mockRequest.mockImplementation((_, callback) => {
95+
callback({ statusCode: 200 });
96+
return mockReq;
97+
});
98+
99+
await NetworkDetector.hasConnectivity();
100+
await NetworkDetector.hasConnectivity();
101+
102+
expect(mockRequest).toHaveBeenCalledTimes(1);
103+
});
104+
});

0 commit comments

Comments
 (0)