Ver Fonte

fix(订单实时): 合并窗口与 busy 的竞态 —— 窗口到期前进入操作必须撤掉已起的计时

复核 Important #1:schedule() 在 busy=false 时起了计时器,用户在 500ms 窗口内开始操作
(busy 翻 true),而 busy 的 watch 只管释放沿 ⇒ 窗口一到 run() 照刷,列表在用户脚下跳
(违反裁定 ④「操作中挂起」)。修法两层:watch 在 busy 翻 true 时 clearTimeout 并记
pending;run() 入口再挡一次 busy(双保险,补刷交给回安全态那条路)。

验证:verify-live-refetch.mjs 新增用例先红后绿(t14-fix1-red.txt:该条红、其余 9 条绿;
现 10/10 全通过),verify-order-stream.mjs 11/11,ts:check 与修复前逐字节相同
(基线 exit 2 不动,diff 无差异)。
alphaH há 1 semana atrás
pai
commit
e9349d0b33

+ 21 - 0
frontend/scripts/verify-live-refetch.mjs

@@ -204,6 +204,27 @@ await check("busy 期间挂起,回安全态立刻补刷一次", async () => {
   dispose();
 });
 
+await check("合并窗口内进入 busy:窗口到期不许刷,回安全态补刷一次", async () => {
+  const stream = fakeStream();
+  const busy = ref(false);
+  let calls = 0;
+  const dispose = useLiveRefetch(["lsy_order"], () => { calls += 1; }, { ...OPTS, stream, busy });
+
+  stream.emit("lsy_order"); // 信号先到:窗口开始计时(此刻 busy = false)
+  busy.value = true; // 用户在窗口还没走完时开始操作
+  await nextTick();
+  await sleep(20); // 窗口到期
+  assert.equal(calls, 0, "已经起计时的窗口必须随 busy 一起撤掉:列表不许在用户操作期间跳");
+
+  busy.value = false;
+  await nextTick();
+  assert.equal(calls, 1, "那一帧没刷成,回安全态要补上(不能丢)");
+
+  await sleep(20);
+  assert.equal(calls, 1, "补刷只做一次");
+  dispose();
+});
+
 await check("busy 翻转但期间没有信号 → 一次都不刷", async () => {
   const stream = fakeStream();
   const busy = ref(false);

+ 17 - 1
frontend/src/composables/useLiveRefetch.ts

@@ -37,6 +37,12 @@ export function useLiveRefetch(
   const run = (): void => {
     if (disposed) return;
     timer = null;
+    // 双保险:正常路径下 watch 已在 busy 翻转时撤掉计时;万一有路径漏进来,
+    // 这里再挡一次 —— 在用户操作期间刷列表正是 busy 要防的那件事,补刷交给回安全态那条路
+    if (busy?.value) {
+      pending = true;
+      return;
+    }
     try {
       const result = refetch();
       if (result && typeof (result as Promise<unknown>).then === "function") {
@@ -65,7 +71,17 @@ export function useLiveRefetch(
   let stopWatch = (): void => {};
   if (busy) {
     stopWatch = watch(busy, isBusy => {
-      if (!isBusy && pending) {
+      if (isBusy) {
+        // 信号先到、用户在合并窗口还没走完时才开始操作:撤掉已起的计时并记 pending。
+        // 不撤的话窗口一到就刷 —— 正是 busy 要防的那件事(在用户脚下跳列表)
+        if (timer !== null) {
+          clearTimeout(timer);
+          timer = null;
+          pending = true;
+        }
+        return;
+      }
+      if (pending) {
         pending = false;
         if (timer !== null) {
           clearTimeout(timer);