Files
client-web/src/api/crossTabRefresh.ts
T
2026-08-27 10:44:14 +08:00

57 lines
2.0 KiB
TypeScript

export interface CrossTabRefreshOptions<T> {
lockName: string
lockTimeoutMs?: number
observedRefreshToken: string
currentRefreshToken: () => string
reuseCurrentSession: () => Promise<T>
refresh: (refreshToken: string) => Promise<T>
}
export class CrossTabRefreshTimeoutError extends Error {
constructor() {
super('登录状态刷新等待超时,请重新登录')
this.name = 'CrossTabRefreshTimeoutError'
}
}
const DEFAULT_LOCK_TIMEOUT_MS = 20_000
export async function coordinateCrossTabRefresh<T>(options: CrossTabRefreshOptions<T>): Promise<T> {
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)
}
}