@@ -503,6 +503,15 @@ interface CoalescedRefreshOutcome {
503503 error ?: OAuthRefreshError
504504}
505505
506+ /**
507+ * Slack lock budgets sized past `TOKEN_REFRESH_TIMEOUT_MS` (15s) in
508+ * lib/oauth/oauth.ts: installation-keyed locks make every sibling row's request
509+ * a follower of one refresh, so followers must keep polling for the leader's
510+ * full provider window and the lock must not expire under a live refresh.
511+ */
512+ const SLACK_FOLLOWER_MAX_WAIT_MS = 16_000
513+ const SLACK_LOCK_TTL_SEC = 20
514+
506515async function performCoalescedRefresh ( {
507516 accountId,
508517 providerId,
@@ -541,6 +550,11 @@ async function performCoalescedRefresh({
541550 const refreshPromise = coalesceLocally ( lockKey , ( ) =>
542551 withLeaderLock < CoalescedRefreshOutcome > ( {
543552 key : lockKey ,
553+ // Installation-keyed Slack locks gather followers from every sibling row,
554+ // so their wait and the lock TTL must outlast the 15s provider timeout —
555+ // the 3s/10s defaults would fail followers early and let a second leader
556+ // start a concurrent rotation mid-refresh.
557+ ...( slackTeamId ? { maxWaitMs : SLACK_FOLLOWER_MAX_WAIT_MS , ttlSec : SLACK_LOCK_TTL_SEC } : { } ) ,
544558 onLeader : async ( ) => {
545559 try {
546560 let refreshTokenToUse = refreshToken
@@ -579,7 +593,17 @@ async function performCoalescedRefresh({
579593 }
580594 return {
581595 accessToken : null ,
582- error : new OAuthRefreshError ( providerId , result . errorCode , result . errorDescription ) ,
596+ // No errorCode = transient (timeout/network), not a provider rejection —
597+ // stay errorless so callers keep their null-fallback behavior.
598+ ...( result . errorCode
599+ ? {
600+ error : new OAuthRefreshError (
601+ providerId ,
602+ result . errorCode ,
603+ result . errorDescription
604+ ) ,
605+ }
606+ : { } ) ,
583607 }
584608 }
585609
0 commit comments