Last active
June 15, 2019 01:05
-
-
Save tim-evans/d6b79d62c01793c3f119f67e2f6f4268 to your computer and use it in GitHub Desktop.
Revisions
-
tim-evans revised this gist
Jun 11, 2019 . 1 changed file with 11 additions and 38 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -4,12 +4,6 @@ export type ParameterType< T extends (...args: any[]) => any > = T extends (...args: infer R) => any ? R : undefined; type TaskRunner = (...args: any[]) => IterableIterator<any>; const startTask = (task: TaskRunner, ...params: ParameterType<TaskRunner>) => { @@ -62,8 +56,8 @@ interface Task { params: ParameterType<TaskRunner>; status: TaskStatus; iterator?: ReturnType<TaskRunner>; promise?: Promise<any>; value?: any; error?: Error; } @@ -89,13 +83,7 @@ function compact(tasks: Task[]) { } const reducer: Reducer<State, Actions> = (state, action) => { let runningTasks = state.tasks.filter(isRunning); switch (action.action) { case 'START_TASK': { @@ -278,19 +266,12 @@ const reducer: Reducer<State, Actions> = (state, action) => { return state; }; function isRunning(task: Task) { return task.status === 'ready' || task.status === 'working' || task.status === 'working*' || task.status === 'finishing' || task.status === 'finishing*'; } interface TaskAPI { @@ -339,14 +320,7 @@ export function useTask<T extends TaskRunner>( dispatch(trackWork(tracked)); } let runningTasks = state.tasks.filter(isRunning); let areAnyReady = state.tasks.some(task => task.status === 'ready'); let areAnyQueued = state.tasks.some(task => task.status === 'queued'); let areAnyRunning = runningTasks.length > 0; @@ -364,14 +338,13 @@ export function useTask<T extends TaskRunner>( let lastTask = state.tasks[state.tasks.length - 1]; let last = lastTask ? { status: isRunning(lastTask) ? 'running' : lastTask.status, value: lastTask.value, error: lastTask.error, cancel() { dispatch(cancelTasks([lastTask])); } } as TaskAPI : null; return [{ isRunning: areAnyRunning, -
tim-evans revised this gist
Jun 11, 2019 . 1 changed file with 3 additions and 2 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -309,7 +309,7 @@ export function useTask<T extends TaskRunner>( ): [ { isRunning: boolean; last: TaskAPI | null; }, (...args: ParameterType<T>) => void ] { @@ -370,7 +370,8 @@ export function useTask<T extends TaskRunner>( cancel() { dispatch(cancelTasks([lastTask])); } } as TaskAPI : null; console.log(last); return [{ isRunning: areAnyRunning, -
tim-evans revised this gist
Jun 11, 2019 . 1 changed file with 136 additions and 24 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -41,14 +41,22 @@ const cancelTasks = (tasks: Task[]) => { } as const; }; const trackWork = (tasks: Task[]) => { return { action: 'TRACK_WORK', tasks } as const; }; type Actions = ReturnType< typeof startTask | typeof startWork | typeof continueWork | typeof cancelTasks | typeof trackWork >; type TaskStatus = 'queued' | 'ready' | 'dropped' | 'working' | 'working*' | 'finishing' | 'finishing*' | 'cancelled' | 'finished'; interface Task { run: TaskRunner; params: ParameterType<TaskRunner>; @@ -65,20 +73,41 @@ interface State { tasks: Task[]; } function compact(tasks: Task[]) { let minimalTasks: Task[] = []; for (let i = 0, len = tasks.length; i < len; i++) { let task = tasks[i]; if (task.status !== 'dropped' && task.status !== 'cancelled' && task.status !== 'finished') { minimalTasks.push(task); } else if (i === len - 1) { minimalTasks.push(task); } } return minimalTasks; } const reducer: Reducer<State, Actions> = (state, action) => { let runningTasks = state.tasks.filter(task => { return task.status === 'ready' || task.status === 'working' || task.status === 'working*' || task.status === 'finishing' || task.status === 'finishing*'; }); switch (action.action) { case 'START_TASK': { if (runningTasks.length === state.maxConcurrency) { switch (state.strategy) { case 'restart': { let tasks = state.tasks.map(task => { if (task === runningTasks[0]) { if (task.status !== 'ready' && task.iterator && task.iterator.return) { task.iterator.return(); } return { ...task, status: 'cancelled' as TaskStatus @@ -89,11 +118,11 @@ const reducer: Reducer<State, Actions> = (state, action) => { return { strategy: state.strategy, maxConcurrency: state.maxConcurrency, tasks: compact([...tasks, { run: action.task, params: action.params, status: 'ready' as TaskStatus }]) }; } case 'drop': { @@ -132,7 +161,10 @@ const reducer: Reducer<State, Actions> = (state, action) => { }; } case 'START_WORK': { let readySlots = state.strategy === 'queue' ? state.maxConcurrency - runningTasks.length : Infinity; let tasks = compact(state.tasks).map(task => { let { status, iterator, value, ...rest } = task; if (task.status === 'ready') { let iterator = task.run(...rest.params); @@ -143,6 +175,16 @@ const reducer: Reducer<State, Actions> = (state, action) => { promise: Promise.resolve(next.value), iterator }; } else if (task.status === 'queued' && readySlots > 0) { readySlots -= 1; let iterator = task.run(...rest.params); let next = iterator.next(); return { ...rest, status: (next.done ? 'finishing' : 'working') as TaskStatus, promise: Promise.resolve(next.value), iterator }; } return task; }); @@ -154,22 +196,23 @@ const reducer: Reducer<State, Actions> = (state, action) => { }; } case 'CONTINUE_WORK': { let tasks = compact(state.tasks).map(task => { if (task.iterator === action.task.iterator) { // The only valid state is from working* if (task.status === 'working*' && task.iterator) { let { promise, ...rest } = task; let next = task.iterator.next(action.value); return { ...rest, status: (next.done ? 'finishing' : 'working') as TaskStatus, promise: Promise.resolve(next.value) }; } else if (task.status === 'finishing*') { return { ...task, value: task.value, status: 'finished' as TaskStatus }; } } return task; @@ -182,14 +225,14 @@ const reducer: Reducer<State, Actions> = (state, action) => { }; } case 'CANCEL_TASKS': { let tasks = compact(state.tasks).map(task => { if (action.tasks.indexOf(task) !== -1) { if (task.status === 'ready' || task.status === 'queued') { return { ...task, status: 'dropped' as TaskStatus }; } else if (task.status === 'working*' || task.status === 'finishing*') { if (task.iterator && task.iterator.return) { task.iterator.return(); } @@ -202,6 +245,29 @@ const reducer: Reducer<State, Actions> = (state, action) => { return task; }); return { strategy: state.strategy, maxConcurrency: state.maxConcurrency, tasks }; } case 'TRACK_WORK': { let tasks = compact(state.tasks).map(task => { if (action.tasks.indexOf(task) !== -1) { let status = task.status; if (status === 'finishing') { status = 'finishing*'; } else if (status === 'working') { status = 'working*'; } return { ...task, status: status as TaskStatus }; } return task; }); return { strategy: state.strategy, maxConcurrency: state.maxConcurrency, @@ -212,14 +278,39 @@ const reducer: Reducer<State, Actions> = (state, action) => { return state; }; // Narrow status to be more usable for consuming code function getStatus(status: TaskStatus) { if ( status === 'ready' || status === 'working' || status === 'working*' || status === 'finishing' || status === 'finishing*' ) { return 'running'; } else { return status; } } interface TaskAPI { status: 'queued' | 'dropped' | 'running' | 'cancelled' | 'finished'; value?: any; error?: Error; cancel(): void; } export function useTask<T extends TaskRunner>( task: T, options?: { strategy: 'restart' | 'drop' | 'queue'; maxConcurrency?: number; } ): [ { isRunning: boolean; last: TaskAPI; }, (...args: ParameterType<T>) => void ] { let maxConcurrency = Infinity; @@ -234,18 +325,26 @@ export function useTask<T extends TaskRunner>( }); // Find all working tasks and schedule more work to be done! let tracked: Task[] = []; state.tasks.filter(task => ['working', 'finishing'].indexOf(task.status) !== -1).forEach(task => { if (task.promise) { tracked.push(task); task.promise.then(result => { dispatch(continueWork(task, result)); }); } }); if (tracked.length > 0) { dispatch(trackWork(tracked)); } let runningTasks = state.tasks.filter(task => { return task.status === 'ready' || task.status === 'working*' || task.status === 'working' || task.status === 'finishing' || task.status === 'finishing*'; }); let areAnyReady = state.tasks.some(task => task.status === 'ready'); @@ -263,5 +362,18 @@ export function useTask<T extends TaskRunner>( dispatch(startTask(task, ...args)); }; let lastTask = state.tasks[state.tasks.length - 1]; let last = lastTask ? { status: getStatus(lastTask.status), value: lastTask.value, error: lastTask.error, cancel() { dispatch(cancelTasks([lastTask])); } } : null; return [{ isRunning: areAnyRunning, last }, run]; } -
tim-evans revised this gist
Jun 10, 2019 . 1 changed file with 80 additions and 51 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -12,13 +12,6 @@ type PromiseType<T> = T extends Promise<infer R> ? R : T; type TaskRunner = (...args: any[]) => IterableIterator<any>; const startTask = (task: TaskRunner, ...params: ParameterType<TaskRunner>) => { return { action: 'START_TASK', @@ -41,17 +34,25 @@ const continueWork = (task: Task, value: any) => { } as const; } const cancelTasks = (tasks: Task[]) => { return { action: 'CANCEL_TASKS', tasks } as const; }; type Actions = ReturnType< typeof startTask | typeof startWork | typeof continueWork | typeof cancelTasks >; type TaskStatus = 'queued' | 'ready' | 'dropped' | 'working' | 'finishing' | 'cancelled' | 'finished'; interface Task { run: TaskRunner; params: ParameterType<TaskRunner>; status: TaskStatus; iterator?: ReturnType<TaskRunner>; promise?: Promise<PromiseType<IteratorType<ReturnType<TaskRunner>>>>; value?: PromiseType<IteratorType<ReturnType<TaskRunner>>>; @@ -67,45 +68,68 @@ interface State { const reducer: Reducer<State, Actions> = (state, action) => { switch (action.action) { case 'START_TASK': { let runningTasks = state.tasks.filter(task => { return task.status === 'ready' || task.status === 'working' || task.status === 'finishing'; }); if (runningTasks.length === state.maxConcurrency) { switch (state.strategy) { case 'restart': { let tasks = state.tasks.map(task => { if (task === runningTasks[0]) { return { ...task, status: 'cancelled' as TaskStatus } } return task; }); return { strategy: state.strategy, maxConcurrency: state.maxConcurrency, tasks: [...tasks, { run: action.task, params: action.params, status: 'dropped' as TaskStatus }] }; } case 'drop': { return { strategy: state.strategy, maxConcurrency: state.maxConcurrency, tasks: [...state.tasks, { run: action.task, params: action.params, status: 'dropped' as TaskStatus }] }; } case 'queue': { return { strategy: state.strategy, maxConcurrency: state.maxConcurrency, tasks: [...state.tasks, { run: action.task, params: action.params, status: 'queued' as TaskStatus }] }; } } } // We can start running the task return { strategy: state.strategy, maxConcurrency: state.maxConcurrency, tasks: [...state.tasks, { run: action.task, params: action.params, status: 'ready' as TaskStatus }] }; } case 'START_WORK': { let tasks = state.tasks.map(task => { @@ -115,7 +139,7 @@ const reducer: Reducer<State, Actions> = (state, action) => { let next = iterator.next(); return { ...rest, status: (next.done ? 'finishing' : 'working') as TaskStatus, promise: Promise.resolve(next.value), iterator }; @@ -138,7 +162,7 @@ const reducer: Reducer<State, Actions> = (state, action) => { let next = task.iterator.next(action.value); return { ...rest, status: (next.done ? 'finishing' : 'working') as TaskStatus, promise: Promise.resolve(next.value) }; } else if (task.status === 'finishing') { @@ -147,9 +171,8 @@ const reducer: Reducer<State, Actions> = (state, action) => { value: task.value } } } return task; }); return { @@ -158,19 +181,25 @@ const reducer: Reducer<State, Actions> = (state, action) => { tasks }; } case 'CANCEL_TASKS': { let tasks = state.tasks.map(task => { if (action.tasks.indexOf(task) !== -1) { if (task.status === 'ready' || task.status === 'queued') { return { ...task, status: 'dropped' as TaskStatus }; } else if (task.status === 'working' || task.status === 'finishing') { if (task.iterator && task.iterator.return) { task.iterator.return(); } return { ...task, status: 'cancelled' as TaskStatus }; } } return task; }); return { @@ -191,7 +220,7 @@ export function useTask<T extends TaskRunner>( } ): [ boolean, (...args: ParameterType<T>) => void ] { let maxConcurrency = Infinity; if (options) { -
tim-evans revised this gist
Jun 10, 2019 . 1 changed file with 95 additions and 34 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -1,4 +1,4 @@ import { Reducer, useReducer, useDebugValue } from 'react'; export type ParameterType< T extends (...args: any[]) => any @@ -7,11 +7,10 @@ export type ParameterType< type IteratorType< T extends IterableIterator<any> > = T extends IterableIterator<infer R> ? R : undefined; type PromiseType<T> = T extends Promise<infer R> ? R : T; type TaskRunner = (...args: any[]) => IterableIterator<any>; const cancelTask = (task: TaskRunner) => { return { @@ -28,23 +27,32 @@ const startTask = (task: TaskRunner, ...params: ParameterType<TaskRunner>) => { } as const; } const startWork = () => { return { action: 'START_WORK', } as const; } const continueWork = (task: Task, value: any) => { return { action: 'CONTINUE_WORK', task, value } as const; } type Actions = ReturnType< typeof startTask | typeof startWork | typeof continueWork | typeof cancelTask >; interface Task { run: TaskRunner; params: ParameterType<TaskRunner>; status: 'queued' | 'ready' | 'dropped' | 'working' | 'finishing' | 'cancelled' | 'finished'; iterator?: ReturnType<TaskRunner>; promise?: Promise<PromiseType<IteratorType<ReturnType<TaskRunner>>>>; value?: PromiseType<IteratorType<ReturnType<TaskRunner>>>; error?: Error; @@ -61,7 +69,7 @@ const reducer: Reducer<State, Actions> = (state, action) => { case 'START_TASK': { let runningTasks = state.tasks.filter(task => task.status === 'working' || task.status === 'finishing' || task.status === 'ready' ); @@ -99,20 +107,17 @@ const reducer: Reducer<State, Actions> = (state, action) => { }); return state; } case 'START_WORK': { let tasks = state.tasks.map(task => { let { status, iterator, value, ...rest } = task; if (task.status === 'ready') { let iterator = task.run(...rest.params); let next = iterator.next(); return { ...rest, status: next.done ? 'finishing' : 'working', promise: Promise.resolve(next.value), iterator }; } return task; @@ -124,20 +129,70 @@ const reducer: Reducer<State, Actions> = (state, action) => { tasks }; } case 'CONTINUE_WORK': { let tasks = state.tasks.map(task => { if (task === action.task) { // The only valid state is from working if (task.status === 'working' && task.iterator) { let { promise, ...rest } = task; let next = task.iterator.next(action.value); return { ...rest, status: next.done ? 'finishing' : 'working', promise: Promise.resolve(next.value) }; } else if (task.status === 'finishing') { return { ...task, value: task.value } } } else { return task; } }); return { strategy: state.strategy, maxConcurrency: state.maxConcurrency, tasks }; } case 'CANCEL_TASK': { let tasks = state.tasks.map(task => { if (task.run === action.task) { if (task.iterator && task.iterator.return) { task.iterator.return(); } return { ...task, status: 'cancelled' }; } else { return task; } }); return { strategy: state.strategy, maxConcurrency: state.maxConcurrency, tasks }; } } return state; }; export function useTask<T extends TaskRunner>( task: T, options?: { strategy: 'restart' | 'drop' | 'queue'; maxConcurrency?: number; } ): [ boolean, (...args: ParameterType<T>) => void; ] { let maxConcurrency = Infinity; if (options) { maxConcurrency = options.maxConcurrency || 1; @@ -149,27 +204,33 @@ export function useTask( tasks: [] }); // Find all working tasks and schedule more work to be done! state.tasks.filter(task => ['working', 'finishing'].indexOf(task.status) !== -1).forEach(task => { if (task.promise) { task.promise.then(result => { dispatch(continueWork(task, result)); }); } }); let runningTasks = state.tasks.filter(task => { return task.status === 'ready' || task.status === 'finishing' || task.status === 'working'; }); let areAnyReady = state.tasks.some(task => task.status === 'ready'); let areAnyQueued = state.tasks.some(task => task.status === 'queued'); let areAnyRunning = runningTasks.length > 0; // Do any work that's necessary if (areAnyReady || (areAnyQueued && runningTasks.length < maxConcurrency)) { dispatch(startWork()); } useDebugValue(areAnyRunning ? 'Running' : 'Idle'); const run = (...args: ParameterType<typeof task>) => { dispatch(startTask(task, ...args)); }; -
tim-evans created this gist
Jun 10, 2019 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,177 @@ import { Reducer, useCallback, useReducer, useDebugValue } from 'react'; export type ParameterType< T extends (...args: any[]) => any > = T extends (...args: infer R) => any ? R : undefined; type IteratorType< T extends IterableIterator<any> > = T extends IterableIterator<infer R> ? R : undefined; type PromiseType< T extends Promise<any> > = T extends Promise<infer R> ? R : undefined; type TaskRunner = (...args: any[]) => IterableIterator<Promise<any>>; const cancelTask = (task: TaskRunner) => { return { action: 'CANCEL_TASK', task } as const; }; const startTask = (task: TaskRunner, ...params: ParameterType<TaskRunner>) => { return { action: 'START_TASK', task, params } as const; } const doWork = () => { return { action: 'DO_WORK', } as const; } type Actions = ReturnType< typeof startTask | typeof doWork | typeof cancelTask >; interface Task { run: TaskRunner; params: ParameterType<TaskRunner>; iterator?: ReturnType<TaskRunner>; status: 'queued' | 'ready' | 'dropped' | 'working' | 'idle' | 'cancelled' | 'finished'; promise?: Promise<PromiseType<IteratorType<ReturnType<TaskRunner>>>>; value?: PromiseType<IteratorType<ReturnType<TaskRunner>>>; error?: Error; } interface State { strategy: 'restart' | 'drop' | 'queue'; maxConcurrency: number; tasks: Task[]; } const reducer: Reducer<State, Actions> = (state, action) => { switch (action.action) { case 'START_TASK': { let runningTasks = state.tasks.filter(task => task.status === 'working' || task.status === 'idle' || task.status === 'ready' ); if (runningTasks.length === state.maxConcurrency) { switch (state.strategy) { case 'restart': { let firstRunningTask = runningTasks[0]; firstRunningTask.status = 'cancelled'; break; } case 'drop': { state.tasks.push({ run: action.task, params: action.params, status: 'dropped' }); return state; } case 'queue': { state.tasks.push({ run: action.task, params: action.params, status: 'queued' }); return state; } } } // We can start running the task state.tasks.push({ run: action.task, params: action.params, status: 'ready' }); return state; } case 'DO_WORK': { let tasks = state.tasks.map(task => { let { status, iterator, value, ...rest } = task; if (task.status === 'ready') { return { ...rest, status: 'idle', iterator: task.run(...rest.params) }; } else if (task.status === 'idle' && iterator) { return { ...rest, status: 'working', promise: iterator.next(value) }; } return task; }); return { strategy: state.strategy, maxConcurrency: state.maxConcurrency, tasks }; } case 'CANCEL_TASK': { } } return state; }; export function useTask( task: TaskRunner, options?: { strategy: 'restart' | 'drop' | 'queue'; maxConcurrency?: number; } ) { let maxConcurrency = Infinity; if (options) { maxConcurrency = options.maxConcurrency || 1; } let [state, dispatch] = useReducer(reducer, { strategy: options ? options.strategy : 'restart', maxConcurrency, tasks: [] }); let areAnyRunning = state.tasks.some(task => { return task.status === 'ready' || task.status === 'idle' || task.status === 'working'; }); // Find all working tasks and schedule more work to be done! state.tasks.filter(task => task.status === 'working').forEach(task => { if (task.promise) { task.promise.then(() => dispatch(doWork())); } }); // Do any work that's necessary if (state.tasks.some(task => task.status === 'ready' || task.status === 'idle')) { dispatch(doWork()); } useDebugValue(areAnyRunning ? 'Running' : 'Idle'); const run = (...args: ParameterType<TaskRunner>) => { dispatch(startTask(task, ...args)); }; return [areAnyRunning, run]; }