export interface CrossTabRefreshOptions { lockName: string lockTimeoutMs?: number observedRefreshToken: string currentRefreshToken: () => string reuseCurrentSession: () => Promise refresh: (refreshToken: string) => Promise } export class CrossTabRefreshTimeoutError extends Error { constructor() { super('登录状态刷新等待超时,请重新登录') this.name = 'CrossTabRefreshTimeoutError' } } const DEFAULT_LOCK_TIMEOUT_MS = 20_000 export async function coordinateCrossTabRefresh(options: CrossTabRefreshOptions): Promise { const run = async () => { const currentRefreshToken = options.currentRefreshToken() if (currentRefreshToken && currentRefreshToken !== options.observedRefreshToken) { return options.reuseCurrentSession() } return options.refresh(currentRefreshToken || options.observedRefreshToken) } if (typeof navigator === 'undefined' || !navigator.locks) { try { return await run() } catch (error) { if (!options.currentRefreshToken()) throw error // Web Locks requires a secure context. On plain HTTP, briefly allow another tab's // successful rotation to reach shared storage before treating this session as expired. for (let attempt = 0; attempt < 20; attempt += 1) { await new Promise((resolve) => setTimeout(resolve, 50)) const currentRefreshToken = options.currentRefreshToken() if (!currentRefreshToken) throw error if (currentRefreshToken && currentRefreshToken !== options.observedRefreshToken) { return options.reuseCurrentSession() } } throw error } } const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS) try { return await navigator.locks.request(options.lockName, { signal: controller.signal }, run) } catch (error) { if (controller.signal.aborted) throw new CrossTabRefreshTimeoutError() throw error } finally { clearTimeout(timeout) } }