Handle task failure and fix task duplication
This commit is contained in:
parent
8d7f15411a
commit
a6dda68d50
4 changed files with 137 additions and 66 deletions
|
@ -1,22 +1,34 @@
|
||||||
import { renderHook } from "@testing-library/react-hooks";
|
import { renderHook } from "@testing-library/react-hooks";
|
||||||
|
import { createMockClient } from "mock-apollo-client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
backgroundTasksRefreshTime,
|
backgroundTasksRefreshTime,
|
||||||
useBackgroundTasks
|
useBackgroundTasks
|
||||||
} from "./BackgroundTasksProvider";
|
} from "./BackgroundTasksProvider";
|
||||||
import { Task, TaskData } from "./types";
|
import { OnCompletedTaskData, Task, TaskData, TaskStatus } from "./types";
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
function renderBackgroundTasks() {
|
||||||
|
const mockClient = createMockClient();
|
||||||
|
const intl = {
|
||||||
|
formatMessage: ({ defaultMessage }) => defaultMessage
|
||||||
|
};
|
||||||
|
|
||||||
|
return renderHook(() =>
|
||||||
|
useBackgroundTasks(mockClient, jest.fn(), intl as any)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
describe("Background task provider", () => {
|
describe("Background task provider", () => {
|
||||||
it("can queue a task", done => {
|
it("can queue a task", done => {
|
||||||
const handle = jest.fn<Promise<boolean>, []>(
|
const handle = jest.fn<Promise<TaskStatus>, []>(
|
||||||
() => new Promise(resolve => resolve(true))
|
() => new Promise(resolve => resolve(TaskStatus.SUCCESS))
|
||||||
);
|
);
|
||||||
const onCompleted = jest.fn();
|
const onCompleted = jest.fn();
|
||||||
const onError = jest.fn();
|
const onError = jest.fn();
|
||||||
|
|
||||||
const { result } = renderHook(useBackgroundTasks);
|
const { result } = renderBackgroundTasks();
|
||||||
|
|
||||||
const taskId = result.current.queue(Task.CUSTOM, {
|
const taskId = result.current.queue(Task.CUSTOM, {
|
||||||
handle,
|
handle,
|
||||||
|
@ -24,23 +36,23 @@ describe("Background task provider", () => {
|
||||||
onError
|
onError
|
||||||
});
|
});
|
||||||
expect(taskId).toBe(1);
|
expect(taskId).toBe(1);
|
||||||
expect(handle).not.toHaveBeenCalled();
|
expect(handle).toHaveBeenCalledTimes(0);
|
||||||
expect(onCompleted).not.toHaveBeenCalled();
|
expect(onCompleted).toHaveBeenCalledTimes(0);
|
||||||
expect(onError).not.toHaveBeenCalled();
|
expect(onError).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
jest.runOnlyPendingTimers();
|
jest.runOnlyPendingTimers();
|
||||||
|
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
expect(handle).toHaveBeenCalled();
|
expect(handle).toHaveBeenCalledTimes(1);
|
||||||
expect(onCompleted).toHaveBeenCalled();
|
expect(onCompleted).toHaveBeenCalledTimes(1);
|
||||||
expect(onError).not.toHaveBeenCalled();
|
expect(onError).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can handle task error", done => {
|
it("can handle task error", done => {
|
||||||
const handle = jest.fn<Promise<boolean>, []>(
|
const handle = jest.fn<Promise<TaskStatus>, []>(
|
||||||
() =>
|
() =>
|
||||||
new Promise(() => {
|
new Promise(() => {
|
||||||
throw new Error("dummy error");
|
throw new Error("dummy error");
|
||||||
|
@ -49,7 +61,7 @@ describe("Background task provider", () => {
|
||||||
const onCompleted = jest.fn();
|
const onCompleted = jest.fn();
|
||||||
const onError = jest.fn();
|
const onError = jest.fn();
|
||||||
|
|
||||||
const { result } = renderHook(useBackgroundTasks);
|
const { result } = renderBackgroundTasks();
|
||||||
|
|
||||||
result.current.queue(Task.CUSTOM, {
|
result.current.queue(Task.CUSTOM, {
|
||||||
handle,
|
handle,
|
||||||
|
@ -60,9 +72,36 @@ describe("Background task provider", () => {
|
||||||
jest.runOnlyPendingTimers();
|
jest.runOnlyPendingTimers();
|
||||||
|
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
expect(handle).toHaveBeenCalled();
|
expect(handle).toHaveBeenCalledTimes(1);
|
||||||
expect(onCompleted).not.toHaveBeenCalled();
|
expect(onCompleted).toHaveBeenCalledTimes(0);
|
||||||
expect(onError).toHaveBeenCalled();
|
expect(onError).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can handle task failure", done => {
|
||||||
|
const handle = jest.fn<Promise<TaskStatus>, []>(
|
||||||
|
() => new Promise(resolve => resolve(TaskStatus.FAILURE))
|
||||||
|
);
|
||||||
|
const onCompleted = jest.fn((data: OnCompletedTaskData) =>
|
||||||
|
expect(data.status).toBe(TaskStatus.FAILURE)
|
||||||
|
);
|
||||||
|
const onError = jest.fn();
|
||||||
|
|
||||||
|
const { result } = renderBackgroundTasks();
|
||||||
|
|
||||||
|
result.current.queue(Task.CUSTOM, {
|
||||||
|
handle,
|
||||||
|
onCompleted,
|
||||||
|
onError
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
expect(handle).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onCompleted).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
@ -71,10 +110,10 @@ describe("Background task provider", () => {
|
||||||
it("can cancel task", done => {
|
it("can cancel task", done => {
|
||||||
const onCompleted = jest.fn();
|
const onCompleted = jest.fn();
|
||||||
|
|
||||||
const { result } = renderHook(useBackgroundTasks);
|
const { result } = renderBackgroundTasks();
|
||||||
|
|
||||||
const taskId = result.current.queue(Task.CUSTOM, {
|
const taskId = result.current.queue(Task.CUSTOM, {
|
||||||
handle: () => new Promise(resolve => resolve(true)),
|
handle: () => new Promise(resolve => resolve(TaskStatus.SUCCESS)),
|
||||||
onCompleted
|
onCompleted
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -85,51 +124,63 @@ describe("Background task provider", () => {
|
||||||
jest.runOnlyPendingTimers();
|
jest.runOnlyPendingTimers();
|
||||||
|
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
expect(onCompleted).not.toHaveBeenCalled();
|
expect(onCompleted).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can queue multiple tasks", done => {
|
it("can queue multiple tasks", done => {
|
||||||
const responses: Array<Promise<boolean>> = [
|
let cycle = 0;
|
||||||
new Promise(resolve =>
|
|
||||||
setTimeout(() => resolve(true), backgroundTasksRefreshTime * 1.4)
|
const tasks: TaskData[] = [
|
||||||
),
|
{
|
||||||
new Promise(resolve =>
|
handle: jest.fn(() =>
|
||||||
setTimeout(() => resolve(true), backgroundTasksRefreshTime * 2.1)
|
Promise.resolve(cycle > 1 ? TaskStatus.SUCCESS : TaskStatus.PENDING)
|
||||||
)
|
),
|
||||||
|
onCompleted: jest.fn()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: jest.fn(() =>
|
||||||
|
Promise.resolve(cycle > 2 ? TaskStatus.SUCCESS : TaskStatus.PENDING)
|
||||||
|
),
|
||||||
|
onCompleted: jest.fn()
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const tasks: TaskData[] = responses.map(response => ({
|
const { result } = renderBackgroundTasks();
|
||||||
handle: () => response,
|
|
||||||
onCompleted: jest.fn()
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { result } = renderHook(useBackgroundTasks);
|
|
||||||
|
|
||||||
tasks.forEach(task => result.current.queue(Task.CUSTOM, task));
|
tasks.forEach(task => result.current.queue(Task.CUSTOM, task));
|
||||||
|
|
||||||
// Set time to backgroundTasksRefreshTime
|
// Set time to backgroundTasksRefreshTime
|
||||||
|
cycle += 1;
|
||||||
jest.advanceTimersByTime(backgroundTasksRefreshTime + 100);
|
jest.advanceTimersByTime(backgroundTasksRefreshTime + 100);
|
||||||
|
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
expect(tasks[0].onCompleted).not.toHaveBeenCalled();
|
expect(tasks[0].handle).toHaveBeenCalledTimes(1);
|
||||||
expect(tasks[1].onCompleted).not.toHaveBeenCalled();
|
expect(tasks[1].handle).toHaveBeenCalledTimes(1);
|
||||||
|
expect(tasks[0].onCompleted).toHaveBeenCalledTimes(0);
|
||||||
|
expect(tasks[1].onCompleted).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
// Set time to backgroundTasksRefreshTime * 2
|
// Set time to backgroundTasksRefreshTime * 2
|
||||||
|
cycle += 1;
|
||||||
jest.advanceTimersByTime(backgroundTasksRefreshTime);
|
jest.advanceTimersByTime(backgroundTasksRefreshTime);
|
||||||
|
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
expect(tasks[0].onCompleted).toHaveBeenCalled();
|
expect(tasks[0].handle).toHaveBeenCalledTimes(2);
|
||||||
expect(tasks[1].onCompleted).not.toHaveBeenCalled();
|
expect(tasks[1].handle).toHaveBeenCalledTimes(2);
|
||||||
|
expect(tasks[0].onCompleted).toHaveBeenCalledTimes(1);
|
||||||
|
expect(tasks[1].onCompleted).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
// Set time to backgroundTasksRefreshTime * 3
|
// Set time to backgroundTasksRefreshTime * 3
|
||||||
|
cycle += 1;
|
||||||
jest.advanceTimersByTime(backgroundTasksRefreshTime);
|
jest.advanceTimersByTime(backgroundTasksRefreshTime);
|
||||||
|
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
expect(tasks[0].onCompleted).toHaveBeenCalled();
|
expect(tasks[0].handle).toHaveBeenCalledTimes(2);
|
||||||
expect(tasks[1].onCompleted).toHaveBeenCalled();
|
expect(tasks[1].handle).toHaveBeenCalledTimes(3);
|
||||||
|
expect(tasks[0].onCompleted).toHaveBeenCalledTimes(1);
|
||||||
|
expect(tasks[1].onCompleted).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
|
import { IMessageContext } from "@saleor/components/messages";
|
||||||
|
import useNotifier from "@saleor/hooks/useNotifier";
|
||||||
|
import ApolloClient from "apollo-client";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useApolloClient } from "react-apollo";
|
||||||
|
import { IntlShape, useIntl } from "react-intl";
|
||||||
|
|
||||||
import BackgroundTasksContext from "./context";
|
import BackgroundTasksContext from "./context";
|
||||||
import { handleTask, queueCustom } from "./tasks";
|
import { handleTask, queueCustom } from "./tasks";
|
||||||
|
@ -6,31 +11,35 @@ import { QueuedTask, Task, TaskData, TaskStatus } from "./types";
|
||||||
|
|
||||||
export const backgroundTasksRefreshTime = 15 * 1000;
|
export const backgroundTasksRefreshTime = 15 * 1000;
|
||||||
|
|
||||||
export function useBackgroundTasks() {
|
// TODO: Remove underscores when these arguments would be finally useful
|
||||||
|
export function useBackgroundTasks(
|
||||||
|
_apolloClient: ApolloClient<any>,
|
||||||
|
_notify: IMessageContext,
|
||||||
|
_intl: IntlShape
|
||||||
|
) {
|
||||||
const idCounter = React.useRef(0);
|
const idCounter = React.useRef(0);
|
||||||
const tasks = React.useRef<QueuedTask[]>([]);
|
const tasks = React.useRef<QueuedTask[]>([]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
const queue = async () => {
|
const queue = async () => {
|
||||||
tasks.current = tasks.current.filter(
|
|
||||||
task => task.status !== TaskStatus.ENDED
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
tasks.current.map(async task => {
|
tasks.current.map(async task => {
|
||||||
let hasFinished: boolean;
|
if (task.status === TaskStatus.PENDING) {
|
||||||
|
let status: TaskStatus;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
hasFinished = await handleTask(task);
|
status = await handleTask(task);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
if (hasFinished) {
|
if (status !== TaskStatus.PENDING) {
|
||||||
const taskIndex = tasks.current.findIndex(
|
const taskIndex = tasks.current.findIndex(
|
||||||
t => t.id === task.id
|
t => t.id === task.id
|
||||||
);
|
);
|
||||||
tasks.current[taskIndex].status = TaskStatus.ENDED;
|
tasks.current[taskIndex].status = status;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -51,10 +60,10 @@ export function useBackgroundTasks() {
|
||||||
|
|
||||||
function queue(type: Task, data?: TaskData) {
|
function queue(type: Task, data?: TaskData) {
|
||||||
idCounter.current += 1;
|
idCounter.current += 1;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case Task.CUSTOM:
|
case Task.CUSTOM:
|
||||||
queueCustom(idCounter.current, tasks, data);
|
queueCustom(idCounter.current, tasks, data);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return idCounter.current;
|
return idCounter.current;
|
||||||
|
@ -67,7 +76,10 @@ export function useBackgroundTasks() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const BackgroundTasksProvider: React.FC = ({ children }) => {
|
const BackgroundTasksProvider: React.FC = ({ children }) => {
|
||||||
const { cancel, queue } = useBackgroundTasks();
|
const apolloClient = useApolloClient();
|
||||||
|
const notify = useNotifier();
|
||||||
|
const intl = useIntl();
|
||||||
|
const { cancel, queue } = useBackgroundTasks(apolloClient, notify, intl);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BackgroundTasksContext.Provider
|
<BackgroundTasksContext.Provider
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
import { QueuedTask, TaskData, TaskStatus } from "./types";
|
import { QueuedTask, TaskData, TaskStatus } from "./types";
|
||||||
|
|
||||||
export async function handleTask(task: QueuedTask): Promise<boolean> {
|
export async function handleTask(task: QueuedTask): Promise<TaskStatus> {
|
||||||
let ok = false;
|
let status = TaskStatus.PENDING;
|
||||||
try {
|
try {
|
||||||
ok = await task.handle();
|
status = await task.handle();
|
||||||
if (ok) {
|
if (status !== TaskStatus.PENDING) {
|
||||||
task.onCompleted();
|
task.onCompleted({
|
||||||
|
status
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
task.onError(error);
|
task.onError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ok;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleError(error: Error) {
|
export function handleError(error: Error) {
|
||||||
|
@ -28,7 +30,6 @@ export function queueCustom(
|
||||||
.forEach(field => {
|
.forEach(field => {
|
||||||
throw new Error(`${field} is required when creating custom task`);
|
throw new Error(`${field} is required when creating custom task`);
|
||||||
});
|
});
|
||||||
|
|
||||||
tasks.current = [
|
tasks.current = [
|
||||||
...tasks.current,
|
...tasks.current,
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,21 +2,28 @@ export enum Task {
|
||||||
CUSTOM
|
CUSTOM
|
||||||
}
|
}
|
||||||
export enum TaskStatus {
|
export enum TaskStatus {
|
||||||
|
FAILURE,
|
||||||
PENDING,
|
PENDING,
|
||||||
ENDED
|
SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OnCompletedTaskData {
|
||||||
|
status: TaskStatus;
|
||||||
|
}
|
||||||
|
export type OnCompletedTaskFn = (data: OnCompletedTaskData) => void;
|
||||||
|
|
||||||
export interface QueuedTask {
|
export interface QueuedTask {
|
||||||
id: number;
|
id: number;
|
||||||
handle: () => Promise<boolean>;
|
handle: () => Promise<TaskStatus>;
|
||||||
status: TaskStatus;
|
status: TaskStatus;
|
||||||
onCompleted: () => void;
|
onCompleted: OnCompletedTaskFn;
|
||||||
onError: (error: Error) => void;
|
onError: (error: Error) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskData {
|
export interface TaskData {
|
||||||
handle?: () => Promise<boolean>;
|
id?: string;
|
||||||
onCompleted?: () => void;
|
handle?: () => Promise<TaskStatus>;
|
||||||
|
onCompleted?: OnCompletedTaskFn;
|
||||||
onError?: () => void;
|
onError?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue