Prevent terminal auth spinner deadlock

This commit is contained in:
2569718930@qq.com
2026-06-01 12:36:29 +08:00
parent fb6c354317
commit 49cb0bb42d
5 changed files with 360 additions and 18 deletions
@@ -124,6 +124,45 @@ export async function runTests() {
"terminal auth bootstrap should accept an authenticated cookie profile immediately",
);
const slowDegradedSession = deferred<{ data: { session: { access_token: string } | null } }>();
const timedCookieResult = await loadTerminalAuthProfile({
hasSupabasePublicEnv: true,
getSession: () => slowDegradedSession.promise,
timeoutMs: 1,
loadAuthProfile: (accessToken) => {
assert(!accessToken, "timeout fallback should use the settled cookie profile when bearer session is still pending");
return Promise.resolve({
authenticated: true,
user_id: "cookie-user",
subscription_active: null,
degraded_auth_profile: true,
});
},
});
assert(
timedCookieResult.user_id === "cookie-user" &&
timedCookieResult.subscription_active === null,
"terminal auth bootstrap timeout must resolve with a known degraded cookie profile instead of spinning forever",
);
const neverCookie = deferred<TerminalAuthProfilePayload>();
const neverSession = deferred<{ data: { session: { access_token: string } | null } }>();
let timeoutRejected = false;
try {
await loadTerminalAuthProfile({
hasSupabasePublicEnv: true,
getSession: () => neverSession.promise,
timeoutMs: 1,
loadAuthProfile: () => neverCookie.promise,
});
} catch (error) {
timeoutRejected = String(error).includes("Terminal auth bootstrap timeout");
}
assert(
timeoutRejected,
"terminal auth bootstrap must reject instead of leaving the loading screen unresolved when no profile request settles",
);
const delayedBearerSession = deferred<{ data: { session: { access_token: string } } }>();
let coldStartSettled = false;
const coldStartResultPromise = loadTerminalAuthProfile({
@@ -17,6 +17,7 @@ type LoadTerminalAuthProfileOptions = {
accessToken?: string | null,
options?: { preferSnapshot?: boolean },
) => Promise<TerminalAuthProfilePayload>;
timeoutMs?: number;
};
type SettledProfile =
@@ -87,11 +88,14 @@ export async function loadTerminalAuthProfile({
getSession,
hasSupabasePublicEnv,
loadAuthProfile,
timeoutMs = 6500,
}: LoadTerminalAuthProfileOptions) {
let resolvedAuthenticated = false;
let resolveAuthenticated:
| ((payload: TerminalAuthProfilePayload) => void)
| null = null;
let latestCookiePayload: TerminalAuthProfilePayload | null = null;
let latestBearerPayload: TerminalAuthProfilePayload | null = null;
const authenticatedProfile = new Promise<TerminalAuthProfilePayload>((resolve) => {
resolveAuthenticated = resolve;
@@ -105,6 +109,7 @@ export async function loadTerminalAuthProfile({
const cookieProfile = settleProfile(
loadAuthProfile(null, { preferSnapshot: true }).then((payload) => {
latestCookiePayload = payload;
resolveIfAuthenticated(payload);
return payload;
}),
@@ -120,6 +125,7 @@ export async function loadTerminalAuthProfile({
).trim();
if (!accessToken) return null;
const payload = await loadAuthProfile(accessToken, { preferSnapshot: true });
latestBearerPayload = payload;
resolveIfAuthenticated(payload);
return payload;
})(),
@@ -129,5 +135,20 @@ export async function loadTerminalAuthProfile({
([cookieResult, bearerResult]) => firstKnownProfile(cookieResult, bearerResult),
);
return Promise.race([authenticatedProfile, fallbackProfile]);
const timeoutProfile = new Promise<TerminalAuthProfilePayload>((resolve, reject) => {
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return;
globalThis.setTimeout(() => {
if (latestBearerPayload) {
resolve(latestBearerPayload);
return;
}
if (latestCookiePayload) {
resolve(latestCookiePayload);
return;
}
reject(new Error("Terminal auth bootstrap timeout"));
}, timeoutMs);
});
return Promise.race([authenticatedProfile, fallbackProfile, timeoutProfile]);
}