R65 A1-A6:Session 稳定性与 409 资源错误修复
ci-e2e / e2e (push) Waiting to run
ci-minimal / check-minimal (push) Waiting to run

这个提交包含在:
2026-04-16 20:23:54 +08:00
父节点 bc6e4955b3
当前提交 8353efcbd7
共修改 7 个文件,包含 425 行新增和 11 行删除
+45 -8
查看文件
@@ -9,7 +9,14 @@ import {
useState,
} from "react";
import { configureHttpAuth } from "../api/http";
import type { AuthState, LoginResponse, MeResponse, ProfileCompleteness } from "../api/types";
import {
isApiError,
isSessionExpiredError,
type AuthState,
type LoginResponse,
type MeResponse,
type ProfileCompleteness,
} from "../api/types";
import { changePassword as changePasswordApi } from "../api/auth";
import { clearPwaRuntimeCaches } from "../pwa";
import {
@@ -76,6 +83,16 @@ function clearSessionHint() {
document.cookie = "has_session=; path=/; max-age=0";
}
function wait(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
function isTerminalBootstrapError(error: unknown) {
return isSessionExpiredError(error) || (isApiError(error) && error.status === 401);
}
function normalizeProfileCompleteness(value: unknown): ProfileCompleteness {
if (!value || typeof value !== "object") {
return DEFAULT_PROFILE_COMPLETENESS;
@@ -353,19 +370,39 @@ export function AuthProvider({ children }: PropsWithChildren) {
if (!bootstrapPromiseRef.current) {
bootstrapPromiseRef.current = (async () => {
try {
const refreshed = await runRefresh();
const completeBootstrap = (refreshed: LoginResponse | null) => {
if (refreshed?.expiresIn) {
scheduleProactiveRefresh(refreshed.expiresIn);
}
return refreshed?.accessToken ?? null;
} catch {
if (isWindowAvailable() && window.navigator.onLine === false && offlineSnapshot) {
activateOfflineSnapshot(offlineSnapshot);
};
try {
return completeBootstrap(await runRefresh());
} catch (error) {
if (isTerminalBootstrapError(error)) {
clearSession();
return null;
}
try {
await wait(1000);
return completeBootstrap(await runRefresh());
} catch (retryError) {
if (isTerminalBootstrapError(retryError)) {
clearSession();
return null;
}
if (isWindowAvailable() && window.navigator.onLine === false && offlineSnapshot) {
activateOfflineSnapshot(offlineSnapshot);
return null;
}
clearSession();
return null;
}
clearSession();
return null;
} finally {
bootstrapPromiseRef.current = null;
}