From bdf90039b503da9ac6f21ce3c5c7269a92f489f7 Mon Sep 17 00:00:00 2001 From: dominik-zeglen Date: Sun, 21 Jun 2020 17:31:33 +0200 Subject: [PATCH] Handle asynchronous tasks --- .../BackgroundTasksProvider.test.tsx | 100 ++++++++++++------ .../BackgroundTasksProvider.tsx | 31 +++++- src/containers/BackgroundTasks/tasks.ts | 18 ++-- src/containers/BackgroundTasks/types.ts | 9 +- 4 files changed, 110 insertions(+), 48 deletions(-) diff --git a/src/containers/BackgroundTasks/BackgroundTasksProvider.test.tsx b/src/containers/BackgroundTasks/BackgroundTasksProvider.test.tsx index 72c57cbae..23f29b4d5 100644 --- a/src/containers/BackgroundTasks/BackgroundTasksProvider.test.tsx +++ b/src/containers/BackgroundTasks/BackgroundTasksProvider.test.tsx @@ -9,77 +9,100 @@ 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, []>( + () => 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, []>( + () => + 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> = [ + 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() })); @@ -87,23 +110,30 @@ describe("Background task provider", () => { 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(); + }); + }); + }); }); }); diff --git a/src/containers/BackgroundTasks/BackgroundTasksProvider.tsx b/src/containers/BackgroundTasks/BackgroundTasksProvider.tsx index 7a1b34007..7199956d9 100644 --- a/src/containers/BackgroundTasks/BackgroundTasksProvider.tsx +++ b/src/containers/BackgroundTasks/BackgroundTasksProvider.tsx @@ -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; @@ -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); diff --git a/src/containers/BackgroundTasks/tasks.ts b/src/containers/BackgroundTasks/tasks.ts index 61a488e94..5fea73b44 100644 --- a/src/containers/BackgroundTasks/tasks.ts +++ b/src/containers/BackgroundTasks/tasks.ts @@ -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 { + 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; } @@ -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 } ]; } diff --git a/src/containers/BackgroundTasks/types.ts b/src/containers/BackgroundTasks/types.ts index 719a6df2c..a4e328e0e 100644 --- a/src/containers/BackgroundTasks/types.ts +++ b/src/containers/BackgroundTasks/types.ts @@ -1,16 +1,21 @@ export enum Task { CUSTOM } +export enum TaskStatus { + PENDING, + ENDED +} export interface QueuedTask { id: number; - handle: () => boolean; + handle: () => Promise; + status: TaskStatus; onCompleted: () => void; onError: (error: Error) => void; } export interface TaskData { - handle?: () => boolean; + handle?: () => Promise; onCompleted?: () => void; onError?: () => void; }