修复钱包异步边界与任务陈旧状态
隔离账号切换后的充值响应并中止失效支付监听 防止乱序钱包快照回滚并保留旧版总额兜底 清理任务重试耗尽后的陈旧状态并补充回归测试 稳定测试夹具、用户错误提示与首帧加载状态 同步单一钱包 Store 的长期决策记录
This commit is contained in:
@@ -156,7 +156,7 @@ describe('createProfileWalletStore', () => {
|
||||
expect(store.getState()).toMatchObject({
|
||||
mudPointBalance: balance(55),
|
||||
mudPointBalanceStatus: 'error',
|
||||
mudPointBalanceError: 'network down',
|
||||
mudPointBalanceError: '泥点明细读取失败',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -173,7 +173,7 @@ describe('createProfileWalletStore', () => {
|
||||
expect(store.getState()).toMatchObject({
|
||||
mudPointBalance: null,
|
||||
mudPointBalanceStatus: 'error',
|
||||
mudPointBalanceError: '充值中心响应缺少泥点余额',
|
||||
mudPointBalanceError: '泥点明细读取失败',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -235,6 +235,43 @@ describe('createProfileWalletStore', () => {
|
||||
expect(store.getState().mudPointBalance?.totalPoints).toBe(100);
|
||||
});
|
||||
|
||||
test('rejects an older direct snapshot after a newer operation has applied', () => {
|
||||
const store = createProfileWalletStore({ getRechargeCenter: vi.fn() });
|
||||
store.getState().setWalletOwner('user-a');
|
||||
const olderSnapshot = store
|
||||
.getState()
|
||||
.captureWalletBalanceSnapshot('user-a');
|
||||
const newerSnapshot = store
|
||||
.getState()
|
||||
.captureWalletBalanceSnapshot('user-a');
|
||||
|
||||
expect(
|
||||
store.getState().applyWalletBalanceSnapshot(newerSnapshot!, balance(100)),
|
||||
).toBe(true);
|
||||
expect(
|
||||
store.getState().applyWalletBalanceSnapshot(olderSnapshot!, balance(20)),
|
||||
).toBe(false);
|
||||
expect(store.getState().mudPointBalance?.totalPoints).toBe(100);
|
||||
});
|
||||
|
||||
test('retains a legacy total without inventing a balance breakdown', async () => {
|
||||
const store = createProfileWalletStore({
|
||||
getRechargeCenter: vi.fn().mockResolvedValue({
|
||||
walletBalance: 37,
|
||||
} as ProfileRechargeCenterResponse),
|
||||
});
|
||||
store.getState().setWalletOwner('user-a');
|
||||
|
||||
await store.getState().onWalletBalanceMayHaveChanged();
|
||||
|
||||
expect(store.getState()).toMatchObject({
|
||||
legacyWalletBalance: 37,
|
||||
mudPointBalance: null,
|
||||
mudPointBalanceStatus: 'error',
|
||||
mudPointBalanceError: '泥点明细读取失败',
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects a snapshot after switching away from and back to the same owner', () => {
|
||||
const store = createProfileWalletStore({ getRechargeCenter: vi.fn() });
|
||||
store.getState().setWalletOwner('user-a');
|
||||
|
||||
@@ -17,11 +17,13 @@ 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;
|
||||
@@ -38,6 +40,7 @@ export type ProfileWalletStore = {
|
||||
|
||||
const EMPTY_WALLET_STATE = {
|
||||
mudPointBalance: null,
|
||||
legacyWalletBalance: null,
|
||||
mudPointBalanceStatus: 'idle',
|
||||
mudPointBalanceError: '',
|
||||
} as const;
|
||||
@@ -48,6 +51,8 @@ export function createProfileWalletStore(
|
||||
let refreshVersion = 0;
|
||||
let settledRefreshVersion = 0;
|
||||
let ownerVersion = 0;
|
||||
let operationSequence = 0;
|
||||
let latestAppliedOperationSequence = 0;
|
||||
let requestGeneration = 0;
|
||||
let activeRefresh: Promise<void> | null = null;
|
||||
let activeAbortController: AbortController | null = null;
|
||||
@@ -69,6 +74,7 @@ export function createProfileWalletStore(
|
||||
}
|
||||
|
||||
ownerVersion += 1;
|
||||
latestAppliedOperationSequence = 0;
|
||||
invalidateActiveRefresh();
|
||||
settledRefreshVersion = refreshVersion;
|
||||
set({
|
||||
@@ -89,6 +95,7 @@ export function createProfileWalletStore(
|
||||
ownerUserId: normalizedOwnerUserId,
|
||||
ownerVersion,
|
||||
invalidationVersion: refreshVersion,
|
||||
operationSequence: ++operationSequence,
|
||||
};
|
||||
},
|
||||
applyWalletBalanceSnapshot: (snapshot, balance) => {
|
||||
@@ -96,15 +103,21 @@ export function createProfileWalletStore(
|
||||
get().ownerUserId !== snapshot.ownerUserId ||
|
||||
ownerVersion !== snapshot.ownerVersion ||
|
||||
snapshot.invalidationVersion < refreshVersion ||
|
||||
snapshot.invalidationVersion < settledRefreshVersion
|
||||
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: '',
|
||||
});
|
||||
@@ -138,16 +151,20 @@ export function createProfileWalletStore(
|
||||
continue;
|
||||
}
|
||||
if (!center.mudPointBalance) {
|
||||
if (Number.isFinite(center.walletBalance)) {
|
||||
set({ legacyWalletBalance: center.walletBalance });
|
||||
}
|
||||
throw new Error('充值中心响应缺少泥点余额');
|
||||
}
|
||||
|
||||
settledRefreshVersion = requestedVersion;
|
||||
set({
|
||||
mudPointBalance: center.mudPointBalance,
|
||||
legacyWalletBalance: center.mudPointBalance.totalPoints,
|
||||
mudPointBalanceStatus: 'ready',
|
||||
mudPointBalanceError: '',
|
||||
});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
if (generation !== requestGeneration) {
|
||||
return;
|
||||
}
|
||||
@@ -159,8 +176,7 @@ export function createProfileWalletStore(
|
||||
settledRefreshVersion = requestedVersion;
|
||||
set({
|
||||
mudPointBalanceStatus: 'error',
|
||||
mudPointBalanceError:
|
||||
error instanceof Error ? error.message : '泥点明细读取失败',
|
||||
mudPointBalanceError: '泥点明细读取失败',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -180,6 +196,7 @@ export function createProfileWalletStore(
|
||||
},
|
||||
resetWalletBalance: () => {
|
||||
ownerVersion += 1;
|
||||
latestAppliedOperationSequence = 0;
|
||||
invalidateActiveRefresh();
|
||||
settledRefreshVersion = refreshVersion;
|
||||
set({ ownerUserId: null, ...EMPTY_WALLET_STATE });
|
||||
|
||||
Reference in New Issue
Block a user