fix(frontend): RefineView 取消在途 refine——AbortController 防段切换竞态(CR-H12)

This commit is contained in:
Yaojia Wang
2026-07-08 11:08:21 +02:00
parent 5665cc9a16
commit 5c02792e5f
3 changed files with 65 additions and 7 deletions

View File

@@ -122,4 +122,40 @@ describe("useRefine", () => {
expect(result.current.status).toBe("idle");
expect(result.current.result).toBeNull();
});
it("中止后迟到的响应不覆盖状态(段切换竞态)", async () => {
// Arrangepost 挂起,手动兑现以模拟在途请求。
let resolvePost!: (v: unknown) => void;
const pending = new Promise((r) => {
resolvePost = r;
});
post.mockReturnValue(pending);
const { result } = renderHook(() => useRefine());
// Act发起回炉进入 refining随后 abort模拟切段取消在途
let refinePromise!: Promise<unknown>;
act(() => {
refinePromise = result.current.refine("p1", 1, "新段");
});
expect(result.current.status).toBe("refining");
act(() => {
result.current.abort();
});
// 迟到的旧响应兑现——应被丢弃:不写状态、不弹 toast。
await act(async () => {
resolvePost({
data: { original: "旧段", refined: "旧改写" },
error: null,
});
await refinePromise;
});
// Assert结果仍为空、未落到 done、未误报 toast。
expect(result.current.result).toBeNull();
expect(result.current.status).not.toBe("done");
expect(toast).not.toHaveBeenCalled();
});
});

View File

@@ -1,6 +1,6 @@
"use client";
import { useCallback, useState } from "react";
import { useCallback, useRef, useState } from "react";
import { api } from "@/lib/api/client";
import { useToast } from "@/components/Toast";
@@ -24,6 +24,8 @@ export interface UseRefine {
segment: string,
instruction?: string,
) => Promise<RefineResult | null>;
// 取消在途回炉(段切换/卸载时调用)——不触碰 state安全用于 effect 清理。
abort: () => void;
reset: () => void;
}
@@ -32,10 +34,15 @@ export interface UseRefine {
export function useRefine(): UseRefine {
const [status, setStatus] = useState<RefineStatus>("idle");
const [result, setResult] = useState<RefineResult | null>(null);
const controllerRef = useRef<AbortController | null>(null);
const toast = useToast();
const refine = useCallback<UseRefine["refine"]>(
async (projectId, chapterNo, segment, instruction) => {
// 取消上一次在途请求,换新 controller段切换竞态防护CR-H12
controllerRef.current?.abort();
const controller = new AbortController();
controllerRef.current = controller;
setStatus("refining");
setResult(null);
try {
@@ -46,8 +53,11 @@ export function useRefine(): UseRefine {
path: { project_id: projectId, chapter_no: chapterNo },
},
body: buildRefineRequest(segment, instruction),
signal: controller.signal,
},
);
// 已被后续请求/卸载中止:丢弃迟到响应,不写状态。
if (controller.signal.aborted) return null;
if (error || !data) {
setStatus("error");
const code = errorCode(error);
@@ -67,6 +77,8 @@ export function useRefine(): UseRefine {
setStatus("done");
return outcome;
} catch {
// 主动中止abort触发的异常不该报错 toast。
if (controller.signal.aborted) return null;
setStatus("error");
toast("回炉请求异常,请检查网络。", "error");
return null;
@@ -75,10 +87,18 @@ export function useRefine(): UseRefine {
[toast],
);
// 仅取消在途请求,不触碰 state——可安全用于 effect 清理 / 卸载。
const abort = useCallback((): void => {
controllerRef.current?.abort();
controllerRef.current = null;
}, []);
const reset = useCallback((): void => {
controllerRef.current?.abort();
controllerRef.current = null;
setStatus("idle");
setResult(null);
}, []);
return { status, result, refine, reset };
return { status, result, refine, abort, reset };
}