Skip to content

Instantly share code, notes, and snippets.

@tim-evans
Last active June 15, 2019 01:05
Show Gist options
  • Select an option

  • Save tim-evans/d6b79d62c01793c3f119f67e2f6f4268 to your computer and use it in GitHub Desktop.

Select an option

Save tim-evans/d6b79d62c01793c3f119f67e2f6f4268 to your computer and use it in GitHub Desktop.

Revisions

  1. tim-evans revised this gist Jun 11, 2019. 1 changed file with 11 additions and 38 deletions.
    49 changes: 11 additions & 38 deletions useTask.ts
    Original 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 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 startTask = (task: TaskRunner, ...params: ParameterType<TaskRunner>) => {
    @@ -62,8 +56,8 @@ interface Task {
    params: ParameterType<TaskRunner>;
    status: TaskStatus;
    iterator?: ReturnType<TaskRunner>;
    promise?: Promise<PromiseType<IteratorType<ReturnType<TaskRunner>>>>;
    value?: PromiseType<IteratorType<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(task => {
    return task.status === 'ready' ||
    task.status === 'working' ||
    task.status === 'working*' ||
    task.status === 'finishing' ||
    task.status === 'finishing*';
    });
    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;
    };

    // 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;
    }
    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(task => {
    return task.status === 'ready' ||
    task.status === 'working*' ||
    task.status === 'working' ||
    task.status === 'finishing' ||
    task.status === 'finishing*';
    });

    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: getStatus(lastTask.status),
    status: isRunning(lastTask) ? 'running' : lastTask.status,
    value: lastTask.value,
    error: lastTask.error,
    cancel() {
    dispatch(cancelTasks([lastTask]));
    }
    } as TaskAPI : null;
    console.log(last);

    return [{
    isRunning: areAnyRunning,
  2. tim-evans revised this gist Jun 11, 2019. 1 changed file with 3 additions and 2 deletions.
    5 changes: 3 additions & 2 deletions useTask.ts
    Original file line number Diff line number Diff line change
    @@ -309,7 +309,7 @@ export function useTask<T extends TaskRunner>(
    ): [
    {
    isRunning: boolean;
    last: TaskAPI;
    last: TaskAPI | null;
    },
    (...args: ParameterType<T>) => void
    ] {
    @@ -370,7 +370,8 @@ export function useTask<T extends TaskRunner>(
    cancel() {
    dispatch(cancelTasks([lastTask]));
    }
    } : null;
    } as TaskAPI : null;
    console.log(last);

    return [{
    isRunning: areAnyRunning,
  3. tim-evans revised this gist Jun 11, 2019. 1 changed file with 136 additions and 24 deletions.
    160 changes: 136 additions & 24 deletions useTask.ts
    Original 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 cancelTasks |
    typeof trackWork
    >;

    type TaskStatus = 'queued' | 'ready' | 'dropped' | 'working' | 'finishing' | 'cancelled' | 'finished';
    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': {
    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]) {
    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: [...tasks, {
    tasks: compact([...tasks, {
    run: action.task,
    params: action.params,
    status: 'dropped' as TaskStatus
    }]
    status: 'ready' as TaskStatus
    }])
    };
    }
    case 'drop': {
    @@ -132,7 +161,10 @@ const reducer: Reducer<State, Actions> = (state, action) => {
    };
    }
    case 'START_WORK': {
    let tasks = state.tasks.map(task => {
    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 = state.tasks.map(task => {
    if (task === action.task) {
    // The only valid state is from working
    if (task.status === 'working' && task.iterator) {
    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') {
    } else if (task.status === 'finishing*') {
    return {
    ...task,
    value: task.value
    }
    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 = state.tasks.map(task => {
    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') {
    } 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;
    }
    ): [
    boolean,
    {
    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 === 'working';
    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));
    };

    return [areAnyRunning, run];
    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];
    }
  4. tim-evans revised this gist Jun 10, 2019. 1 changed file with 80 additions and 51 deletions.
    131 changes: 80 additions & 51 deletions useTask.ts
    Original 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 cancelTask = (task: TaskRunner) => {
    return {
    action: 'CANCEL_TASK',
    task
    } as const;
    };

    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 cancelTask
    typeof cancelTasks
    >;

    type TaskStatus = 'queued' | 'ready' | 'dropped' | 'working' | 'finishing' | 'cancelled' | 'finished';
    interface Task {
    run: TaskRunner;
    params: ParameterType<TaskRunner>;
    status: 'queued' | 'ready' | 'dropped' | 'working' | 'finishing' | 'cancelled' | 'finished';
    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 =>
    task.status === 'working' ||
    task.status === 'finishing' ||
    task.status === 'ready'
    );
    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 firstRunningTask = runningTasks[0];
    firstRunningTask.status = 'cancelled';
    break;
    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': {
    state.tasks.push({
    run: action.task,
    params: action.params,
    status: 'dropped'
    });
    return state;
    return {
    strategy: state.strategy,
    maxConcurrency: state.maxConcurrency,
    tasks: [...state.tasks, {
    run: action.task,
    params: action.params,
    status: 'dropped' as TaskStatus
    }]
    };
    }
    case 'queue': {
    state.tasks.push({
    run: action.task,
    params: action.params,
    status: 'queued'
    });
    return state;
    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
    state.tasks.push({
    run: action.task,
    params: action.params,
    status: 'ready'
    });
    return state;
    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',
    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',
    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
    }
    }
    } else {
    return task;
    }
    return task;
    });

    return {
    @@ -158,19 +181,25 @@ const reducer: Reducer<State, Actions> = (state, action) => {
    tasks
    };
    }
    case 'CANCEL_TASK': {
    case 'CANCEL_TASKS': {
    let tasks = state.tasks.map(task => {
    if (task.run === action.task) {
    if (task.iterator && task.iterator.return) {
    task.iterator.return();
    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,
    status: 'cancelled'
    };
    } else {
    return task;
    }
    return task;
    });

    return {
    @@ -191,7 +220,7 @@ export function useTask<T extends TaskRunner>(
    }
    ): [
    boolean,
    (...args: ParameterType<T>) => void;
    (...args: ParameterType<T>) => void
    ] {
    let maxConcurrency = Infinity;
    if (options) {
  5. tim-evans revised this gist Jun 10, 2019. 1 changed file with 95 additions and 34 deletions.
    129 changes: 95 additions & 34 deletions useTask.ts
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    import { Reducer, useCallback, useReducer, useDebugValue } from 'react';
    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 extends Promise<any>
    > = T extends Promise<infer R> ? R : undefined;

    type TaskRunner = (...args: any[]) => IterableIterator<Promise<any>>;
    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 doWork = () => {
    const startWork = () => {
    return {
    action: 'START_WORK',
    } as const;
    }

    const continueWork = (task: Task, value: any) => {
    return {
    action: 'DO_WORK',
    action: 'CONTINUE_WORK',
    task,
    value
    } as const;
    }

    type Actions = ReturnType<
    typeof startTask |
    typeof doWork |
    typeof startWork |
    typeof continueWork |
    typeof cancelTask
    >;

    interface Task {
    run: TaskRunner;
    params: ParameterType<TaskRunner>;
    status: 'queued' | 'ready' | 'dropped' | 'working' | 'finishing' | 'cancelled' | 'finished';
    iterator?: ReturnType<TaskRunner>;
    status: 'queued' | 'ready' | 'dropped' | 'working' | 'idle' | 'cancelled' | 'finished';
    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 === 'idle' ||
    task.status === 'finishing' ||
    task.status === 'ready'
    );

    @@ -99,20 +107,17 @@ const reducer: Reducer<State, Actions> = (state, action) => {
    });
    return state;
    }
    case 'DO_WORK': {
    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: 'idle',
    iterator: task.run(...rest.params)
    };
    } else if (task.status === 'idle' && iterator) {
    return {
    ...rest,
    status: 'working',
    promise: iterator.next(value)
    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(
    task: TaskRunner,
    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: []
    });

    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 => {
    state.tasks.filter(task => ['working', 'finishing'].indexOf(task.status) !== -1).forEach(task => {
    if (task.promise) {
    task.promise.then(() => dispatch(doWork()));
    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 (state.tasks.some(task => task.status === 'ready' || task.status === 'idle')) {
    dispatch(doWork());
    if (areAnyReady || (areAnyQueued && runningTasks.length < maxConcurrency)) {
    dispatch(startWork());
    }

    useDebugValue(areAnyRunning ? 'Running' : 'Idle');

    const run = (...args: ParameterType<TaskRunner>) => {
    const run = (...args: ParameterType<typeof task>) => {
    dispatch(startTask(task, ...args));
    };

  6. tim-evans created this gist Jun 10, 2019.
    177 changes: 177 additions & 0 deletions useTask.ts
    Original 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];
    }