Handle asynchronous tasks
This commit is contained in:
parent
7a2a18a1d4
commit
bdf90039b5
4 changed files with 110 additions and 48 deletions
|
@ -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();
|
||||||
|
|
||||||
expect(onCompleted).toHaveBeenCalled();
|
setImmediate(() => {
|
||||||
expect(onError).not.toHaveBeenCalled();
|
expect(handle).toHaveBeenCalled();
|
||||||
|
expect(onCompleted).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();
|
||||||
|
|
||||||
expect(onCompleted).not.toHaveBeenCalled();
|
setImmediate(() => {
|
||||||
expect(onError).toHaveBeenCalled();
|
expect(handle).toHaveBeenCalled();
|
||||||
|
expect(onCompleted).not.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();
|
||||||
|
|
||||||
expect(onCompleted).not.toHaveBeenCalled();
|
setImmediate(() => {
|
||||||
|
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);
|
||||||
|
|
||||||
expect(tasks[0].onCompleted).not.toHaveBeenCalled();
|
setImmediate(() => {
|
||||||
expect(tasks[1].onCompleted).not.toHaveBeenCalled();
|
expect(tasks[0].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[1].onCompleted).not.toHaveBeenCalled();
|
||||||
|
|
||||||
expect(tasks[0].onCompleted).toHaveBeenCalled();
|
// Set time to backgroundTasksRefreshTime * 3
|
||||||
expect(tasks[1].onCompleted).not.toHaveBeenCalled();
|
jest.advanceTimersByTime(backgroundTasksRefreshTime);
|
||||||
|
|
||||||
responses[1].finished = true;
|
setImmediate(() => {
|
||||||
|
expect(tasks[1].onCompleted).toHaveBeenCalled();
|
||||||
|
expect(tasks[1].onCompleted).toHaveBeenCalled();
|
||||||
|
|
||||||
jest.runOnlyPendingTimers();
|
done();
|
||||||
|
});
|
||||||
expect(tasks[1].onCompleted).toHaveBeenCalled();
|
});
|
||||||
expect(tasks[1].onCompleted).toHaveBeenCalled();
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
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();
|
||||||
|
if (ok) {
|
||||||
|
task.onCompleted();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
task.onError(error);
|
task.onError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ok) {
|
|
||||||
task.onCompleted();
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue