Skip to content

Commit

Permalink
Handle asynchronous tasks
Browse files Browse the repository at this point in the history
  • Loading branch information
dominik-zeglen committed Jun 21, 2020
1 parent ee86028 commit 35a89bf
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 48 deletions.
100 changes: 65 additions & 35 deletions src/containers/BackgroundTasks/BackgroundTasksProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,101 +9,131 @@ import { Task, TaskData } from "./types";
jest.useFakeTimers();

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 onError = jest.fn();

const { result } = renderHook(useBackgroundTasks);

const taskId = result.current.queue(Task.CUSTOM, {
handle: () => true,
handle,
onCompleted,
onError
});
expect(taskId).toBe(1);
expect(handle).not.toHaveBeenCalled();
expect(onCompleted).not.toHaveBeenCalled();
expect(onError).not.toHaveBeenCalled();

jest.runOnlyPendingTimers();

expect(onCompleted).toHaveBeenCalled();
expect(onError).not.toHaveBeenCalled();
setImmediate(() => {
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 onError = jest.fn();

const { result } = renderHook(useBackgroundTasks);

result.current.queue(Task.CUSTOM, {
handle: () => {
throw new Error("dummy error");
},
handle,
onCompleted,
onError
});

jest.runOnlyPendingTimers();

expect(onCompleted).not.toHaveBeenCalled();
expect(onError).toHaveBeenCalled();
setImmediate(() => {
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 { result } = renderHook(useBackgroundTasks);

const taskId = result.current.queue(Task.CUSTOM, {
handle: () => true,
handle: () => new Promise(resolve => resolve(true)),
onCompleted
});

jest.advanceTimersByTime(backgroundTasksRefreshTime - 1000);
// Cancel task before executing it
jest.advanceTimersByTime(backgroundTasksRefreshTime * 0.9);
result.current.cancel(taskId);

jest.runOnlyPendingTimers();

expect(onCompleted).not.toHaveBeenCalled();
setImmediate(() => {
expect(onCompleted).not.toHaveBeenCalled();

done();
});
});

it("can queue multiple tasks", () => {
const responses = [
{
finished: false
},
{
finished: false
}
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.finished,
handle: () => response,
onCompleted: jest.fn()
}));

const { result } = renderHook(useBackgroundTasks);

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();
expect(tasks[1].onCompleted).not.toHaveBeenCalled();
setImmediate(() => {
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();
expect(tasks[1].onCompleted).not.toHaveBeenCalled();
// Set time to backgroundTasksRefreshTime * 3
jest.advanceTimersByTime(backgroundTasksRefreshTime);

responses[1].finished = true;
setImmediate(() => {
expect(tasks[1].onCompleted).toHaveBeenCalled();
expect(tasks[1].onCompleted).toHaveBeenCalled();

jest.runOnlyPendingTimers();

expect(tasks[1].onCompleted).toHaveBeenCalled();
expect(tasks[1].onCompleted).toHaveBeenCalled();
done();
});
});
});
});
});
31 changes: 29 additions & 2 deletions src/containers/BackgroundTasks/BackgroundTasksProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";

import BackgroundTasksContext from "./context";
import { handleTask, queueCustom } from "./tasks";
import { QueuedTask, Task, TaskData } from "./types";
import { QueuedTask, Task, TaskData, TaskStatus } from "./types";

export const backgroundTasksRefreshTime = 15 * 1000;

Expand All @@ -12,7 +12,34 @@ export function useBackgroundTasks() {

React.useEffect(() => {
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);

return () => clearInterval(intervalId);
Expand Down
18 changes: 9 additions & 9 deletions src/containers/BackgroundTasks/tasks.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { QueuedTask, TaskData } from "./types";
import { QueuedTask, TaskData, TaskStatus } from "./types";

export function handleTask(task: QueuedTask) {
let ok: boolean;
export async function handleTask(task: QueuedTask): Promise<boolean> {
let ok = false;
try {
ok = task.handle();
ok = await task.handle();
if (ok) {
task.onCompleted();
}
} catch (error) {
task.onError(error);
}

if (ok) {
task.onCompleted();
}

return ok;
}

Expand All @@ -36,7 +35,8 @@ export function queueCustom(
handle: data.handle,
id,
onCompleted: data.onCompleted,
onError: data.onError || handleError
onError: data.onError || handleError,
status: TaskStatus.PENDING
}
];
}
9 changes: 7 additions & 2 deletions src/containers/BackgroundTasks/types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
export enum Task {
CUSTOM
}
export enum TaskStatus {
PENDING,
ENDED
}

export interface QueuedTask {
id: number;
handle: () => boolean;
handle: () => Promise<boolean>;
status: TaskStatus;
onCompleted: () => void;
onError: (error: Error) => void;
}

export interface TaskData {
handle?: () => boolean;
handle?: () => Promise<boolean>;
onCompleted?: () => void;
onError?: () => void;
}
Expand Down

0 comments on commit 35a89bf

Please sign in to comment.