fix(web): localStorage safety, default fetch timeout, visibility-aware polling

This commit is contained in:
2026-04-29 12:00:40 +08:00
parent 7377c2135f
commit 357d08f2e3
5 changed files with 210 additions and 43 deletions
+35 -20
View File
@@ -1,6 +1,7 @@
import { ApiException, type ApiError } from './types'
const BASE = '' // 同源;dev 走 vite proxy
const DEFAULT_TIMEOUT_MS = 30_000
interface RequestOptions {
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE'
@@ -27,29 +28,43 @@ export async function request<T>(path: string, opts: RequestOptions = {}): Promi
const token = authTokenProvider()
if (token) headers['Authorization'] = `Bearer ${token}`
const resp = await fetch(BASE + path, {
method: opts.method || 'GET',
headers,
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
signal: opts.signal,
})
if (resp.status === 401) {
onUnauthorized()
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS)
if (opts.signal) {
if (opts.signal.aborted) {
controller.abort()
} else {
opts.signal.addEventListener('abort', () => controller.abort(), { once: true })
}
}
if (resp.status === 204) {
return undefined as T
}
try {
const resp = await fetch(BASE + path, {
method: opts.method || 'GET',
headers,
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
signal: controller.signal,
})
const text = await resp.text()
const data = text ? JSON.parse(text) : null
if (resp.status === 401) {
onUnauthorized()
}
if (!resp.ok) {
throw new ApiException(
resp.status,
(data as ApiError) || { code: 'unknown', message: resp.statusText },
)
if (resp.status === 204) {
return undefined as T
}
const text = await resp.text()
const data = text ? JSON.parse(text) : null
if (!resp.ok) {
throw new ApiException(
resp.status,
(data as ApiError) || { code: 'unknown', message: resp.statusText },
)
}
return data as T
} finally {
clearTimeout(timer)
}
return data as T
}