R65 A1-A6:Session 稳定性与 409 资源错误修复
这个提交包含在:
@@ -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;
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户