fix: align scheduled refresh buffer with isExpiring buffer#80
Conversation
Greptile SummaryThis PR fixes a scheduling bug where short-lived tokens (TTL ≤ 5 minutes) would have their refresh timer fire when
Confidence Score: 4/5Safe to merge; the core bug is correctly fixed and the new test clearly demonstrates the before/after behavior. The buffer alignment and <= boundary fix are correct and well-tested. One residual gap: if setTimeout fires a few milliseconds early (normal jitter), getAccessTokenSilently can still take the isExpiring=false early-return path without rescheduling, leaving the token unmonitored. This is far less likely than before, but the path is not fully guarded. The getAccessTokenSilently early-return path around lines 262-263 of tokenStore.ts deserves a second look for the missing reschedule case. Important Files Changed
|
Minor bug with the scheduled token refresh mechanism for tokens with a TTL of 5 minutes or less.
tl;dr - For tokens of 5min or less, the buffer to refresh drops to 30s, but the scheduler still schedules to (TTL - 60s). This results in a no-op scheduled refresh, as at 1min to TTL
isExpiringreturnsfalse.Problem
tokenStore.tshas two pieces of logic that both reason about "how close to expiry should we refresh," and they disagree.getRefreshDelayalways usesTOKEN_EXPIRY_BUFFER_SECONDS = 60:parseTokenuses a different buffer for short-lived tokens:The scheduled timer's callback is
getAccessTokenSilently, which callsparseTokenand early-returns without refreshing or rescheduling whenisExpiring === false.For a 5-minute token issued at T=0:
scheduleRefresh(300). Timer set for T+240s (60s buffer).parseTokensays 60s left, but its buffer is 30s →isExpiring = false→ early-return. No refresh, no new timer.getAccessTokenSilently.Solution
Route both buffers through one source of truth.
getExpiryBuffer(totalTokenLifetime)returns 30s for tokens with lifetime ≤ 300s, 60s otherwise.parseTokenuses it and now also returnstotalTokenLifetime.scheduleRefresh/getRefreshDelayaccept the fulltokenDataand apply the same helper.isExpiringswitches from<to<=so a token whose remaining lifetime lands exactly on the buffer boundary is treated as expiring - the schedule fires at(timeUntilExpiry - buffer) * 1000ms, which landstimeUntilExpiryexactly back at
buffer. Strict<would no-op at that boundary.After the change, a 5-minute token schedules its first refresh at T+270s, and the timer's callback call
Tests
One new behavioral test in
tokenStore.spec.ts:refreshAccessTokenActionwas called exactly once.Verified RED on the unfixed code (
expected 1 times, got 0- the schedule fires but the callback no-op