修复钱包旧快照吞掉终态刷新
为钱包快照绑定请求发起时的 owner 生命周期与 invalidation 版本 拒绝过期普通 GET 快照覆盖余额或中止更新刷新 同步主站与 AI Game Creator 调用方及竞态回归测试 更新账户与充值基线约束
This commit is contained in:
@@ -19,7 +19,9 @@ function balance(totalPoints: number): ProfileMudPointBalance {
|
||||
}
|
||||
|
||||
function center(totalPoints: number): ProfileRechargeCenterResponse {
|
||||
return { mudPointBalance: balance(totalPoints) } as ProfileRechargeCenterResponse;
|
||||
return {
|
||||
mudPointBalance: balance(totalPoints),
|
||||
} as ProfileRechargeCenterResponse;
|
||||
}
|
||||
|
||||
function deferred<T>() {
|
||||
@@ -91,7 +93,9 @@ describe('createProfileWalletStore', () => {
|
||||
store.getState().setWalletOwner('user-a');
|
||||
|
||||
const refresh = store.getState().onWalletBalanceMayHaveChanged();
|
||||
store.getState().applyWalletBalanceSnapshot('user-a', balance(80));
|
||||
const snapshot = store.getState().captureWalletBalanceSnapshot('user-a');
|
||||
expect(snapshot).not.toBeNull();
|
||||
store.getState().applyWalletBalanceSnapshot(snapshot!, balance(80));
|
||||
pending.resolve(center(20));
|
||||
await refresh;
|
||||
|
||||
@@ -114,7 +118,8 @@ describe('createProfileWalletStore', () => {
|
||||
});
|
||||
const store = createProfileWalletStore({ getRechargeCenter });
|
||||
store.getState().setWalletOwner('user-a');
|
||||
store.getState().applyWalletBalanceSnapshot('user-a', balance(50));
|
||||
const snapshot = store.getState().captureWalletBalanceSnapshot('user-a');
|
||||
store.getState().applyWalletBalanceSnapshot(snapshot!, balance(50));
|
||||
|
||||
const oldRefresh = store.getState().onWalletBalanceMayHaveChanged();
|
||||
store.getState().setWalletOwner('user-b');
|
||||
@@ -143,7 +148,8 @@ describe('createProfileWalletStore', () => {
|
||||
getRechargeCenter: vi.fn().mockRejectedValue(new Error('network down')),
|
||||
});
|
||||
store.getState().setWalletOwner('user-a');
|
||||
store.getState().applyWalletBalanceSnapshot('user-a', balance(55));
|
||||
const snapshot = store.getState().captureWalletBalanceSnapshot('user-a');
|
||||
store.getState().applyWalletBalanceSnapshot(snapshot!, balance(55));
|
||||
|
||||
await store.getState().onWalletBalanceMayHaveChanged();
|
||||
|
||||
@@ -171,14 +177,15 @@ describe('createProfileWalletStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('ignores a snapshot captured for a different owner', () => {
|
||||
test('does not capture a snapshot for a different owner', () => {
|
||||
const store = createProfileWalletStore({
|
||||
getRechargeCenter: vi.fn(),
|
||||
});
|
||||
store.getState().setWalletOwner('user-b');
|
||||
|
||||
store.getState().applyWalletBalanceSnapshot('user-a', balance(100));
|
||||
const snapshot = store.getState().captureWalletBalanceSnapshot('user-a');
|
||||
|
||||
expect(snapshot).toBeNull();
|
||||
expect(store.getState().mudPointBalance).toBeNull();
|
||||
expect(store.getState().mudPointBalanceStatus).toBe('idle');
|
||||
});
|
||||
@@ -189,13 +196,60 @@ describe('createProfileWalletStore', () => {
|
||||
});
|
||||
store.getState().setWalletOwner('user-a');
|
||||
|
||||
store
|
||||
const snapshot = store
|
||||
.getState()
|
||||
.applyWalletBalanceSnapshot(' user-a ', balance(100));
|
||||
.captureWalletBalanceSnapshot(' user-a ');
|
||||
store.getState().applyWalletBalanceSnapshot(snapshot!, balance(100));
|
||||
|
||||
expect(store.getState()).toMatchObject({
|
||||
mudPointBalance: balance(100),
|
||||
mudPointBalanceStatus: 'ready',
|
||||
});
|
||||
});
|
||||
|
||||
test('does not let an older ordinary snapshot swallow a terminal refresh', async () => {
|
||||
const terminalRefresh = deferred<ProfileRechargeCenterResponse>();
|
||||
const requestSignals: Array<AbortSignal | undefined> = [];
|
||||
const store = createProfileWalletStore({
|
||||
getRechargeCenter: vi.fn((signal?: AbortSignal) => {
|
||||
requestSignals.push(signal);
|
||||
return terminalRefresh.promise;
|
||||
}),
|
||||
});
|
||||
store.getState().setWalletOwner('user-a');
|
||||
const ordinarySnapshot = store
|
||||
.getState()
|
||||
.captureWalletBalanceSnapshot('user-a');
|
||||
|
||||
const refresh = store.getState().onWalletBalanceMayHaveChanged();
|
||||
expect(
|
||||
store
|
||||
.getState()
|
||||
.applyWalletBalanceSnapshot(ordinarySnapshot!, balance(80)),
|
||||
).toBe(false);
|
||||
expect(requestSignals[0]?.aborted).toBe(false);
|
||||
|
||||
terminalRefresh.resolve(center(100));
|
||||
await refresh;
|
||||
|
||||
expect(store.getState().mudPointBalance?.totalPoints).toBe(100);
|
||||
});
|
||||
|
||||
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');
|
||||
const oldLifecycleSnapshot = store
|
||||
.getState()
|
||||
.captureWalletBalanceSnapshot('user-a');
|
||||
|
||||
store.getState().setWalletOwner('user-b');
|
||||
store.getState().setWalletOwner('user-a');
|
||||
|
||||
expect(
|
||||
store
|
||||
.getState()
|
||||
.applyWalletBalanceSnapshot(oldLifecycleSnapshot!, balance(100)),
|
||||
).toBe(false);
|
||||
expect(store.getState().mudPointBalance).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,19 +8,30 @@ import type {
|
||||
export type MudPointBalanceStatus = 'idle' | 'loading' | 'ready' | 'error';
|
||||
|
||||
export type ProfileWalletApi = {
|
||||
getRechargeCenter(signal?: AbortSignal): Promise<ProfileRechargeCenterResponse>;
|
||||
getRechargeCenter(
|
||||
signal?: AbortSignal,
|
||||
): Promise<ProfileRechargeCenterResponse>;
|
||||
};
|
||||
|
||||
export type ProfileWalletBalanceSnapshot = Readonly<{
|
||||
ownerUserId: string;
|
||||
ownerVersion: number;
|
||||
invalidationVersion: number;
|
||||
}>;
|
||||
|
||||
export type ProfileWalletStore = {
|
||||
ownerUserId: string | null;
|
||||
mudPointBalance: ProfileMudPointBalance | null;
|
||||
mudPointBalanceStatus: MudPointBalanceStatus;
|
||||
mudPointBalanceError: string;
|
||||
setWalletOwner: (userId: string | null) => void;
|
||||
applyWalletBalanceSnapshot: (
|
||||
captureWalletBalanceSnapshot: (
|
||||
ownerUserId: string,
|
||||
) => ProfileWalletBalanceSnapshot | null;
|
||||
applyWalletBalanceSnapshot: (
|
||||
snapshot: ProfileWalletBalanceSnapshot,
|
||||
balance: ProfileMudPointBalance,
|
||||
) => void;
|
||||
) => boolean;
|
||||
onWalletBalanceMayHaveChanged: () => Promise<void>;
|
||||
resetWalletBalance: () => void;
|
||||
};
|
||||
@@ -36,6 +47,7 @@ export function createProfileWalletStore(
|
||||
): UseBoundStore<StoreApi<ProfileWalletStore>> {
|
||||
let refreshVersion = 0;
|
||||
let settledRefreshVersion = 0;
|
||||
let ownerVersion = 0;
|
||||
let requestGeneration = 0;
|
||||
let activeRefresh: Promise<void> | null = null;
|
||||
let activeAbortController: AbortController | null = null;
|
||||
@@ -56,6 +68,7 @@ export function createProfileWalletStore(
|
||||
return;
|
||||
}
|
||||
|
||||
ownerVersion += 1;
|
||||
invalidateActiveRefresh();
|
||||
settledRefreshVersion = refreshVersion;
|
||||
set({
|
||||
@@ -63,22 +76,39 @@ export function createProfileWalletStore(
|
||||
...EMPTY_WALLET_STATE,
|
||||
});
|
||||
},
|
||||
applyWalletBalanceSnapshot: (ownerUserId, balance) => {
|
||||
captureWalletBalanceSnapshot: (ownerUserId) => {
|
||||
const normalizedOwnerUserId = ownerUserId.trim();
|
||||
if (
|
||||
!normalizedOwnerUserId ||
|
||||
get().ownerUserId !== normalizedOwnerUserId
|
||||
) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
ownerUserId: normalizedOwnerUserId,
|
||||
ownerVersion,
|
||||
invalidationVersion: refreshVersion,
|
||||
};
|
||||
},
|
||||
applyWalletBalanceSnapshot: (snapshot, balance) => {
|
||||
if (
|
||||
get().ownerUserId !== snapshot.ownerUserId ||
|
||||
ownerVersion !== snapshot.ownerVersion ||
|
||||
snapshot.invalidationVersion < refreshVersion ||
|
||||
snapshot.invalidationVersion < settledRefreshVersion
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
invalidateActiveRefresh();
|
||||
settledRefreshVersion = refreshVersion;
|
||||
settledRefreshVersion = snapshot.invalidationVersion;
|
||||
set({
|
||||
mudPointBalance: balance,
|
||||
mudPointBalanceStatus: 'ready',
|
||||
mudPointBalanceError: '',
|
||||
});
|
||||
return true;
|
||||
},
|
||||
onWalletBalanceMayHaveChanged: () => {
|
||||
refreshVersion += 1;
|
||||
@@ -149,6 +179,7 @@ export function createProfileWalletStore(
|
||||
return refresh;
|
||||
},
|
||||
resetWalletBalance: () => {
|
||||
ownerVersion += 1;
|
||||
invalidateActiveRefresh();
|
||||
settledRefreshVersion = refreshVersion;
|
||||
set({ ownerUserId: null, ...EMPTY_WALLET_STATE });
|
||||
|
||||
Reference in New Issue
Block a user