Handle asynchronous tasks

This commit is contained in:
dominik-zeglen 2020-06-21 17:31:33 +02:00
parent 7a2a18a1d4
commit bdf90039b5
4 changed files with 110 additions and 48 deletions

View file

@ -9,77 +9,100 @@ import { Task, TaskData } from "./types";
jest.useFakeTimers(); jest.useFakeTimers();
describe("Background task provider", () => { describe("Background task provider", () => {
it("can queue a task", () => { it("can queue a task", done => {
const handle = jest.fn<Promise<boolean>, []>(
() => new Promise(resolve => resolve(true))
);
const onCompleted = jest.fn(); const onCompleted = jest.fn();
const onError = jest.fn(); const onError = jest.fn();
const { result } = renderHook(useBackgroundTasks); const { result } = renderHook(useBackgroundTasks);
const taskId = result.current.queue(Task.CUSTOM, { const taskId = result.current.queue(Task.CUSTOM, {
handle: () => true, handle,
onCompleted, onCompleted,
onError onError
}); });
expect(taskId).toBe(1); expect(taskId).toBe(1);
expect(handle).not.toHaveBeenCalled();
expect(onCompleted).not.toHaveBeenCalled(); expect(onCompleted).not.toHaveBeenCalled();
expect(onError).not.toHaveBeenCalled(); expect(onError).not.toHaveBeenCalled();
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
setImmediate(() => {
expect(handle).toHaveBeenCalled();
expect(onCompleted).toHaveBeenCalled(); expect(onCompleted).toHaveBeenCalled();
expect(onError).not.toHaveBeenCalled(); expect(onError).not.toHaveBeenCalled();
done();
});
}); });
it("can handle task error", () => { it("can handle task error", done => {
const handle = jest.fn<Promise<boolean>, []>(
() =>
new Promise(() => {
throw new Error("dummy error");
})
);
const onCompleted = jest.fn(); const onCompleted = jest.fn();
const onError = jest.fn(); const onError = jest.fn();
const { result } = renderHook(useBackgroundTasks); const { result } = renderHook(useBackgroundTasks);
result.current.queue(Task.CUSTOM, { result.current.queue(Task.CUSTOM, {
handle: () => { handle,
throw new Error("dummy error");
},
onCompleted, onCompleted,
onError onError
}); });
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
setImmediate(() => {
expect(handle).toHaveBeenCalled();
expect(onCompleted).not.toHaveBeenCalled(); expect(onCompleted).not.toHaveBeenCalled();
expect(onError).toHaveBeenCalled(); expect(onError).toHaveBeenCalled();
done();
});
}); });
it("can cancel task", () => { it("can cancel task", done => {
const onCompleted = jest.fn(); const onCompleted = jest.fn();
const { result } = renderHook(useBackgroundTasks); const { result } = renderHook(useBackgroundTasks);
const taskId = result.current.queue(Task.CUSTOM, { const taskId = result.current.queue(Task.CUSTOM, {
handle: () => true, handle: () => new Promise(resolve => resolve(true)),
onCompleted onCompleted
}); });
jest.advanceTimersByTime(backgroundTasksRefreshTime - 1000); // Cancel task before executing it
jest.advanceTimersByTime(backgroundTasksRefreshTime * 0.9);
result.current.cancel(taskId); result.current.cancel(taskId);
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
setImmediate(() => {
expect(onCompleted).not.toHaveBeenCalled(); expect(onCompleted).not.toHaveBeenCalled();
done();
});
}); });
it("can queue multiple tasks", () => { it("can queue multiple tasks", done => {
const responses = [ const responses: Array<Promise<boolean>> = [
{ new Promise(resolve =>
finished: false setTimeout(() => resolve(true), backgroundTasksRefreshTime * 1.4)
}, ),
{ new Promise(resolve =>
finished: false setTimeout(() => resolve(true), backgroundTasksRefreshTime * 2.1)
} )
]; ];
const tasks: TaskData[] = responses.map(response => ({ const tasks: TaskData[] = responses.map(response => ({
handle: () => response.finished, handle: () => response,
onCompleted: jest.fn() onCompleted: jest.fn()
})); }));
@ -87,23 +110,30 @@ describe("Background task provider", () => {
tasks.forEach(task => result.current.queue(Task.CUSTOM, task)); tasks.forEach(task => result.current.queue(Task.CUSTOM, task));
jest.runOnlyPendingTimers(); // Set time to backgroundTasksRefreshTime
jest.advanceTimersByTime(backgroundTasksRefreshTime + 100);
setImmediate(() => {
expect(tasks[0].onCompleted).not.toHaveBeenCalled(); expect(tasks[0].onCompleted).not.toHaveBeenCalled();
expect(tasks[1].onCompleted).not.toHaveBeenCalled(); expect(tasks[1].onCompleted).not.toHaveBeenCalled();
responses[0].finished = true; // Set time to backgroundTasksRefreshTime * 2
jest.advanceTimersByTime(backgroundTasksRefreshTime);
jest.runOnlyPendingTimers();
setImmediate(() => {
expect(tasks[0].onCompleted).toHaveBeenCalled(); expect(tasks[0].onCompleted).toHaveBeenCalled();
expect(tasks[1].onCompleted).not.toHaveBeenCalled(); expect(tasks[1].onCompleted).not.toHaveBeenCalled();
responses[1].finished = true; // Set time to backgroundTasksRefreshTime * 3
jest.advanceTimersByTime(backgroundTasksRefreshTime);
jest.runOnlyPendingTimers();
setImmediate(() => {
expect(tasks[1].onCompleted).toHaveBeenCalled(); expect(tasks[1].onCompleted).toHaveBeenCalled();
expect(tasks[1].onCompleted).toHaveBeenCalled(); expect(tasks[1].onCompleted).toHaveBeenCalled();
done();
});
});
});
}); });
}); });

View file

@ -2,7 +2,7 @@ import React from "react";
import BackgroundTasksContext from "./context"; import BackgroundTasksContext from "./context";
import { handleTask, queueCustom } from "./tasks"; import { handleTask, queueCustom } from "./tasks";
import { QueuedTask, Task, TaskData } from "./types"; import { QueuedTask, Task, TaskData, TaskStatus } from "./types";
export const backgroundTasksRefreshTime = 15 * 1000; export const backgroundTasksRefreshTime = 15 * 1000;
@ -12,7 +12,34 @@ export function useBackgroundTasks() {
React.useEffect(() => { React.useEffect(() => {
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
tasks.current = tasks.current.filter(task => !handleTask(task)); const queue = async () => {
tasks.current = tasks.current.filter(
task => task.status !== TaskStatus.ENDED
);
try {
await Promise.all(
tasks.current.map(async task => {
let hasFinished: boolean;
try {
hasFinished = await handleTask(task);
} catch (error) {
throw error;
}
if (hasFinished) {
const taskIndex = tasks.current.findIndex(
t => t.id === task.id
);
tasks.current[taskIndex].status = TaskStatus.ENDED;
}
})
);
} catch (error) {
throw error;
}
};
queue();
}, backgroundTasksRefreshTime); }, backgroundTasksRefreshTime);
return () => clearInterval(intervalId); return () => clearInterval(intervalId);

View file

@ -1,16 +1,15 @@
import { QueuedTask, TaskData } from "./types"; import { QueuedTask, TaskData, TaskStatus } from "./types";
export function handleTask(task: QueuedTask) { export async function handleTask(task: QueuedTask): Promise<boolean> {
let ok: boolean; let ok = false;
try { try {
ok = task.handle(); ok = await task.handle();
} catch (error) {
task.onError(error);
}
if (ok) { if (ok) {
task.onCompleted(); task.onCompleted();
} }
} catch (error) {
task.onError(error);
}
return ok; return ok;
} }
@ -36,7 +35,8 @@ export function queueCustom(
handle: data.handle, handle: data.handle,
id, id,
onCompleted: data.onCompleted, onCompleted: data.onCompleted,
onError: data.onError || handleError onError: data.onError || handleError,
status: TaskStatus.PENDING
} }
]; ];
} }

View file

@ -1,16 +1,21 @@
export enum Task { export enum Task {
CUSTOM CUSTOM
} }
export enum TaskStatus {
PENDING,
ENDED
}
export interface QueuedTask { export interface QueuedTask {
id: number; id: number;
handle: () => boolean; handle: () => Promise<boolean>;
status: TaskStatus;
onCompleted: () => void; onCompleted: () => void;
onError: (error: Error) => void; onError: (error: Error) => void;
} }
export interface TaskData { export interface TaskData {
handle?: () => boolean; handle?: () => Promise<boolean>;
onCompleted?: () => void; onCompleted?: () => void;
onError?: () => void; onError?: () => void;
} }