修复任务列表重试请求泄漏
在任一 bootstrap 分支失败时中止当前轮全部请求。 补充失败分支悬挂、重试后卸载的 AbortSignal 回归测试。
This commit is contained in:
@@ -625,7 +625,9 @@ describe('ImageCanvasTaskSidebarView', () => {
|
||||
});
|
||||
let activeRequestCount = 0;
|
||||
listExternalGenerationTasksMock.mockImplementation(
|
||||
(options: Parameters<typeof listExternalGenerationTasks>[0] = {}) => {
|
||||
(
|
||||
options: Parameters<typeof listExternalGenerationTasks>[0] = {},
|
||||
): ReturnType<typeof listExternalGenerationTasks> => {
|
||||
if (options.statuses?.includes('running')) {
|
||||
activeRequestCount += 1;
|
||||
if (activeRequestCount === 1) {
|
||||
@@ -713,6 +715,67 @@ describe('ImageCanvasTaskSidebarView', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('aborts every bootstrap attempt when one branch fails and after retry unmounts', async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const requestSignals: AbortSignal[] = [];
|
||||
let activeRequestCount = 0;
|
||||
listExternalGenerationTasksMock.mockImplementation(
|
||||
(options: Parameters<typeof listExternalGenerationTasks>[0] = {}) => {
|
||||
const signal = options.signal;
|
||||
if (!signal) {
|
||||
return Promise.reject(new Error('missing abort signal'));
|
||||
}
|
||||
requestSignals.push(signal);
|
||||
if (options.statuses?.includes('running')) {
|
||||
activeRequestCount += 1;
|
||||
if (activeRequestCount === 1) {
|
||||
return Promise.reject(new Error('active bootstrap failed'));
|
||||
}
|
||||
}
|
||||
return new Promise<
|
||||
Awaited<ReturnType<typeof listExternalGenerationTasks>>
|
||||
>((_, reject) => {
|
||||
signal.addEventListener(
|
||||
'abort',
|
||||
() => reject(new DOMException('Aborted', 'AbortError')),
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const { unmount } = render(
|
||||
<ImageCanvasTaskSidebarView
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
onFocusExternalTask={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(requestSignals).toHaveLength(2);
|
||||
expect(requestSignals[0]).toBe(requestSignals[1]);
|
||||
expect(requestSignals[0]!.aborted).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(requestSignals).toHaveLength(4);
|
||||
expect(requestSignals[2]).toBe(requestSignals[3]);
|
||||
expect(requestSignals[2]!.aborted).toBe(false);
|
||||
|
||||
unmount();
|
||||
expect(requestSignals[2]!.aborted).toBe(true);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it('loads more completed tasks while scrolling and keeps the request capped', async () => {
|
||||
const completedTasks = Array.from({ length: 120 }, (_, index) => {
|
||||
const completedAt = new Date(Date.now() - index * 1000).toISOString();
|
||||
|
||||
@@ -362,23 +362,24 @@ export function ImageCanvasTaskSidebarView({
|
||||
let retryTimerId: number | null = null;
|
||||
let retryIndex = 0;
|
||||
const loadTaskLists = () => {
|
||||
controller = new AbortController();
|
||||
const attemptController = new AbortController();
|
||||
controller = attemptController;
|
||||
Promise.all([
|
||||
listExternalGenerationTasks({
|
||||
limit: ACTIVE_TASK_LIST_LIMIT,
|
||||
includeAcknowledgedTerminal: false,
|
||||
statuses: ['running', 'queued'],
|
||||
signal: controller.signal,
|
||||
signal: attemptController.signal,
|
||||
}),
|
||||
listExternalGenerationTasks({
|
||||
limit: completedListLimit,
|
||||
includeAcknowledgedTerminal: true,
|
||||
statuses: ['completed', 'failed'],
|
||||
signal: controller.signal,
|
||||
signal: attemptController.signal,
|
||||
}),
|
||||
])
|
||||
.then(([activeResponse, completedResponse]) => {
|
||||
if (disposed) {
|
||||
if (disposed || attemptController.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
const activeTasks = activeResponse.tasks.filter(isActiveExternalTask);
|
||||
@@ -408,6 +409,10 @@ export function ImageCanvasTaskSidebarView({
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
attemptController.abort();
|
||||
if (controller === attemptController) {
|
||||
controller = null;
|
||||
}
|
||||
if (
|
||||
disposed ||
|
||||
retryIndex >= TASK_LIST_BOOTSTRAP_RETRY_DELAYS_MS.length
|
||||
@@ -417,6 +422,11 @@ export function ImageCanvasTaskSidebarView({
|
||||
const retryDelayMs = TASK_LIST_BOOTSTRAP_RETRY_DELAYS_MS[retryIndex];
|
||||
retryIndex += 1;
|
||||
retryTimerId = window.setTimeout(loadTaskLists, retryDelayMs);
|
||||
})
|
||||
.finally(() => {
|
||||
if (controller === attemptController) {
|
||||
controller = null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user