@hjvedvik/tasks
Version:
Terminal task list
182 lines (147 loc) • 4.63 kB
JavaScript
const pMap = require('p-map')
const hirestime = require('hirestime')
const { taskConcurrency, logAndSleep } = require('./utils')
const TTYRenderer = require('./renderers/TTYRenderer')
const NonTTYRenderer = require('./renderers/NonTTYRenderer')
const Task = require('./Task')
class Tasks {
constructor (tasks, options = {}) {
this.isTTY = process.stdout.isTTY
this.parent = this
this.tasks = tasks.map(task => {
return new Task(task, this)
})
this.options = Object.assign({
concurrent: false,
showSummary: false,
showSubtasks: true,
collapseSubtasks: true,
fps: 12,
// theme options
dateFormat: 'HH:mm:ss',
successColor: 'green',
progressColor: 'green',
errorColor: 'red'
}, options)
}
/**
* Add a new task.
*
* @param {Object} task
*/
add (task) {
this.tasks.push(task)
}
/**
* Run tasks.
*
* @param {Object} context User defined context that will be
* passed down to all tasks.
* @return {Promise}
*/
async run (context = {}) {
const concurrency = taskConcurrency(this.options.concurrent)
const Renderer = this.isTTY ? TTYRenderer : NonTTYRenderer
this.renderer = new Renderer(this)
const promises = pMap(this.tasks, async task => {
const time = hirestime()
const skip = await task.skip(context)
if (!skip) {
task.isCurrent = true
const result = await task.run(context)
if (result instanceof Tasks) {
task.subtasks = result
task.subtasks.parent = this.parent
} else {
task.totalTime = time(hirestime.S)
// re-render and wait some milliseconds if task rendered
// a progress bar to let user see that the bar reached 100%
if (task.progress) await logAndSleep(this)
}
task.isCurrent = false
} else {
task.isSkipped = true
// set summary if skip method returned a string
if (typeof skip === 'string') {
task.summary = skip
}
}
// the task returned a new set of tasks
if (task.subtasks) {
await task.subtasks.run(context)
// end parent timer after all subtasks
task.totalTime = time(hirestime.S)
// also re-render and wait some milliseconds after subtasks
// to let user see last that the last task succeeds
await logAndSleep(this)
// remove all subtasks when collapsing, except tasks with errors
if (this.options.collapseSubtasks) {
task.subtasks.tasks = task.subtasks.tasks.filter(task => {
return !!task.errors.length
})
}
}
task.cache = null
task.isPending = false
task.isDone = true
}, { concurrency })
return promises.then(async () => {
if (this.parent === this) {
await logAndSleep(this)
this.renderer.done()
}
return context
})
}
/**
* Render the output for all tasks in this set. Output from inactive
* tasks will be cached until they are active.
*
* @return {String}
*/
render (level = 0) {
let output = ''
for (const task of this.tasks) {
if (task.isCurrent || !this.isTTY || !task.cache) {
task.cache = this.renderer.render(task, { level })
}
output += task.cache
if (task.subtasks && this.options.showSubtasks) {
output += task.subtasks.render(level + 1)
}
}
return output
}
/**
* Log output.
*
* @param {Boolean} force Force a render.
* @param {Number} skip Amount of renders to skip before actually
* rendering new output. Useful for tasks
* which will trigger lots of renders which
* again will reduce overall performance.
*/
log (force = !this.isTTY, skip = 0) {
const render = parent => {
parent._skippedRenders = 0
parent._lastRender = process.hrtime()
const result = parent.render().trim()
if (result) {
parent.renderer.log(result)
}
}
const { parent } = this
// forcing a re-render
if (force) return render(parent)
// wants to skip certain re-renders
if (skip && skip > parent._skippedRenders) {
return parent._skippedRenders++
}
// render maximum 60 frames per second
const lastRender = process.hrtime(parent._lastRender)
if ((lastRender[1] / 1000000) > 1000 / parent.options.fps) {
return render(parent)
}
}
}
module.exports = Tasks