code: function forkQueue(name, mainTask, cb) {
    let tasks = [],
      result,
      completed = false
    addTask(mainTask)
  
    function abort(err) {
      cancelAll()
      cb(err, true)
    }
  
    function addTask(task) {
      tasks.push(task)
      task.cont = (res, isErr) => {
        if (completed) {
          return
        }
  
        remove(tasks, task)
        task.cont = noop
        if (isErr) {
          abort(res)
        } else {
          if (task === mainTask) {
            result = res
          }
          if (!tasks.length) {
            completed = true
            cb(result)
          }
        }
      }
      // task.cont.cancel = task.cancel
    }
  
    function cancelAll() {
      if (completed) {
        return
      }
      completed = true
      tasks.forEach(t => {
        t.cont = noop
        t.cancel()
      })
      tasks = []
    }
  
    return {
      addTask,
      cancelAll,
      abort,
      getTasks: () => tasks,
      taskNames: () => tasks.map(t => t.name),
    }
  }
docs: Used to track a parent task and its forks
  In the new fork model, forked tasks are attached by default to their parent
  We model this using the concept of Parent task && main Task
  main task is the main flow of the current Generator, the parent tasks is the
  aggregation of the main tasks + all its forked tasks.
  Thus the whole model represents an execution tree with multiple branches (vs the
  linear execution tree in sequential (non parallel) programming)
  
  A parent tasks has the following semantics
  - It completes if all its forks either complete or all cancelled
  - If it's cancelled, all forks are cancelled as well
  - It aborts if any uncaught error bubbles up from forks
  - If it completes, the return value is the one returned by the main task