From 7be285ef89daac151bd3badfa01f2bff67b89206 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 8 Apr 2026 13:28:08 +0900 Subject: [PATCH] feat(oauth): add per-server refresh mutex Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/features/mcp-oauth/refresh-mutex.ts | 58 +++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/features/mcp-oauth/refresh-mutex.ts diff --git a/src/features/mcp-oauth/refresh-mutex.ts b/src/features/mcp-oauth/refresh-mutex.ts new file mode 100644 index 000000000..3b7c3e710 --- /dev/null +++ b/src/features/mcp-oauth/refresh-mutex.ts @@ -0,0 +1,58 @@ +import type { OAuthTokenData } from "./storage" + +/** + * Per-server OAuth refresh mutex to prevent concurrent refresh race conditions. + * + * When multiple operations need to refresh a token for the same server, + * this ensures only one refresh request is made and all waiters receive + * the same result. + */ + +const ongoingRefreshes = new Map>() + +/** + * Execute a token refresh with per-server mutual exclusion. + * + * If a refresh is already in progress for the given server, this will + * return the same promise to all concurrent callers. Once the refresh + * completes (success or failure), the lock is released. + * + * @param serverUrl - The OAuth server URL (used as mutex key) + * @param refreshFn - The actual refresh operation to execute + * @returns Promise that resolves to the new token data + */ +export async function withRefreshMutex( + serverUrl: string, + refreshFn: () => Promise, +): Promise { + const existing = ongoingRefreshes.get(serverUrl) + if (existing) { + return existing + } + + const refreshPromise = refreshFn().finally(() => { + ongoingRefreshes.delete(serverUrl) + }) + + ongoingRefreshes.set(serverUrl, refreshPromise) + return refreshPromise +} + +/** + * Check if a refresh is currently in progress for a server. + * + * @param serverUrl - The OAuth server URL + * @returns true if a refresh operation is active + */ +export function isRefreshInProgress(serverUrl: string): boolean { + return ongoingRefreshes.has(serverUrl) +} + +/** + * Get the number of servers currently undergoing token refresh. + * + * @returns Number of active refresh operations + */ +export function getActiveRefreshCount(): number { + return ongoingRefreshes.size +}