57 lines
2.0 KiB
TypeScript
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)
|
|
}
|
|
}
|