feat(task): info, set and warning are now instance methods

Info, properties and warnings should be set on a specific/known task instead of relying on the async context.

For instance, this permits updating the progress of a task from the code on one of its subtasks.
This commit is contained in:
Julien Fontanet
2024-05-13 11:01:58 +02:00
parent 3dde723e31
commit 32448cfa1b
3 changed files with 97 additions and 32 deletions

View File

@@ -3,6 +3,10 @@
const assert = require('node:assert').strict
const { AsyncLocalStorage } = require('node:async_hooks')
function alreadyEnded() {
throw new Error('task has already ended')
}
// define a read-only, non-enumerable, non-configurable property
function define(object, property, value) {
Object.defineProperty(object, property, { value })
@@ -21,7 +25,7 @@ const asyncStorage = global[asyncStorageKey] ?? (global[asyncStorageKey] = new A
const getTask = () => asyncStorage.getStore()
exports.Task = class Task {
class Task {
static get abortSignal() {
const task = getTask()
if (task !== undefined) {
@@ -29,13 +33,6 @@ exports.Task = class Task {
}
}
static info(message, data) {
const task = getTask()
if (task !== undefined) {
task.#emit('info', { data, message })
}
}
static run(...args) {
let opts = args[0]
let fn
@@ -57,20 +54,6 @@ exports.Task = class Task {
return new Task(opts).run(() => fn.apply(thisArg, args))
}
static set(name, value) {
const task = getTask()
if (task !== undefined) {
task.#emit('property', { name, value })
}
}
static warning(message, data) {
const task = getTask()
if (task !== undefined) {
task.#emit('warning', { data, message })
}
}
static wrap(opts, fn) {
// compatibility with @decorateWith
if (typeof fn !== 'function') {
@@ -127,9 +110,7 @@ exports.Task = class Task {
this.#emit('abortionRequested', { reason: signal.reason })
if (!this.#running) {
const status = FAILURE
this.#status = status
this.#emit('end', { result: signal.reason, status })
this.#end(FAILURE, signal.reason)
}
}
})
@@ -140,12 +121,26 @@ exports.Task = class Task {
}
#emit(type, data) {
assert.equal(this.#startData, undefined, 'task has not started yet')
data.id = this.id
data.timestamp = Date.now()
data.type = type
this.#onProgress(data)
}
#end(status, result) {
assert.equal(this.#status, PENDING)
this.#status = status
this.#emit('end', { status, result })
this.#onProgress = alreadyEnded
}
info(message, data) {
this.#emit('info', { data, message })
}
#maybeStart() {
const startData = this.#startData
if (startData !== undefined) {
@@ -157,8 +152,7 @@ exports.Task = class Task {
async run(fn) {
const result = await this.runInside(fn)
if (this.status === PENDING) {
this.#status = SUCCESS
this.#emit('end', { status: SUCCESS, result })
this.#end(SUCCESS, result)
}
return result
}
@@ -172,18 +166,27 @@ exports.Task = class Task {
this.#maybeStart()
try {
const result = await asyncStorage.run(this, fn)
const result = await asyncStorage.run(this, fn, this)
this.#running = false
return result
} catch (result) {
const status = FAILURE
this.#status = status
this.#emit('end', { status, result })
this.#end(FAILURE, result)
throw result
}
}
set(name, value) {
assert.equal(this.status, PENDING)
this.#emit('property', { name, value })
}
warning(message, data) {
assert.equal(this.status, PENDING)
this.#emit('warning', { data, message })
}
wrap(fn) {
const task = this
return function taskRun() {
@@ -198,3 +201,15 @@ exports.Task = class Task {
}
}
}
exports.Task = Task
// setup static aliases
for (const name of ['info', 'set', 'warning']) {
const method = Task.prototype[name]
Task[name] = function () {
const task = getTask()
if (task !== undefined) {
return method.apply(task, arguments)
}
}
}

View File

@@ -219,6 +219,31 @@ describe('Task', function () {
})
})
describe('#set()', function () {
const name = 'progress'
const value = 10
it('throws when the task is not started', function () {
const task = createTask()
assert.throws(() => task.set(name, value), { message: 'task has not started yet' })
})
it(`emits an property message`, async function () {
const task = createTask()
await task.run(async () => {
await Task.run(() => {
task.set(name, value)
assertEvent(task, {
name,
type: 'property',
value,
})
})
})
})
})
describe('.warning()', function () {
it('does nothing when run outside a task', function () {
Task.warning('foo')
@@ -237,6 +262,30 @@ describe('Task', function () {
})
})
for (const type of ['info', 'warning']) {
describe(`#${type}()`, function () {
it('throws when the task is not started', function () {
const task = createTask()
assert.throws(() => task[type]('foo'), { message: 'task has not started yet' })
})
it(`emits an ${type} message`, async function () {
const task = createTask()
await task.run(async () => {
await Task.run(() => {
task[type]('foo')
assertEvent(task, {
data: undefined,
message: 'foo',
type,
})
})
})
})
})
}
describe('#id', function () {
it('can be set', function () {
const task = createTask()

View File

@@ -34,6 +34,7 @@
<!--packages-start-->
- @vates/fuse-vhd patch
- @vates/task minor
- @xen-orchestra/proxy-cli patch
- @xen-orchestra/vmware-explorer patch
- @xen-orchestra/web patch