Created
November 12, 2019 21:03
-
-
Save kalda341/d59a4e7de9d8e144e9579b4e50326ef9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const Task = (func, args) => { | |
| const taskState = { | |
| isRunning: false, | |
| isCancelled: false, | |
| isFinished: false, | |
| isError: false, | |
| result: null, | |
| error: null | |
| }; | |
| let resolve = null; | |
| let reject = null; | |
| const promise = new Promise((innerResolve, innerReject) => { | |
| resolve = innerResolve; | |
| reject = innerReject; | |
| }); | |
| const start = () => { | |
| const success = result => { | |
| taskState.isRunning = false; | |
| taskState.isCancelled = false; | |
| taskState.isSuccess = true; | |
| taskState.isError = false; | |
| taskState.result = result; | |
| taskState.error = null; | |
| resolve(result); | |
| }; | |
| const error = error => { | |
| taskState.isRunning = false; | |
| taskState.isCancelled = false; | |
| taskState.isSuccess = false; | |
| taskState.isError = true; | |
| taskState.result = null; | |
| taskState.error = error; | |
| reject(error); | |
| }; | |
| const cancelled = () => { | |
| // The state should already be set by the cancel function | |
| reject(new Error('Task was cancelled')); | |
| }; | |
| // Immediately invoked function | |
| (async () => { | |
| // Handle cancel before starting generator | |
| if (taskState.isCancelled) { | |
| return cancelled(); | |
| } | |
| const generator = func(...args); | |
| let currentValue = null; | |
| // eslint-disable-next-line no-constant-condition | |
| while (true) { | |
| try { | |
| const intermediateResult = generator.next(currentValue); | |
| currentValue = await intermediateResult.value; | |
| // Handle cancel check after any successful async operation | |
| if (taskState.isCancelled) { | |
| return cancelled(); | |
| } else if (intermediateResult.done) { | |
| return success(currentValue); | |
| } | |
| } catch (e) { | |
| // Handle cancel check after any unsuccessful async operation | |
| if (taskState.isCancelled) { | |
| return cancelled(e); | |
| } | |
| // Let the task function handle the error if it can | |
| try { | |
| generator.throw(e); | |
| } catch (e) { | |
| return error(e); | |
| } | |
| } | |
| } | |
| })(); | |
| return promise; | |
| }; | |
| const cancel = () => { | |
| if (!taskState.isFinished && !taskState.isError) { | |
| taskState.isRunning = false; | |
| taskState.isCancelled = true; | |
| taskState.isSuccess = false; | |
| taskState.isError = false; | |
| taskState.result = null; | |
| taskState.error = null; | |
| } | |
| }; | |
| // Actions | |
| taskState.start = start; | |
| taskState.cancel = cancel; | |
| // Promise functions | |
| taskState.catch = promise.catch.bind(promise); | |
| taskState.then = promise.then.bind(promise); | |
| taskState.finally = promise.finally.bind(promise); | |
| return taskState; | |
| }; | |
| const TaskFactory = (func, strategy) => { | |
| let queuedTasks = []; | |
| let runningTasks = []; | |
| let isRunning = false; | |
| const setRunningTasks = tasks => { | |
| runningTasks = tasks; | |
| isRunning = runningTasks.length !== 0; | |
| } | |
| const queueTask = task => { | |
| queuedTasks = append(task, queuedTasks); | |
| } | |
| const maybeStartQueuedTask = () => { | |
| if (queuedTasks.length && (strategy != 'ENQUEUE' || !isRunning)) { | |
| const task = queuedTasks[0]; | |
| // Note - compare by refrence | |
| queuedTasks = reject(t => t === task, queuedTasks); | |
| setRunningTasks(append(task, runningTasks)); | |
| task.start(); | |
| } | |
| } | |
| const taskFinished = task => { | |
| // Note - compare by refrence | |
| setRunningTasks(reject(t => t === task, runningTasks)); | |
| maybeStartQueuedTask(); | |
| } | |
| const perform = (...args) => performFunc(func, args); | |
| const performFunc = (func, args) => { | |
| const task = Task(func, args); | |
| if (strategy === 'RESTARTABLE') { | |
| cancelAll(); | |
| } else if (strategy === 'DROP' && isRunning) { | |
| task.cancel(); | |
| return task; | |
| } | |
| queueTask(task); | |
| maybeStartQueuedTask(); | |
| task | |
| // Prevent uncaught promise error | |
| .catch(() => {}) | |
| .finally(() => { | |
| taskFinished(task); | |
| }); | |
| return task; | |
| }; | |
| const cancelAll = () => { | |
| queuedTasks = []; | |
| forEach(task => task.cancel(), runningTasks); | |
| } | |
| return { | |
| perform, | |
| performFunc, | |
| cancelAll, | |
| }; | |
| } | |
| export const useTask = (func, strategy) => { | |
| const factoryRef = useRef(TaskFactory(null, strategy)); | |
| const factory = factoryRef.current; | |
| // Cancel all on unmount | |
| useEffect(() => { | |
| return () => { | |
| factory.cancelAll(); | |
| } | |
| }, []); | |
| // Override perform to always use the most recent function | |
| return merge( | |
| factory, | |
| { perform: (...args) => factory.performFunc(func, args) } | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment