Skip to content

Commit ad2b663

Browse files
committed
Refactor useConnectionStatus hook with adaptive polling and remove unused deps
1 parent ab80d1d commit ad2b663

File tree

2 files changed

+200
-4
lines changed

2 files changed

+200
-4
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { describe, test, expect } from 'bun:test'
2+
3+
import { getNextInterval } from '../use-connection-status'
4+
5+
/**
6+
* Tests for the adaptive health check interval logic.
7+
*
8+
* These tests verify the core exponential backoff algorithm that determines
9+
* how frequently health checks should run based on consecutive successful checks.
10+
*/
11+
12+
describe('useConnectionStatus - adaptive interval logic', () => {
13+
describe('getNextInterval', () => {
14+
test('returns 10s for 0-2 consecutive successes', () => {
15+
expect(getNextInterval(0)).toBe(10_000)
16+
expect(getNextInterval(1)).toBe(10_000)
17+
expect(getNextInterval(2)).toBe(10_000)
18+
})
19+
20+
test('returns 30s for 3-5 consecutive successes', () => {
21+
expect(getNextInterval(3)).toBe(30_000)
22+
expect(getNextInterval(4)).toBe(30_000)
23+
expect(getNextInterval(5)).toBe(30_000)
24+
})
25+
26+
test('returns 60s for 6-9 consecutive successes', () => {
27+
expect(getNextInterval(6)).toBe(60_000)
28+
expect(getNextInterval(7)).toBe(60_000)
29+
expect(getNextInterval(8)).toBe(60_000)
30+
expect(getNextInterval(9)).toBe(60_000)
31+
})
32+
33+
test('returns 2min for 10-14 consecutive successes', () => {
34+
expect(getNextInterval(10)).toBe(120_000)
35+
expect(getNextInterval(11)).toBe(120_000)
36+
expect(getNextInterval(14)).toBe(120_000)
37+
})
38+
39+
test('returns 5min for 15-19 consecutive successes', () => {
40+
expect(getNextInterval(15)).toBe(300_000)
41+
expect(getNextInterval(16)).toBe(300_000)
42+
expect(getNextInterval(19)).toBe(300_000)
43+
})
44+
45+
test('returns 10min (max) for 20+ consecutive successes', () => {
46+
expect(getNextInterval(20)).toBe(600_000)
47+
expect(getNextInterval(25)).toBe(600_000)
48+
expect(getNextInterval(100)).toBe(600_000)
49+
expect(getNextInterval(1000)).toBe(600_000)
50+
})
51+
})
52+
53+
describe('interval progression', () => {
54+
test('progresses through all thresholds in order', () => {
55+
const progression = [
56+
{ successes: 0, expectedInterval: 10_000 },
57+
{ successes: 3, expectedInterval: 30_000 },
58+
{ successes: 6, expectedInterval: 60_000 },
59+
{ successes: 10, expectedInterval: 120_000 },
60+
{ successes: 15, expectedInterval: 300_000 },
61+
{ successes: 20, expectedInterval: 600_000 },
62+
]
63+
64+
for (const { successes, expectedInterval } of progression) {
65+
expect(getNextInterval(successes)).toBe(expectedInterval)
66+
}
67+
})
68+
69+
test('intervals increase monotonically', () => {
70+
let previousInterval = 0
71+
// Test first occurrence of each threshold
72+
const testPoints = [0, 3, 6, 10, 15, 20, 50]
73+
74+
for (const successes of testPoints) {
75+
const interval = getNextInterval(successes)
76+
expect(interval).toBeGreaterThanOrEqual(previousInterval)
77+
previousInterval = interval
78+
}
79+
})
80+
})
81+
82+
describe('edge cases', () => {
83+
test('handles negative values (treated as 0)', () => {
84+
// In practice, consecutive successes should never be negative,
85+
// but the function should handle it gracefully
86+
expect(getNextInterval(-1)).toBe(10_000)
87+
expect(getNextInterval(-100)).toBe(10_000)
88+
})
89+
90+
test('handles very large values', () => {
91+
expect(getNextInterval(Number.MAX_SAFE_INTEGER)).toBe(600_000)
92+
})
93+
})
94+
95+
describe('boundary conditions', () => {
96+
test('uses correct interval at exact threshold boundaries', () => {
97+
// At threshold - 1: should use previous interval
98+
expect(getNextInterval(2)).toBe(10_000) // Just before 3
99+
expect(getNextInterval(5)).toBe(30_000) // Just before 6
100+
expect(getNextInterval(9)).toBe(60_000) // Just before 10
101+
expect(getNextInterval(14)).toBe(120_000) // Just before 15
102+
expect(getNextInterval(19)).toBe(300_000) // Just before 20
103+
104+
// At threshold: should use new interval
105+
expect(getNextInterval(3)).toBe(30_000)
106+
expect(getNextInterval(6)).toBe(60_000)
107+
expect(getNextInterval(10)).toBe(120_000)
108+
expect(getNextInterval(15)).toBe(300_000)
109+
expect(getNextInterval(20)).toBe(600_000)
110+
})
111+
})
112+
})

