e3682fd06f
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 7m31s
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m32s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m33s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m40s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m21s
Project CI / AI game creator shell Rust crates (push) Successful in 3m11s
Project CI / Native shell tests (push) Successful in 16m15s
Project CI / Frontend tests (push) Successful in 14m57s
Project CI / Repository checks (push) Successful in 14m48s
Project CI / Backend tests (push) Successful in 20m7s
Project CI / AI game creator shell web tests (push) Successful in 6m20s
移除服务器选择并按来源隔离登录凭据 新增官网客户端下载入口和匿名平台聚合接口 根据发布清单自动展示Windows与macOS首装包 补齐Mac首装元数据和上传顺序校验 同步定向测试与下载发布规范
659 lines
21 KiB
TypeScript
659 lines
21 KiB
TypeScript
import type { AuthUser } from '../../../../packages/shared/src/contracts/auth';
|
||
import { resolveTauriInvoke } from '../app/tauri';
|
||
import {
|
||
clearStoredAuthAccessToken,
|
||
getCurrentClientAuthUser,
|
||
getStoredAuthAccessToken,
|
||
isClientAuthAuthorityFailure,
|
||
refreshClientAuthAccessToken,
|
||
setStoredAuthAccessToken,
|
||
} from './clientAuth';
|
||
import { getClientServerBaseUrl } from './clientHttp';
|
||
import {
|
||
type ClientOperation,
|
||
createClientOperation,
|
||
transitionClientOperation,
|
||
} from './clientOperation';
|
||
|
||
function readStoredAccessTokenOrThrow(apiBaseUrl: string) {
|
||
const accessToken = getStoredAuthAccessToken(apiBaseUrl);
|
||
if (!accessToken) {
|
||
throw new Error('陶泥儿登录凭据缺失,请重新登录');
|
||
}
|
||
return accessToken;
|
||
}
|
||
|
||
type CommittedPlatformSession = {
|
||
user: AuthUser;
|
||
accessToken: string;
|
||
apiBaseUrl: string;
|
||
generation: number;
|
||
};
|
||
|
||
/** 原生写入:身份代次表达主体归属,revision 只表达写入顺序。 */
|
||
type PlatformNativeSessionWrite = {
|
||
identityGeneration: number;
|
||
revision: number;
|
||
};
|
||
|
||
export type PlatformSessionRefreshResult =
|
||
| { status: 'refreshed'; user: AuthUser; generation: number }
|
||
| { status: 'stale' }
|
||
| {
|
||
status: 'failed';
|
||
error: unknown;
|
||
/**
|
||
* 只有服务端明确否认当前身份(401/403,且收敛重试后仍失败)才为 true。
|
||
* 网络错误、5xx、网关错误和响应契约异常必须保留既有会话与 access token,
|
||
* 调用方不得据此把用户登出。
|
||
*/
|
||
authoritative: boolean;
|
||
};
|
||
|
||
type PlatformSessionRefreshListener = (
|
||
result: PlatformSessionRefreshResult,
|
||
) => void;
|
||
|
||
type PlatformSessionGenerationListener = (generation: number) => void;
|
||
|
||
let platformAuthGeneration = 0;
|
||
/** 原生写入 revision:每次安装 / 清除都推进,用于拒绝迟到写入。 */
|
||
let platformNativeRevision = 0;
|
||
/** 原生身份代次:只在登录、切号、登出或新 authority epoch 推进,续期保持不变。 */
|
||
let platformNativeIdentityGeneration = 0;
|
||
let platformNativeSessionFloorPromise: Promise<{
|
||
identityGeneration: number;
|
||
revision: number;
|
||
}> | null = null;
|
||
let committedPlatformSession: CommittedPlatformSession | null = null;
|
||
let desiredPlatformSession: CommittedPlatformSession | null = null;
|
||
let platformSessionRefreshPromise: Promise<PlatformSessionRefreshResult> | null =
|
||
null;
|
||
let platformSessionOperation: ClientOperation<
|
||
'auth-transition',
|
||
{ userId: string | null }
|
||
> | null = null;
|
||
/**
|
||
* 本地会话写入的排队闸门。
|
||
*
|
||
* Rust 侧 `install_platform_session_in` / `clear_platform_session_in` 按 generation 单调校验:
|
||
* 更旧的 install 与更旧的 clear 都会被拒绝。因此渲染层必须保证的只有"新 generation 不被旧调用
|
||
* 无限挡住",而不需要让队列永远等下去。这里给闸门加一个上限:底层 invoke 迟迟不返回(例如
|
||
* Runner 卡住、IPC 不回调)时,后续登录/退出仍能继续推进,迟到的旧写入由 Rust 按代次拒绝。
|
||
*/
|
||
const PLATFORM_SESSION_NATIVE_MUTATION_ABANDONMENT_MS = 60_000;
|
||
let platformSessionNativeMutationGate: Promise<void> = Promise.resolve();
|
||
const platformSessionRefreshListeners =
|
||
new Set<PlatformSessionRefreshListener>();
|
||
const platformSessionGenerationListeners =
|
||
new Set<PlatformSessionGenerationListener>();
|
||
|
||
export function getPlatformSessionOperation() {
|
||
return platformSessionOperation;
|
||
}
|
||
|
||
function restoreCommittedAccessToken() {
|
||
if (committedPlatformSession?.accessToken) {
|
||
setStoredAuthAccessToken(
|
||
committedPlatformSession.accessToken,
|
||
committedPlatformSession.apiBaseUrl,
|
||
);
|
||
return;
|
||
}
|
||
clearStoredAuthAccessToken();
|
||
}
|
||
|
||
function restoreCurrentRendererAccessToken() {
|
||
if (!desiredPlatformSession) {
|
||
clearStoredAuthAccessToken();
|
||
return;
|
||
}
|
||
restoreCommittedAccessToken();
|
||
}
|
||
|
||
function notifyPlatformSessionRefresh(result: PlatformSessionRefreshResult) {
|
||
for (const listener of platformSessionRefreshListeners) {
|
||
listener(result);
|
||
}
|
||
}
|
||
|
||
function notifyPlatformSessionGeneration() {
|
||
for (const listener of platformSessionGenerationListeners) {
|
||
listener(platformAuthGeneration);
|
||
}
|
||
}
|
||
|
||
async function installNativePlatformSession(
|
||
session: CommittedPlatformSession,
|
||
write: PlatformNativeSessionWrite,
|
||
) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) return;
|
||
await invoke('install_platform_account_session', {
|
||
userId: session.user.id,
|
||
accessToken: session.accessToken,
|
||
apiBaseUrl: session.apiBaseUrl,
|
||
identityGeneration: write.identityGeneration,
|
||
revision: write.revision,
|
||
});
|
||
}
|
||
|
||
async function clearNativePlatformSession(write: PlatformNativeSessionWrite) {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) return;
|
||
await invoke('clear_platform_account_session', {
|
||
identityGeneration: write.identityGeneration,
|
||
revision: write.revision,
|
||
});
|
||
}
|
||
|
||
function waitForNativeMutationAbandonment(
|
||
settled: Promise<void>,
|
||
timeoutMs: number,
|
||
) {
|
||
let timerId: number | undefined;
|
||
const abandoned = new Promise<void>((resolve) => {
|
||
timerId = window.setTimeout(resolve, timeoutMs);
|
||
});
|
||
return Promise.race([settled, abandoned]).finally(() => {
|
||
if (timerId !== undefined) {
|
||
window.clearTimeout(timerId);
|
||
}
|
||
});
|
||
}
|
||
|
||
function enqueuePlatformSessionNativeMutation<T>(
|
||
operation: () => Promise<T>,
|
||
abandonmentMs = PLATFORM_SESSION_NATIVE_MUTATION_ABANDONMENT_MS,
|
||
): Promise<T> {
|
||
const pending = platformSessionNativeMutationGate
|
||
.catch(() => undefined)
|
||
.then(operation);
|
||
platformSessionNativeMutationGate = waitForNativeMutationAbandonment(
|
||
pending.then(
|
||
() => undefined,
|
||
() => undefined,
|
||
),
|
||
abandonmentMs,
|
||
);
|
||
return pending;
|
||
}
|
||
|
||
async function readNativePlatformSessionGenerationFloor() {
|
||
const invoke = resolveTauriInvoke();
|
||
if (!invoke) return { identityGeneration: 0, revision: 0 };
|
||
const state = await invoke<{
|
||
identityGeneration?: unknown;
|
||
revision?: unknown;
|
||
} | null>('read_platform_account_session_state');
|
||
// Browser/unit-test adapters commonly expose a no-op invoke that returns null
|
||
// for native-only read commands. They have no surviving Rust generation floor.
|
||
if (state === null || state === undefined) {
|
||
return { identityGeneration: 0, revision: 0 };
|
||
}
|
||
const identityGeneration = Number(state.identityGeneration ?? 0);
|
||
const revision = Number(state.revision ?? 0);
|
||
if (
|
||
!Number.isSafeInteger(identityGeneration) ||
|
||
identityGeneration < 0 ||
|
||
!Number.isSafeInteger(revision) ||
|
||
revision < 0
|
||
) {
|
||
throw new Error('本地运行时登录态写入下限无效,请重启客户端后重试');
|
||
}
|
||
return { identityGeneration, revision };
|
||
}
|
||
|
||
async function reserveNativePlatformSessionWrite(options: {
|
||
identityChange: boolean;
|
||
}): Promise<PlatformNativeSessionWrite> {
|
||
platformNativeSessionFloorPromise ??=
|
||
readNativePlatformSessionGenerationFloor();
|
||
let floor: { identityGeneration: number; revision: number };
|
||
try {
|
||
floor = await platformNativeSessionFloorPromise;
|
||
} catch (error) {
|
||
// 一次瞬时失败(IPC 抖动、Runner 刚重启)不能被缓存成"永久失败":否则本次渲染进程
|
||
// 内的后续登录/退出都会在同一个已 reject 的 promise 上失败,用户重试也不会重新读取。
|
||
platformNativeSessionFloorPromise = null;
|
||
throw error;
|
||
}
|
||
platformNativeRevision = Math.max(
|
||
platformNativeRevision + 1,
|
||
platformAuthGeneration,
|
||
floor.revision + 1,
|
||
);
|
||
// 同一账号的凭据续期必须复用当前身份代次;只有登录、切号、登出或新 authority epoch
|
||
// 才允许推进它,否则在途生成 operation 会被自己的续期判成"旧账号请求"。
|
||
platformNativeIdentityGeneration = options.identityChange
|
||
? Math.max(
|
||
platformNativeIdentityGeneration + 1,
|
||
floor.identityGeneration + 1,
|
||
)
|
||
: Math.max(platformNativeIdentityGeneration, floor.identityGeneration);
|
||
return {
|
||
identityGeneration: platformNativeIdentityGeneration,
|
||
revision: platformNativeRevision,
|
||
};
|
||
}
|
||
|
||
async function reconcileNativePlatformSessionToCurrentAuthority() {
|
||
for (;;) {
|
||
const authoritativeGeneration = platformAuthGeneration;
|
||
const authoritativeSession = desiredPlatformSession
|
||
? { ...desiredPlatformSession }
|
||
: null;
|
||
// 只有权威会话与上一次已提交会话不是同一身份时才推进身份代次:同账号续期后的对账
|
||
// 仍然算同一身份,不得让在途 operation 失效。
|
||
const identityChange =
|
||
!committedPlatformSession ||
|
||
!authoritativeSession ||
|
||
committedPlatformSession.user.id !== authoritativeSession.user.id ||
|
||
committedPlatformSession.apiBaseUrl !== authoritativeSession.apiBaseUrl;
|
||
const write = await reserveNativePlatformSessionWrite({ identityChange });
|
||
restoreCurrentRendererAccessToken();
|
||
try {
|
||
if (authoritativeSession) {
|
||
await installNativePlatformSession(authoritativeSession, write);
|
||
} else {
|
||
await clearNativePlatformSession(write);
|
||
}
|
||
} catch (error) {
|
||
if (platformAuthGeneration === authoritativeGeneration) {
|
||
committedPlatformSession = null;
|
||
desiredPlatformSession = null;
|
||
restoreCommittedAccessToken();
|
||
notifyPlatformSessionGeneration();
|
||
}
|
||
throw error;
|
||
}
|
||
if (platformAuthGeneration !== authoritativeGeneration) {
|
||
continue;
|
||
}
|
||
committedPlatformSession = authoritativeSession
|
||
? {
|
||
...authoritativeSession,
|
||
generation: authoritativeGeneration,
|
||
}
|
||
: null;
|
||
desiredPlatformSession = committedPlatformSession
|
||
? { ...committedPlatformSession }
|
||
: null;
|
||
restoreCommittedAccessToken();
|
||
notifyPlatformSessionGeneration();
|
||
return;
|
||
}
|
||
}
|
||
|
||
function resolvePlatformApiBaseUrl() {
|
||
return getClientServerBaseUrl();
|
||
}
|
||
|
||
async function commitNativePlatformSession(
|
||
candidate: CommittedPlatformSession,
|
||
authorityGeneration: number,
|
||
options: { identityChange: boolean },
|
||
): Promise<CommittedPlatformSession | null> {
|
||
const write = await reserveNativePlatformSessionWrite({
|
||
identityChange: options.identityChange,
|
||
});
|
||
try {
|
||
await installNativePlatformSession(candidate, write);
|
||
} catch (error) {
|
||
if (platformAuthGeneration === authorityGeneration) {
|
||
desiredPlatformSession = committedPlatformSession
|
||
? { ...committedPlatformSession }
|
||
: null;
|
||
}
|
||
await reconcileNativePlatformSessionToCurrentAuthority();
|
||
if (platformAuthGeneration !== authorityGeneration) return null;
|
||
throw error;
|
||
}
|
||
if (platformAuthGeneration !== authorityGeneration) {
|
||
await reconcileNativePlatformSessionToCurrentAuthority();
|
||
return null;
|
||
}
|
||
committedPlatformSession = candidate;
|
||
desiredPlatformSession = { ...candidate };
|
||
restoreCommittedAccessToken();
|
||
if (options.identityChange) {
|
||
notifyPlatformSessionGeneration();
|
||
}
|
||
return candidate;
|
||
}
|
||
|
||
async function commitPlatformSession(
|
||
user: AuthUser,
|
||
accessToken: string,
|
||
apiBaseUrl: string,
|
||
expectedGeneration: number,
|
||
): Promise<CommittedPlatformSession | null> {
|
||
if (platformAuthGeneration !== expectedGeneration) {
|
||
restoreCurrentRendererAccessToken();
|
||
return null;
|
||
}
|
||
const candidate: CommittedPlatformSession = {
|
||
user,
|
||
accessToken,
|
||
apiBaseUrl,
|
||
generation: expectedGeneration + 1,
|
||
};
|
||
// Reserve a new generation so older refresh/login work becomes stale, but keep the previous
|
||
// committed identity authoritative until Rust and Runner have accepted the candidate.
|
||
platformAuthGeneration = candidate.generation;
|
||
desiredPlatformSession = { ...candidate };
|
||
notifyPlatformSessionGeneration();
|
||
restoreCommittedAccessToken();
|
||
return commitNativePlatformSession(candidate, candidate.generation, {
|
||
identityChange: true,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 同一身份的凭据续期:只替换 access token 与 native 写入 revision,保持身份代次不变,
|
||
* 因此在途生成、编辑、上传、确认和下载 operation 不会被自己的续期判成旧账号请求。
|
||
*/
|
||
async function commitPlatformCredentialRefresh(
|
||
user: AuthUser,
|
||
accessToken: string,
|
||
apiBaseUrl: string,
|
||
expectedGeneration: number,
|
||
): Promise<CommittedPlatformSession | null> {
|
||
if (platformAuthGeneration !== expectedGeneration) {
|
||
restoreCurrentRendererAccessToken();
|
||
return null;
|
||
}
|
||
const current = committedPlatformSession;
|
||
if (!current) {
|
||
restoreCurrentRendererAccessToken();
|
||
return null;
|
||
}
|
||
if (current.user.id !== user.id || current.apiBaseUrl !== apiBaseUrl) {
|
||
// 身份已经变化:按换号路径重新提交,不能复用旧身份代次。
|
||
return commitPlatformSession(
|
||
user,
|
||
accessToken,
|
||
apiBaseUrl,
|
||
expectedGeneration,
|
||
);
|
||
}
|
||
const candidate: CommittedPlatformSession = {
|
||
user,
|
||
accessToken,
|
||
apiBaseUrl,
|
||
generation: current.generation,
|
||
};
|
||
desiredPlatformSession = { ...candidate };
|
||
restoreCurrentRendererAccessToken();
|
||
return commitNativePlatformSession(candidate, expectedGeneration, {
|
||
identityChange: false,
|
||
});
|
||
}
|
||
|
||
export function currentPlatformSessionGeneration() {
|
||
return platformAuthGeneration;
|
||
}
|
||
|
||
export function currentPlatformSessionApiBaseUrl() {
|
||
return committedPlatformSession?.apiBaseUrl || resolvePlatformApiBaseUrl();
|
||
}
|
||
|
||
/** 仅供测试断言:同一账号续期不得推进这个身份代次。 */
|
||
export function currentPlatformNativeIdentityGenerationForTests() {
|
||
return platformNativeIdentityGeneration;
|
||
}
|
||
|
||
export function beginPlatformSessionTransition() {
|
||
platformAuthGeneration += 1;
|
||
desiredPlatformSession = committedPlatformSession
|
||
? { ...committedPlatformSession }
|
||
: null;
|
||
notifyPlatformSessionGeneration();
|
||
return platformAuthGeneration;
|
||
}
|
||
|
||
export function beginPlatformSessionClearTransition() {
|
||
platformAuthGeneration += 1;
|
||
desiredPlatformSession = null;
|
||
notifyPlatformSessionGeneration();
|
||
return platformAuthGeneration;
|
||
}
|
||
|
||
export async function commitAuthenticatedPlatformSession(
|
||
user: AuthUser,
|
||
expectedGeneration: number,
|
||
apiBaseUrl = resolvePlatformApiBaseUrl(),
|
||
) {
|
||
const accessToken = readStoredAccessTokenOrThrow(apiBaseUrl);
|
||
const operation = createClientOperation(
|
||
'auth-transition',
|
||
{ userId: user.id },
|
||
{
|
||
scope: { apiBaseUrl, sessionGeneration: expectedGeneration },
|
||
deadlineMs: 45_000,
|
||
cancellable: false,
|
||
},
|
||
);
|
||
platformSessionOperation = transitionClientOperation(operation, 'runner');
|
||
return enqueuePlatformSessionNativeMutation(async () => {
|
||
try {
|
||
const session = await commitPlatformSession(
|
||
user,
|
||
accessToken,
|
||
apiBaseUrl,
|
||
expectedGeneration,
|
||
);
|
||
if (!session) {
|
||
platformSessionOperation = transitionClientOperation(
|
||
operation,
|
||
'unknown',
|
||
);
|
||
return null;
|
||
}
|
||
platformSessionOperation = transitionClientOperation(
|
||
operation,
|
||
'success',
|
||
{ scope: { sessionGeneration: session.generation } },
|
||
);
|
||
return session.generation;
|
||
} catch (error) {
|
||
platformSessionOperation = transitionClientOperation(
|
||
operation,
|
||
'retryable-failure',
|
||
);
|
||
throw error;
|
||
}
|
||
});
|
||
}
|
||
|
||
export async function refreshPlatformSessionForGeneration(
|
||
expectedGeneration: number,
|
||
apiBaseUrl = resolvePlatformApiBaseUrl(),
|
||
) {
|
||
const token = await refreshClientAuthAccessToken(apiBaseUrl);
|
||
if (platformAuthGeneration !== expectedGeneration) {
|
||
restoreCurrentRendererAccessToken();
|
||
return null;
|
||
}
|
||
return token;
|
||
}
|
||
|
||
export function requestPlatformSessionRefresh(expectedUserId?: string) {
|
||
if (platformSessionRefreshPromise) return platformSessionRefreshPromise;
|
||
|
||
const expectedGeneration = platformAuthGeneration;
|
||
const apiBaseUrl =
|
||
committedPlatformSession?.apiBaseUrl || resolvePlatformApiBaseUrl();
|
||
const expectedSessionUserId =
|
||
expectedUserId?.trim() || committedPlatformSession?.user.id || '';
|
||
platformSessionRefreshPromise =
|
||
(async (): Promise<PlatformSessionRefreshResult> => {
|
||
try {
|
||
const refreshed = await refreshPlatformSessionForGeneration(
|
||
expectedGeneration,
|
||
apiBaseUrl,
|
||
);
|
||
if (!refreshed) return { status: 'stale' };
|
||
const user = await getCurrentClientAuthUser(apiBaseUrl);
|
||
if (
|
||
platformAuthGeneration !== expectedGeneration ||
|
||
!user ||
|
||
(expectedSessionUserId && user.id !== expectedSessionUserId)
|
||
) {
|
||
restoreCurrentRendererAccessToken();
|
||
return { status: 'stale' };
|
||
}
|
||
// 同一账号的续期只更新凭据:身份代次保持不变,因此在途生成 operation 不会被
|
||
// 自己的续期判成旧账号请求。
|
||
const committed = await enqueuePlatformSessionNativeMutation(() =>
|
||
commitPlatformCredentialRefresh(
|
||
user,
|
||
readStoredAccessTokenOrThrow(apiBaseUrl),
|
||
apiBaseUrl,
|
||
expectedGeneration,
|
||
),
|
||
);
|
||
if (committed === null) return { status: 'stale' };
|
||
return {
|
||
status: 'refreshed',
|
||
user,
|
||
generation: committed.generation,
|
||
};
|
||
} catch (error) {
|
||
if (platformAuthGeneration !== expectedGeneration) {
|
||
restoreCurrentRendererAccessToken();
|
||
return { status: 'stale' };
|
||
}
|
||
let failure = error;
|
||
const currentOwnerUserId = committedPlatformSession?.user.id || '';
|
||
if (
|
||
currentOwnerUserId &&
|
||
currentOwnerUserId !== expectedSessionUserId
|
||
) {
|
||
restoreCurrentRendererAccessToken();
|
||
return { status: 'stale' };
|
||
}
|
||
// 只有服务端明确否认当前身份才算权威失效。网络错误、5xx、网关错误和响应契约
|
||
// 异常必须保留既有会话与 access token,否则一次后台保活抖动就会把用户登出。
|
||
const authoritative = isClientAuthAuthorityFailure(error);
|
||
if (
|
||
authoritative &&
|
||
(!currentOwnerUserId || currentOwnerUserId === expectedSessionUserId)
|
||
) {
|
||
const clearGeneration = beginPlatformSessionClearTransition();
|
||
try {
|
||
await clearCommittedPlatformSession(clearGeneration);
|
||
} catch (clearError) {
|
||
failure = clearError;
|
||
}
|
||
return { status: 'failed', error: failure, authoritative: true };
|
||
}
|
||
restoreCurrentRendererAccessToken();
|
||
return { status: 'failed', error: failure, authoritative: false };
|
||
}
|
||
})().then((result) => {
|
||
notifyPlatformSessionRefresh(result);
|
||
return result;
|
||
});
|
||
platformSessionRefreshPromise.finally(() => {
|
||
platformSessionRefreshPromise = null;
|
||
});
|
||
return platformSessionRefreshPromise;
|
||
}
|
||
|
||
export function subscribePlatformSessionRefresh(
|
||
listener: PlatformSessionRefreshListener,
|
||
) {
|
||
platformSessionRefreshListeners.add(listener);
|
||
return () => {
|
||
platformSessionRefreshListeners.delete(listener);
|
||
};
|
||
}
|
||
|
||
export function subscribePlatformSessionGeneration(
|
||
listener: PlatformSessionGenerationListener,
|
||
) {
|
||
platformSessionGenerationListeners.add(listener);
|
||
return () => {
|
||
platformSessionGenerationListeners.delete(listener);
|
||
};
|
||
}
|
||
|
||
export async function clearCommittedPlatformSession(generation: number) {
|
||
const operation = createClientOperation(
|
||
'auth-transition',
|
||
{ userId: null },
|
||
{
|
||
scope: { sessionGeneration: generation },
|
||
deadlineMs: 45_000,
|
||
cancellable: false,
|
||
},
|
||
);
|
||
platformSessionOperation = transitionClientOperation(operation, 'runner');
|
||
return enqueuePlatformSessionNativeMutation(async () => {
|
||
try {
|
||
if (platformAuthGeneration !== generation) {
|
||
await reconcileNativePlatformSessionToCurrentAuthority();
|
||
platformSessionOperation = transitionClientOperation(
|
||
operation,
|
||
'unknown',
|
||
);
|
||
return;
|
||
}
|
||
desiredPlatformSession = null;
|
||
const write = await reserveNativePlatformSessionWrite({
|
||
identityChange: true,
|
||
});
|
||
try {
|
||
await clearNativePlatformSession(write);
|
||
} catch {
|
||
if (platformAuthGeneration === generation) {
|
||
desiredPlatformSession = null;
|
||
}
|
||
await reconcileNativePlatformSessionToCurrentAuthority();
|
||
platformSessionOperation = transitionClientOperation(
|
||
operation,
|
||
'retryable-failure',
|
||
);
|
||
return;
|
||
}
|
||
if (platformAuthGeneration !== generation) {
|
||
await reconcileNativePlatformSessionToCurrentAuthority();
|
||
platformSessionOperation = transitionClientOperation(
|
||
operation,
|
||
'unknown',
|
||
);
|
||
return;
|
||
}
|
||
committedPlatformSession = null;
|
||
desiredPlatformSession = null;
|
||
restoreCommittedAccessToken();
|
||
notifyPlatformSessionGeneration();
|
||
platformSessionOperation = transitionClientOperation(
|
||
operation,
|
||
'success',
|
||
);
|
||
} catch (error) {
|
||
platformSessionOperation = transitionClientOperation(
|
||
operation,
|
||
'retryable-failure',
|
||
);
|
||
throw error;
|
||
}
|
||
});
|
||
}
|
||
|
||
export function resetPlatformSessionStateForTests() {
|
||
platformAuthGeneration = 0;
|
||
platformNativeRevision = 0;
|
||
platformNativeIdentityGeneration = 0;
|
||
platformNativeSessionFloorPromise = null;
|
||
committedPlatformSession = null;
|
||
desiredPlatformSession = null;
|
||
platformSessionRefreshPromise = null;
|
||
platformSessionOperation = null;
|
||
platformSessionNativeMutationGate = Promise.resolve();
|
||
platformSessionRefreshListeners.clear();
|
||
platformSessionGenerationListeners.clear();
|
||
}
|