@@ -677,6 +677,15 @@ interface CoalescedRefreshOutcome {
677677 error ?: OAuthRefreshError
678678}
679679
680+ /**
681+ * Slack lock budgets sized past `TOKEN_REFRESH_TIMEOUT_MS` (15s) in
682+ * lib/oauth/oauth.ts: installation-keyed locks make every sibling row's request
683+ * a follower of one refresh, so followers must keep polling for the leader's
684+ * full provider window and the lock must not expire under a live refresh.
685+ */
686+ const SLACK_FOLLOWER_MAX_WAIT_MS = 16_000
687+ const SLACK_LOCK_TTL_SEC = 20
688+
680689async function performCoalescedRefresh ( {
681690 accountId,
682691 providerId,
@@ -715,6 +724,11 @@ async function performCoalescedRefresh({
715724 const refreshPromise = coalesceLocally ( lockKey , ( ) =>
716725 withLeaderLock < CoalescedRefreshOutcome > ( {
717726 key : lockKey ,
727+ // Installation-keyed Slack locks gather followers from every sibling row,
728+ // so their wait and the lock TTL must outlast the 15s provider timeout —
729+ // the 3s/10s defaults would fail followers early and let a second leader
730+ // start a concurrent rotation mid-refresh.
731+ ...( slackTeamId ? { maxWaitMs : SLACK_FOLLOWER_MAX_WAIT_MS , ttlSec : SLACK_LOCK_TTL_SEC } : { } ) ,
718732 onLeader : async ( ) => {
719733 try {
720734 let refreshTokenToUse = refreshToken
@@ -753,7 +767,17 @@ async function performCoalescedRefresh({
753767 }
754768 return {
755769 accessToken : null ,
756- error : new OAuthRefreshError ( providerId , result . errorCode , result . errorDescription ) ,
770+ // No errorCode = transient (timeout/network), not a provider rejection —
771+ // stay errorless so callers keep their null-fallback behavior.
772+ ...( result . errorCode
773+ ? {
774+ error : new OAuthRefreshError (
775+ providerId ,
776+ result . errorCode ,
777+ result . errorDescription
778+ ) ,
779+ }
780+ : { } ) ,
757781 }
758782 }
759783
0 commit comments