import { create, type StoreApi, type UseBoundStore } from 'zustand'; import type { ProfileMudPointBalance, ProfileRechargeCenterResponse, } from '../contracts/runtime'; export type MudPointBalanceStatus = 'idle' | 'loading' | 'ready' | 'error'; export type ProfileWalletApi = { getRechargeCenter( signal?: AbortSignal, ): Promise; }; export type ProfileWalletBalanceSnapshot = Readonly<{ ownerUserId: string; ownerVersion: number; invalidationVersion: number; operationSequence: number; }>; export type ProfileWalletStore = { ownerUserId: string | null; mudPointBalance: ProfileMudPointBalance | null; legacyWalletBalance: number | null; mudPointBalanceStatus: MudPointBalanceStatus; mudPointBalanceError: string; setWalletOwner: (userId: string | null) => void; captureWalletBalanceSnapshot: ( ownerUserId: string, ) => ProfileWalletBalanceSnapshot | null; applyWalletBalanceSnapshot: ( snapshot: ProfileWalletBalanceSnapshot, balance: ProfileMudPointBalance, ) => boolean; applyLegacyWalletBalanceSnapshot: ( snapshot: ProfileWalletBalanceSnapshot, balance: number, ) => boolean; onWalletBalanceMayHaveChanged: () => Promise; resetWalletBalance: () => void; }; const EMPTY_WALLET_STATE = { mudPointBalance: null, legacyWalletBalance: null, mudPointBalanceStatus: 'idle', mudPointBalanceError: '', } as const; export function createProfileWalletStore( api: ProfileWalletApi, ): UseBoundStore> { let refreshVersion = 0; let settledRefreshVersion = 0; let ownerVersion = 0; let operationSequence = 0; let latestAppliedOperationSequence = 0; let requestGeneration = 0; let activeRefresh: Promise | null = null; let activeAbortController: AbortController | null = null; const invalidateActiveRefresh = () => { requestGeneration += 1; activeAbortController?.abort(); activeAbortController = null; activeRefresh = null; }; return create((set, get) => ({ ownerUserId: null, ...EMPTY_WALLET_STATE, setWalletOwner: (userId) => { const normalizedUserId = userId?.trim() || null; if (get().ownerUserId === normalizedUserId) { return; } ownerVersion += 1; latestAppliedOperationSequence = 0; invalidateActiveRefresh(); settledRefreshVersion = refreshVersion; set({ ownerUserId: normalizedUserId, ...EMPTY_WALLET_STATE, }); }, captureWalletBalanceSnapshot: (ownerUserId) => { const normalizedOwnerUserId = ownerUserId.trim(); if ( !normalizedOwnerUserId || get().ownerUserId !== normalizedOwnerUserId ) { return null; } return { ownerUserId: normalizedOwnerUserId, ownerVersion, invalidationVersion: refreshVersion, operationSequence: ++operationSequence, }; }, applyWalletBalanceSnapshot: (snapshot, balance) => { if ( get().ownerUserId !== snapshot.ownerUserId || ownerVersion !== snapshot.ownerVersion || snapshot.invalidationVersion < refreshVersion || snapshot.invalidationVersion < settledRefreshVersion || snapshot.operationSequence < latestAppliedOperationSequence ) { return false; } invalidateActiveRefresh(); latestAppliedOperationSequence = Math.max( latestAppliedOperationSequence, snapshot.operationSequence, ); settledRefreshVersion = snapshot.invalidationVersion; set({ mudPointBalance: balance, legacyWalletBalance: balance.totalPoints, mudPointBalanceStatus: 'ready', mudPointBalanceError: '', }); return true; }, applyLegacyWalletBalanceSnapshot: (snapshot, balance) => { if ( !Number.isFinite(balance) || get().ownerUserId !== snapshot.ownerUserId || ownerVersion !== snapshot.ownerVersion || snapshot.invalidationVersion < refreshVersion || snapshot.invalidationVersion < settledRefreshVersion || snapshot.operationSequence < latestAppliedOperationSequence ) { return false; } invalidateActiveRefresh(); latestAppliedOperationSequence = Math.max( latestAppliedOperationSequence, snapshot.operationSequence, ); settledRefreshVersion = snapshot.invalidationVersion; set({ mudPointBalance: null, legacyWalletBalance: balance, mudPointBalanceStatus: 'ready', mudPointBalanceError: '', }); return true; }, onWalletBalanceMayHaveChanged: () => { refreshVersion += 1; if (activeRefresh) { return activeRefresh; } const abortController = new AbortController(); const refresh = (async () => { while (settledRefreshVersion < refreshVersion) { const ownerUserId = get().ownerUserId; const requestedVersion = refreshVersion; const generation = requestGeneration; if (!ownerUserId) { settledRefreshVersion = requestedVersion; return; } set({ mudPointBalanceStatus: 'loading', mudPointBalanceError: '' }); try { const center = await api.getRechargeCenter(abortController.signal); if (generation !== requestGeneration) { return; } if (requestedVersion !== refreshVersion) { settledRefreshVersion = requestedVersion; continue; } if (!center.mudPointBalance) { if (Number.isFinite(center.walletBalance)) { set({ mudPointBalance: null, legacyWalletBalance: center.walletBalance, }); } throw new Error('充值中心响应缺少泥点余额'); } settledRefreshVersion = requestedVersion; set({ mudPointBalance: center.mudPointBalance, legacyWalletBalance: center.mudPointBalance.totalPoints, mudPointBalanceStatus: 'ready', mudPointBalanceError: '', }); } catch { if (generation !== requestGeneration) { return; } if (requestedVersion !== refreshVersion) { settledRefreshVersion = requestedVersion; continue; } settledRefreshVersion = requestedVersion; set({ mudPointBalanceStatus: 'error', mudPointBalanceError: '泥点明细读取失败', }); } } })(); activeRefresh = refresh; activeAbortController = abortController; const clearActiveRefresh = () => { if (activeRefresh === refresh) { activeRefresh = null; } if (activeAbortController === abortController) { activeAbortController = null; } }; void refresh.then(clearActiveRefresh, clearActiveRefresh); return refresh; }, resetWalletBalance: () => { ownerVersion += 1; latestAppliedOperationSequence = 0; invalidateActiveRefresh(); settledRefreshVersion = refreshVersion; set({ ownerUserId: null, ...EMPTY_WALLET_STATE }); }, })); }