Merge pull request #574 from mirumee/add/background-tasks
Add background task manager
This commit is contained in:
commit
e7a0495556
9 changed files with 318 additions and 5 deletions
|
@ -56,6 +56,7 @@ All notable, unreleased changes to this project will be documented in this file.
|
||||||
- Update product stock management to newest design - #515 by @dominik-zeglen
|
- Update product stock management to newest design - #515 by @dominik-zeglen
|
||||||
- Handle untracked products - #523 by @dominik-zeglen
|
- Handle untracked products - #523 by @dominik-zeglen
|
||||||
- Display correct error if there were no graphql errors - #525 by @dominik-zeglen
|
- Display correct error if there were no graphql errors - #525 by @dominik-zeglen
|
||||||
|
- Add background task manager - #574 by @dominik-zeglen
|
||||||
|
|
||||||
## 2.0.0
|
## 2.0.0
|
||||||
|
|
||||||
|
|
139
src/containers/BackgroundTasks/BackgroundTasksProvider.test.tsx
Normal file
139
src/containers/BackgroundTasks/BackgroundTasksProvider.test.tsx
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
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", done => {
|
||||||
|
const handle = jest.fn<Promise<boolean>, []>(
|
||||||
|
() => new Promise(resolve => resolve(true))
|
||||||
|
);
|
||||||
|
const onCompleted = jest.fn();
|
||||||
|
const onError = jest.fn();
|
||||||
|
|
||||||
|
const { result } = renderHook(useBackgroundTasks);
|
||||||
|
|
||||||
|
const taskId = result.current.queue(Task.CUSTOM, {
|
||||||
|
handle,
|
||||||
|
onCompleted,
|
||||||
|
onError
|
||||||
|
});
|
||||||
|
expect(taskId).toBe(1);
|
||||||
|
expect(handle).not.toHaveBeenCalled();
|
||||||
|
expect(onCompleted).not.toHaveBeenCalled();
|
||||||
|
expect(onError).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
expect(handle).toHaveBeenCalled();
|
||||||
|
expect(onCompleted).toHaveBeenCalled();
|
||||||
|
expect(onError).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can handle task error", done => {
|
||||||
|
const handle = jest.fn<Promise<boolean>, []>(
|
||||||
|
() =>
|
||||||
|
new Promise(() => {
|
||||||
|
throw new Error("dummy error");
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const onCompleted = jest.fn();
|
||||||
|
const onError = jest.fn();
|
||||||
|
|
||||||
|
const { result } = renderHook(useBackgroundTasks);
|
||||||
|
|
||||||
|
result.current.queue(Task.CUSTOM, {
|
||||||
|
handle,
|
||||||
|
onCompleted,
|
||||||
|
onError
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
expect(handle).toHaveBeenCalled();
|
||||||
|
expect(onCompleted).not.toHaveBeenCalled();
|
||||||
|
expect(onError).toHaveBeenCalled();
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can cancel task", done => {
|
||||||
|
const onCompleted = jest.fn();
|
||||||
|
|
||||||
|
const { result } = renderHook(useBackgroundTasks);
|
||||||
|
|
||||||
|
const taskId = result.current.queue(Task.CUSTOM, {
|
||||||
|
handle: () => new Promise(resolve => resolve(true)),
|
||||||
|
onCompleted
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cancel task before executing it
|
||||||
|
jest.advanceTimersByTime(backgroundTasksRefreshTime * 0.9);
|
||||||
|
result.current.cancel(taskId);
|
||||||
|
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
expect(onCompleted).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can queue multiple tasks", done => {
|
||||||
|
const responses: Array<Promise<boolean>> = [
|
||||||
|
new Promise(resolve =>
|
||||||
|
setTimeout(() => resolve(true), backgroundTasksRefreshTime * 1.4)
|
||||||
|
),
|
||||||
|
new Promise(resolve =>
|
||||||
|
setTimeout(() => resolve(true), backgroundTasksRefreshTime * 2.1)
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
const tasks: TaskData[] = responses.map(response => ({
|
||||||
|
handle: () => response,
|
||||||
|
onCompleted: jest.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { result } = renderHook(useBackgroundTasks);
|
||||||
|
|
||||||
|
tasks.forEach(task => result.current.queue(Task.CUSTOM, task));
|
||||||
|
|
||||||
|
// Set time to backgroundTasksRefreshTime
|
||||||
|
jest.advanceTimersByTime(backgroundTasksRefreshTime + 100);
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
expect(tasks[0].onCompleted).not.toHaveBeenCalled();
|
||||||
|
expect(tasks[1].onCompleted).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Set time to backgroundTasksRefreshTime * 2
|
||||||
|
jest.advanceTimersByTime(backgroundTasksRefreshTime);
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
expect(tasks[0].onCompleted).toHaveBeenCalled();
|
||||||
|
expect(tasks[1].onCompleted).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Set time to backgroundTasksRefreshTime * 3
|
||||||
|
jest.advanceTimersByTime(backgroundTasksRefreshTime);
|
||||||
|
|
||||||
|
setImmediate(() => {
|
||||||
|
expect(tasks[0].onCompleted).toHaveBeenCalled();
|
||||||
|
expect(tasks[1].onCompleted).toHaveBeenCalled();
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
85
src/containers/BackgroundTasks/BackgroundTasksProvider.tsx
Normal file
85
src/containers/BackgroundTasks/BackgroundTasksProvider.tsx
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import BackgroundTasksContext from "./context";
|
||||||
|
import { handleTask, queueCustom } from "./tasks";
|
||||||
|
import { QueuedTask, Task, TaskData, TaskStatus } 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(() => {
|
||||||
|
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);
|
||||||
|
|
||||||
|
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, TaskStatus } from "./types";
|
||||||
|
|
||||||
|
export async function handleTask(task: QueuedTask): Promise<boolean> {
|
||||||
|
let ok = false;
|
||||||
|
try {
|
||||||
|
ok = await task.handle();
|
||||||
|
if (ok) {
|
||||||
|
task.onCompleted();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
task.onError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
status: TaskStatus.PENDING
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
26
src/containers/BackgroundTasks/types.ts
Normal file
26
src/containers/BackgroundTasks/types.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
export enum Task {
|
||||||
|
CUSTOM
|
||||||
|
}
|
||||||
|
export enum TaskStatus {
|
||||||
|
PENDING,
|
||||||
|
ENDED
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueuedTask {
|
||||||
|
id: number;
|
||||||
|
handle: () => Promise<boolean>;
|
||||||
|
status: TaskStatus;
|
||||||
|
onCompleted: () => void;
|
||||||
|
onError: (error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskData {
|
||||||
|
handle?: () => Promise<boolean>;
|
||||||
|
onCompleted?: () => void;
|
||||||
|
onError?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackgroundTasksContextType {
|
||||||
|
cancel: (id: number) => void;
|
||||||
|
queue: (type: Task, data?: TaskData) => void;
|
||||||
|
}
|
8
src/hooks/useBackgroundTask.ts
Normal file
8
src/hooks/useBackgroundTask.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import BackgroundTasksContext from "@saleor/containers/BackgroundTasks/context";
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
function useBackgroundTask() {
|
||||||
|
return useContext(BackgroundTasksContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useBackgroundTask;
|
|
@ -35,6 +35,7 @@ import { WindowTitle } from "./components/WindowTitle";
|
||||||
import { API_URI, APP_MOUNT_URI, GTM_ID } from "./config";
|
import { API_URI, APP_MOUNT_URI, GTM_ID } from "./config";
|
||||||
import ConfigurationSection, { createConfigurationMenu } from "./configuration";
|
import ConfigurationSection, { createConfigurationMenu } from "./configuration";
|
||||||
import AppStateProvider from "./containers/AppState";
|
import AppStateProvider from "./containers/AppState";
|
||||||
|
import BackgroundTasksProvider from "./containers/BackgroundTasks";
|
||||||
import { CustomerSection } from "./customers";
|
import { CustomerSection } from "./customers";
|
||||||
import DiscountSection from "./discounts";
|
import DiscountSection from "./discounts";
|
||||||
import HomePage from "./home";
|
import HomePage from "./home";
|
||||||
|
@ -135,11 +136,13 @@ const App: React.FC = () => {
|
||||||
<DateProvider>
|
<DateProvider>
|
||||||
<LocaleProvider>
|
<LocaleProvider>
|
||||||
<MessageManager>
|
<MessageManager>
|
||||||
|
<BackgroundTasksProvider>
|
||||||
<AppStateProvider>
|
<AppStateProvider>
|
||||||
<ShopProvider>
|
<ShopProvider>
|
||||||
<Routes />
|
<Routes />
|
||||||
</ShopProvider>
|
</ShopProvider>
|
||||||
</AppStateProvider>
|
</AppStateProvider>
|
||||||
|
</BackgroundTasksProvider>
|
||||||
</MessageManager>
|
</MessageManager>
|
||||||
</LocaleProvider>
|
</LocaleProvider>
|
||||||
</DateProvider>
|
</DateProvider>
|
||||||
|
|
Loading…
Reference in a new issue