From 9e119e2a2f550c719e18c46a7ad99c595f195587 Mon Sep 17 00:00:00 2001 From: baiqing Date: Fri, 15 May 2026 14:52:22 +0800 Subject: [PATCH] fix(marketplace): stale detail response guard (pr_agent #444 round 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 跟 round 2 同类 race condition:openDetail 用户快速点两张卡片时,先点 A 的 detail response 可能晚于 B 到达 → 覆盖 B 的 detail / Install / Like 作用错对象。 修:复用 useRef seq token 模式,detailSeqRef 单调递增,response 比较 seq 丢 弃 stale。逻辑跟 refresh() 完全对称。 --- openless-all/app/src/pages/Marketplace.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openless-all/app/src/pages/Marketplace.tsx b/openless-all/app/src/pages/Marketplace.tsx index f3704536..04f15cca 100644 --- a/openless-all/app/src/pages/Marketplace.tsx +++ b/openless-all/app/src/pages/Marketplace.tsx @@ -53,9 +53,10 @@ export function Marketplace() { return () => window.clearTimeout(id); }, [query]); - // 单调递增 seq 防 stale 响应覆盖:用户快速改 query 时旧请求的 response + // 单调递增 seq 防 stale 响应覆盖:用户快速改 query / 切换 pack 时旧请求 response // 可能晚于新请求到达,比较 seq 丢弃过期结果。 const reqSeqRef = useRef(0); + const detailSeqRef = useRef(0); const refresh = useCallback(async () => { const seq = ++reqSeqRef.current; setLoading(true); @@ -78,18 +79,21 @@ export function Marketplace() { }, [refresh]); const openDetail = async (id: string) => { + const seq = ++detailSeqRef.current; setSelectedId(id); setDetail(null); setDetailLoading(true); try { const d = await fetchMarketplaceDetail(id); + if (seq !== detailSeqRef.current) return; // stale: 用户已切到另一个 pack setDetail(d); } catch (error) { + if (seq !== detailSeqRef.current) return; console.error('[marketplace] detail failed', error); setActionMsg({ kind: 'err', text: t('marketplace.errors.detail', { err: errorMessage(error) }) }); setSelectedId(null); } finally { - setDetailLoading(false); + if (seq === detailSeqRef.current) setDetailLoading(false); } };