@@ -948,23 +948,16 @@ function isRevocationReprobeDue(chain: RevocableChain | null | undefined): boole
948948 return ! revokedAt || Date . now ( ) - revokedAt . getTime ( ) >= REVOKED_REFRESH_REPROBE_INTERVAL_MS
949949}
950950
951- /** What a settled refresh produced: one this process led, or one a follower saw its leader store . */
952- type SettledRefreshOutcome =
951+ /** What one coalesced refresh produced; `null` is a failure worth retrying later . */
952+ type RefreshOutcome =
953953 | { kind : 'token' ; accessToken : string }
954954 | { kind : 'revoked' ; errorCode : string }
955955
956- /**
957- * What one coalesced refresh produced; `null` is a failure worth retrying later. `lease-held`
958- * means this caller followed another lease holder for its whole wait and nothing was stored, while
959- * the account still holds the chain the caller read and no leader flagged it dead.
960- */
961- type RefreshOutcome = SettledRefreshOutcome | { kind : 'lease-held' }
962-
963- function tokenOutcome ( accessToken : string | null ) : SettledRefreshOutcome | null {
956+ function tokenOutcome ( accessToken : string | null ) : RefreshOutcome | null {
964957 return accessToken ? { kind : 'token' , accessToken } : null
965958}
966959
967- function revokedOutcome ( errorCode : string | null ) : SettledRefreshOutcome | null {
960+ function revokedOutcome ( errorCode : string | null ) : RefreshOutcome | null {
968961 return errorCode ? { kind : 'revoked' , errorCode } : null
969962}
970963
@@ -1022,10 +1015,6 @@ function isOAuthAccessTokenExpiring(
10221015 return expiresAt != null && expiresAt . getTime ( ) <= now . getTime ( ) + refreshWindowMs
10231016}
10241017
1025- function remainingValidityMs ( expiresAt : Date , now = new Date ( ) ) : number {
1026- return expiresAt . getTime ( ) - now . getTime ( )
1027- }
1028-
10291018/**
10301019 * Where a provider can refresh ahead of expiry, a caller is served the stored token with this much
10311020 * validity left or less while a background refresh replaces it. It starts above
@@ -1039,12 +1028,6 @@ const OAUTH_REFRESH_AHEAD_WINDOW_MS = 10 * 60 * 1000
10391028 * the two windows so every account still refreshes ahead before callers have to wait.
10401029 */
10411030const OAUTH_REFRESH_AHEAD_JITTER_MS = 60 * 1000
1042- /**
1043- * The validity a stored token must still have, after a blocking refresh that never settled, to be
1044- * served instead of failing the request. It sits below {@link OAUTH_ACCESS_TOKEN_REFRESH_WINDOW_MS}
1045- * on purpose: the request can still use such a token, and its next caller refreshes again.
1046- */
1047- const OAUTH_STORED_TOKEN_FALLBACK_MIN_MS = 30 * 1000
10481031
10491032/**
10501033 * A Google refresh does not rotate the refresh token, and Microsoft does not revoke the old one
@@ -1106,7 +1089,7 @@ function planAccessTokenRefresh(
11061089 if (
11071090 refreshesAhead &&
11081091 accessTokenExpiresAt &&
1109- remainingValidityMs ( accessTokenExpiresAt , now ) <=
1092+ accessTokenExpiresAt . getTime ( ) - now . getTime ( ) <=
11101093 OAUTH_REFRESH_AHEAD_WINDOW_MS - refreshAheadJitterMs ( accountId )
11111094 ) {
11121095 return { kind : 'refresh-ahead' , refreshToken }
@@ -1146,11 +1129,13 @@ const REFRESH_LOCK_HEADROOM_MS = 15_000
11461129const REFRESH_LOCK_TTL_SEC = Math . ceil ( ( TOKEN_REFRESH_TIMEOUT_MS + REFRESH_LOCK_HEADROOM_MS ) / 1000 )
11471130const REFRESH_FOLLOWER_MAX_WAIT_MS = REFRESH_LOCK_TTL_SEC * 1000
11481131/**
1149- * How long callers sharing one in-process refresh wait for it: past a follower's full wait (which
1150- * starts once the lock is contested, and ends with one more read), so a follower concludes on its
1151- * own; and still bounded, so a leader hung in this process fails its callers instead of wedging them.
1132+ * How long callers sharing one in-process refresh wait for it: a follower's full wait, then the
1133+ * leader run it may take over when that wait ends with a lapsed lease (budgeted like any lease),
1134+ * plus headroom for the lock round trips around them; so a follower concludes on its own, and a
1135+ * leader hung in this process still fails its callers instead of wedging them.
11521136 */
1153- const REFRESH_SETTLE_TIMEOUT_MS = REFRESH_FOLLOWER_MAX_WAIT_MS + REFRESH_LOCK_HEADROOM_MS
1137+ const REFRESH_SETTLE_TIMEOUT_MS =
1138+ REFRESH_FOLLOWER_MAX_WAIT_MS + REFRESH_LOCK_TTL_SEC * 1000 + REFRESH_LOCK_HEADROOM_MS
11541139
11551140/**
11561141 * The raw scope one refresh coordinates on: the account row, or the installation for a Slack
@@ -1338,27 +1323,25 @@ async function performCoalescedRefresh({
13381323
13391324 const refreshPromise = coalesceLocally (
13401325 lockKey ,
1341- async ( ) : Promise < RefreshOutcome | null > => {
1342- let led = false
1343- /** The refresh token the follower's latest read found, `undefined` when that read found nothing. */
1344- let followerSawRefreshToken : string | null | undefined
1345- const outcome = await withLeaderLock < SettledRefreshOutcome > ( {
1326+ ( ) =>
1327+ withLeaderLock < RefreshOutcome > ( {
13461328 key : lockKey ,
13471329 ttlSec : REFRESH_LOCK_TTL_SEC ,
13481330 maxWaitMs : REFRESH_FOLLOWER_MAX_WAIT_MS ,
13491331 onLeader : async ( ) => {
1350- led = true
13511332 try {
13521333 /**
1353- * Reread under the lock: another process may have recorded a revocation after the
1354- * caller read the row, and the version guards the record against a reconnect that
1355- * lands while the provider call is in flight.
1334+ * Reread under the lock: another process may have recorded a revocation or flagged the
1335+ * credential dead after the caller read the row (a follower taking over the lock after
1336+ * a failed leader released it sees only these), and the version guards the record
1337+ * against a reconnect that lands while the provider call is in flight.
13561338 */
13571339 const current = await readStoredChain ( accountId )
13581340 const recordedMeanwhile = recordedRevocationCode ( current )
13591341 if ( recordedMeanwhile && ! isRevocationReprobeDue ( current ) ) {
13601342 return revokedOutcome ( recordedMeanwhile )
13611343 }
1344+ if ( await getRecentTerminalError ( scopeKey ) ) return null
13621345 /**
13631346 * A writer that stored a new token after the caller's read (a refresh that finished just
13641347 * before this one took the lock, or a reconnect) already answers it, so this refresh
@@ -1578,7 +1561,6 @@ async function performCoalescedRefresh({
15781561 . from ( account )
15791562 . where ( eq ( account . id , accountId ) )
15801563 . limit ( 1 )
1581- followerSawRefreshToken = row ?. refreshToken
15821564 if (
15831565 row ?. accessToken &&
15841566 row . accessTokenExpiresAt &&
@@ -1592,24 +1574,14 @@ async function performCoalescedRefresh({
15921574 ? revokedOutcome ( revokedByLeader )
15931575 : null
15941576 } catch ( error ) {
1595- followerSawRefreshToken = undefined
15961577 logger . warn ( 'Follower DB read failed during refresh poll' , {
15971578 ...logContext ,
15981579 ...( privacyMode === 'selector' ? { } : { error : toError ( error ) . message } ) ,
15991580 } )
16001581 return null
16011582 }
16021583 } ,
1603- } )
1604- if ( outcome || led ) return outcome
1605- /**
1606- * A follower's wait ran out with nothing stored. That counts against the chain unless the
1607- * last read still found the chain this caller read and no leader flagged it dead meanwhile.
1608- */
1609- const chainStanding =
1610- followerSawRefreshToken === refreshToken && ! ( await getRecentTerminalError ( scopeKey ) )
1611- return chainStanding ? { kind : 'lease-held' } : null
1612- } ,
1584+ } ) ,
16131585 REFRESH_SETTLE_TIMEOUT_MS
16141586 )
16151587
@@ -1672,21 +1644,8 @@ async function resolveStoredAccessToken(
16721644 if ( fresh ?. kind === 'token' ) {
16731645 return { kind : 'token' , accessToken : fresh . accessToken , refreshed : true }
16741646 }
1675- /**
1676- * A refresh ahead abandoned with its process (Trigger.dev runs have no keep-alive) holds its
1677- * lease until the TTL, so a follower can wait it out with nothing; a chain that survives
1678- * refreshes still serves its stored token if enough validity is left after that wait. Any other
1679- * failure (a dead flag, a terminal error, a vanished account) says the chain itself is unusable.
1680- */
1681- const orphaned =
1682- fresh ?. kind === 'lease-held' &&
1683- refreshesAheadOfExpiry ( refresh . providerId ) &&
1684- stored . accessTokenExpiresAt != null &&
1685- remainingValidityMs ( stored . accessTokenExpiresAt ) > OAUTH_STORED_TOKEN_FALLBACK_MIN_MS
1686- if ( stored . accessToken && ( plan . kind === 'refresh-or-reuse' || orphaned ) ) {
1687- logger . info (
1688- `${ prefix } ${ orphaned ? 'Refresh lease still held elsewhere' : 'Refresh unavailable' } ; reusing still-valid access token`
1689- )
1647+ if ( plan . kind === 'refresh-or-reuse' && stored . accessToken ) {
1648+ logger . info ( `${ prefix } Refresh unavailable; reusing still-valid access token` )
16901649 return { kind : 'token' , accessToken : stored . accessToken , refreshed : false }
16911650 }
16921651 return { kind : 'failed' , outcome : fresh }
0 commit comments