diff --git a/@vates/task/index.js b/@vates/task/index.js index 80d17d2c24..660bcd1e24 100644 --- a/@vates/task/index.js +++ b/@vates/task/index.js @@ -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) + } + } +} diff --git a/@vates/task/index.test.js b/@vates/task/index.test.js index 40714ffbe3..aa1122887d 100644 --- a/@vates/task/index.test.js +++ b/@vates/task/index.test.js @@ -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() diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 4e930026b5..36444e72df 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -34,6 +34,7 @@ - @vates/fuse-vhd patch +- @vates/task minor - @xen-orchestra/proxy-cli patch - @xen-orchestra/vmware-explorer patch - @xen-orchestra/web patch