fix(agent-office): useActivityFeed stale 응답 무시 (필터 변경 중 in-flight 요청)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,28 +12,35 @@ export function useActivityFeed(filters, refreshTrigger = 0) {
|
|||||||
|
|
||||||
const offsetRef = useRef(0);
|
const offsetRef = useRef(0);
|
||||||
const loadingRef = useRef(false);
|
const loadingRef = useRef(false);
|
||||||
|
const requestIdRef = useRef(0);
|
||||||
const filtersRef = useRef(filters);
|
const filtersRef = useRef(filters);
|
||||||
filtersRef.current = filters;
|
filtersRef.current = filters;
|
||||||
|
|
||||||
const filterKey = JSON.stringify(filters);
|
const filterKey = JSON.stringify(filters);
|
||||||
|
|
||||||
const fetchPage = useCallback(async (offset, replace) => {
|
const fetchPage = useCallback(async (offset, replace) => {
|
||||||
if (loadingRef.current) return;
|
// append(loadMore)만 중복 방지. replace(필터/refresh 재조회)는 항상 우선 진행.
|
||||||
|
if (!replace && loadingRef.current) return;
|
||||||
|
const reqId = ++requestIdRef.current;
|
||||||
loadingRef.current = true;
|
loadingRef.current = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const data = await agentActivity({ ...filtersRef.current, limit: PAGE_SIZE, offset });
|
const data = await agentActivity({ ...filtersRef.current, limit: PAGE_SIZE, offset });
|
||||||
|
if (reqId !== requestIdRef.current) return; // 더 새로운 요청이 시작됨 → stale 응답 무시
|
||||||
const newItems = Array.isArray(data?.items) ? data.items : [];
|
const newItems = Array.isArray(data?.items) ? data.items : [];
|
||||||
setTotal(data?.total || 0);
|
setTotal(data?.total || 0);
|
||||||
setItems(prev => (replace ? newItems : [...prev, ...newItems]));
|
setItems(prev => (replace ? newItems : [...prev, ...newItems]));
|
||||||
offsetRef.current = offset + newItems.length;
|
offsetRef.current = offset + newItems.length;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (reqId !== requestIdRef.current) return;
|
||||||
setError(e.message || '불러오기 실패');
|
setError(e.message || '불러오기 실패');
|
||||||
} finally {
|
} finally {
|
||||||
|
if (reqId === requestIdRef.current) {
|
||||||
loadingRef.current = false;
|
loadingRef.current = false;
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -56,4 +56,18 @@ describe('useActivityFeed', () => {
|
|||||||
await waitFor(() => expect(result.current.items).toHaveLength(1));
|
await waitFor(() => expect(result.current.items).toHaveLength(1));
|
||||||
expect(result.current.hasMore).toBe(true);
|
expect(result.current.hasMore).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('필터 변경 중이던 이전(stale) 요청 응답은 무시된다', async () => {
|
||||||
|
let resolveFirst;
|
||||||
|
const firstPromise = new Promise(r => { resolveFirst = r; });
|
||||||
|
mockAgentActivity
|
||||||
|
.mockReturnValueOnce(firstPromise) // 초기 요청 — 느리게 resolve
|
||||||
|
.mockResolvedValueOnce({ items: [item({ task_id: 'fresh', agent_id: 'insta' })], total: 1 });
|
||||||
|
const { result, rerender } = renderHook(({ f }) => useActivityFeed(f, 0), { initialProps: { f: { days: 7 } } });
|
||||||
|
rerender({ f: { days: 7, agent_id: 'insta' } }); // 첫 요청 resolve 전에 필터 변경
|
||||||
|
await waitFor(() => expect(result.current.items[0]?.task_id).toBe('fresh'));
|
||||||
|
await act(async () => { resolveFirst({ items: [item({ task_id: 'stale' })], total: 99 }); });
|
||||||
|
expect(result.current.items[0].task_id).toBe('fresh'); // stale이 덮어쓰지 않음
|
||||||
|
expect(result.current.total).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user