cli/src/hooks/use-connection-status.ts

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,124 @@ import { useEffect, useState } from 'react'
33
import { getCodebuffClient } from '../utils/codebuff-client'
44
import { logger } from '../utils/logger'
55

6+
// Adaptive health check interval configuration
7+
// Progressively increases polling interval based on consecutive successful checks
8+
export const HEALTH_CHECK_CONFIG = {
9+
// Initial interval after startup or failure (ms)
10+
INITIAL_INTERVAL: 10_000, // 10 seconds
11+
// Interval thresholds based on consecutive successful checks
12+
INTERVALS: [
13+
{ successCount: 3, interval: 30_000 }, // 30 seconds after 3 successes
14+
{ successCount: 6, interval: 60_000 }, // 1 minute after 6 successes
15+
{ successCount: 10, interval: 120_000 }, // 2 minutes after 10 successes
16+
{ successCount: 15, interval: 300_000 }, // 5 minutes after 15 successes
17+
{ successCount: 20, interval: 600_000 }, // 10 minutes after 20 successes
18+
],
19+
} as const
20+
21+
/**
22+
* Calculates the next health check interval based on consecutive successful checks
23+
* Exported for testing purposes
24+
*/
25+
export function getNextInterval(consecutiveSuccesses: number): number {
26+
// Find the highest threshold that we've passed
27+
for (let i = HEALTH_CHECK_CONFIG.INTERVALS.length - 1; i >= 0; i--) {
28+
const { successCount, interval } = HEALTH_CHECK_CONFIG.INTERVALS[i]
29+
if (consecutiveSuccesses >= successCount) {
30+
return interval
31+
}
32+
}
33+
return HEALTH_CHECK_CONFIG.INITIAL_INTERVAL
34+
}
35+
36+
/**
37+
* Hook to monitor connection status to the Codebuff backend.
38+
* Uses adaptive exponential backoff to reduce polling frequency when connection is stable.
39+
*/
640
export const useConnectionStatus = () => {
41+
742
const [isConnected, setIsConnected] = useState(true)
843

944
useEffect(() => {
1045
let isMounted = true
46+
let timeoutId: NodeJS.Timeout | null = null
47+
let consecutiveSuccesses = 0
48+
let currentInterval: number = HEALTH_CHECK_CONFIG.INITIAL_INTERVAL
49+
50+
const scheduleNextCheck = (interval: number) => {
51+
if (!isMounted) return
52+
timeoutId = setTimeout(() => checkConnection(), interval)
53+
}
1154

1255
const checkConnection = async () => {
1356
const client = getCodebuffClient()
1457
if (!client) {
1558
if (isMounted) {
1659
setIsConnected(false)
60+
consecutiveSuccesses = 0
61+
currentInterval = HEALTH_CHECK_CONFIG.INITIAL_INTERVAL
62+
logger.debug(
63+
{ interval: currentInterval },
64+
'Health check: No client, reset to initial interval',
65+
)
66+
scheduleNextCheck(currentInterval)
1767
}
1868
return
1969
}
2070

2171
try {
2272
const connected = await client.checkConnection()
23-
if (isMounted) {
24-
setIsConnected(connected)
73+
if (!isMounted) return
74+
75+
setIsConnected(connected)
76+
77+
if (connected) {
78+
consecutiveSuccesses++
79+
const newInterval = getNextInterval(consecutiveSuccesses)
80+
81+
// Log when interval changes
82+
if (newInterval !== currentInterval) {
83+
logger.debug(
84+
{
85+
consecutiveSuccesses,
86+
oldInterval: currentInterval,
87+
newInterval,
88+
},
89+
'Health check interval increased',
90+
)
91+
currentInterval = newInterval
92+
}
93+
94+
scheduleNextCheck(currentInterval)
95+
} else {
96+
// Reset to fast polling on connection failure
97+
consecutiveSuccesses = 0
98+
currentInterval = HEALTH_CHECK_CONFIG.INITIAL_INTERVAL
99+
logger.debug(
100+
{ interval: currentInterval },
101+
'Health check failed, reset to initial interval',
102+
)
103+
scheduleNextCheck(currentInterval)
25104
}
26105
} catch (error) {
27106
logger.debug({ error }, 'Connection check failed')
28107
if (isMounted) {
29108
setIsConnected(false)
109+
consecutiveSuccesses = 0
110+
currentInterval = HEALTH_CHECK_CONFIG.INITIAL_INTERVAL
111+
scheduleNextCheck(currentInterval)
30112
}
31113
}
32114
}
33115

116+
// Start first check immediately
34117
checkConnection()
35-
const interval = setInterval(checkConnection, 30000)
36118

37119
return () => {
38120
isMounted = false
39-
clearInterval(interval)
121+
if (timeoutId) {
122+
clearTimeout(timeoutId)
123+
}
40124
}
41125
}, [])
42126

0 commit comments

Comments
 (0)