Add background task manager
This commit is contained in:
parent
4468857856
commit
7a2a18a1d4
6 changed files with 239 additions and 0 deletions
109
src/containers/BackgroundTasks/BackgroundTasksProvider.test.tsx
Normal file
109
src/containers/BackgroundTasks/BackgroundTasksProvider.test.tsx
Normal file
|
@ -0,0 +1,109 @@
|
|||
import { renderHook } from "@testing-library/react-hooks";
|
||||
|
||||
import {
|
||||
backgroundTasksRefreshTime,
|
||||
useBackgroundTasks
|
||||
} from "./BackgroundTasksProvider";
|
||||
import { Task, TaskData } from "./types";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("Background task provider", () => {
|
||||
it("can queue a task", () => {
|
||||
const onCompleted = jest.fn();
|
||||
const onError = jest.fn();
|
||||
|
||||
const { result } = renderHook(useBackgroundTasks);
|
||||
|
||||
const taskId = result.current.queue(Task.CUSTOM, {
|
||||
handle: () => true,
|
||||
onCompleted,
|
||||
onError
|
||||
});
|
||||
expect(taskId).toBe(1);
|
||||
expect(onCompleted).not.toHaveBeenCalled();
|
||||
expect(onError).not.toHaveBeenCalled();
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(onCompleted).toHaveBeenCalled();
|
||||
expect(onError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("can handle task error", () => {
|
||||
const onCompleted = jest.fn();
|
||||
const onError = jest.fn();
|
||||
|
||||
const { result } = renderHook(useBackgroundTasks);
|
||||
|
||||
result.current.queue(Task.CUSTOM, {
|
||||
handle: () => {
|
||||
throw new Error("dummy error");
|
||||
},
|
||||
onCompleted,
|
||||
onError
|
||||
});
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(onCompleted).not.toHaveBeenCalled();
|
||||
expect(onError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("can cancel task", () => {
|
||||
const onCompleted = jest.fn();
|
||||
|
||||
const { result } = renderHook(useBackgroundTasks);
|
||||
|
||||
const taskId = result.current.queue(Task.CUSTOM, {
|
||||
handle: () => true,
|
||||
onCompleted
|
||||
});
|
||||
|
||||
jest.advanceTimersByTime(backgroundTasksRefreshTime - 1000);
|
||||
result.current.cancel(taskId);
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(onCompleted).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("can queue multiple tasks", () => {
|
||||
const responses = [
|
||||
{
|
||||
finished: false
|
||||
},
|
||||
{
|
||||
finished: false
|
||||
}
|
||||
];
|
||||
|
||||
const tasks: TaskData[] = responses.map(response => ({
|
||||
handle: () => response.finished,
|
||||
onCompleted: jest.fn()
|
||||
}));
|
||||
|
||||
const { result } = renderHook(useBackgroundTasks);
|
||||
|
||||
tasks.forEach(task => result.current.queue(Task.CUSTOM, task));
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(tasks[0].onCompleted).not.toHaveBeenCalled();
|
||||
expect(tasks[1].onCompleted).not.toHaveBeenCalled();
|
||||
|
||||
responses[0].finished = true;
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(tasks[0].onCompleted).toHaveBeenCalled();
|
||||
expect(tasks[1].onCompleted).not.toHaveBeenCalled();
|
||||
|
||||
responses[1].finished = true;
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
expect(tasks[1].onCompleted).toHaveBeenCalled();
|
||||
expect(tasks[1].onCompleted).toHaveBeenCalled();
|
||||
});
|
||||
});
|
58
src/containers/BackgroundTasks/BackgroundTasksProvider.tsx
Normal file
58
src/containers/BackgroundTasks/BackgroundTasksProvider.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import React from "react";
|
||||
|
||||
import BackgroundTasksContext from "./context";
|
||||
import { handleTask, queueCustom } from "./tasks";
|
||||
import { QueuedTask, Task, TaskData } from "./types";
|
||||
|
||||
export const backgroundTasksRefreshTime = 15 * 1000;
|
||||
|
||||
export function useBackgroundTasks() {
|
||||
const idCounter = React.useRef(0);
|
||||
const tasks = React.useRef<QueuedTask[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
tasks.current = tasks.current.filter(task => !handleTask(task));
|
||||
}, backgroundTasksRefreshTime);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
});
|
||||
|
||||
function cancel(id: number) {
|
||||
tasks.current = tasks.current.filter(task => task.id !== id);
|
||||
}
|
||||
|
||||
function queue(type: Task, data?: TaskData) {
|
||||
idCounter.current += 1;
|
||||
|
||||
switch (type) {
|
||||
case Task.CUSTOM:
|
||||
queueCustom(idCounter.current, tasks, data);
|
||||
}
|
||||
|
||||
return idCounter.current;
|
||||
}
|
||||
|
||||
return {
|
||||
cancel,
|
||||
queue
|
||||
};
|
||||
}
|
||||
|
||||
const BackgroundTasksProvider: React.FC = ({ children }) => {
|
||||
const { cancel, queue } = useBackgroundTasks();
|
||||
|
||||
return (
|
||||
<BackgroundTasksContext.Provider
|
||||
value={{
|
||||
cancel,
|
||||
queue
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</BackgroundTasksContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
BackgroundTasksProvider.displayName = "BackgroundTasksProvider";
|
||||
export default BackgroundTasksProvider;
|
7
src/containers/BackgroundTasks/context.ts
Normal file
7
src/containers/BackgroundTasks/context.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { createContext } from "React";
|
||||
|
||||
import { BackgroundTasksContextType } from "./types";
|
||||
|
||||
const BackgroundTasksContext = createContext<BackgroundTasksContextType>(null);
|
||||
|
||||
export default BackgroundTasksContext;
|
2
src/containers/BackgroundTasks/index.ts
Normal file
2
src/containers/BackgroundTasks/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./BackgroundTasksProvider";
|
||||
export { default } from "./BackgroundTasksProvider";
|
42
src/containers/BackgroundTasks/tasks.ts
Normal file
42
src/containers/BackgroundTasks/tasks.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { QueuedTask, TaskData } from "./types";
|
||||
|
||||
export function handleTask(task: QueuedTask) {
|
||||
let ok: boolean;
|
||||
try {
|
||||
ok = task.handle();
|
||||
} catch (error) {
|
||||
task.onError(error);
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
task.onCompleted();
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
export function handleError(error: Error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
export function queueCustom(
|
||||
id: number,
|
||||
tasks: React.MutableRefObject<QueuedTask[]>,
|
||||
data: TaskData
|
||||
) {
|
||||
(["handle", "onCompleted"] as Array<keyof TaskData>)
|
||||
.filter(field => !data[field])
|
||||
.forEach(field => {
|
||||
throw new Error(`${field} is required when creating custom task`);
|
||||
});
|
||||
|
||||
tasks.current = [
|
||||
...tasks.current,
|
||||
{
|
||||
handle: data.handle,
|
||||
id,
|
||||
onCompleted: data.onCompleted,
|
||||
onError: data.onError || handleError
|
||||
}
|
||||
];
|
||||
}
|
21
src/containers/BackgroundTasks/types.ts
Normal file
21
src/containers/BackgroundTasks/types.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
export enum Task {
|
||||
CUSTOM
|
||||
}
|
||||
|
||||
export interface QueuedTask {
|
||||
id: number;
|
||||
handle: () => boolean;
|
||||
onCompleted: () => void;
|
||||
onError: (error: Error) => void;
|
||||
}
|
||||
|
||||
export interface TaskData {
|
||||
handle?: () => boolean;
|
||||
onCompleted?: () => void;
|
||||
onError?: () => void;
|
||||
}
|
||||
|
||||
export interface BackgroundTasksContextType {
|
||||
cancel: (id: number) => void;
|
||||
queue: (type: Task, data?: TaskData) => void;
|
||||
}
|
Loading…
Reference in a new issue