universal-common
Version:
Library that provides useful missing base class library functionality.
195 lines (180 loc) • 5.93 kB
JavaScript
import InvalidOperationError from "./InvalidOperationError.js";
/**
* Represents a unit of work that can be executed asynchronously.
*
* The Task class provides a higher-level abstraction over JavaScript Promises,
* with additional features like explicit state tracking and manual execution control.
* This pattern is inspired by the Task pattern from languages like C#.
*
* @example
* // Create a task that will execute later
* const task = new Task(() => {
* return fetch('https://api.example.com/data');
* });
*
* // Later, start the task and handle its result
* task.start();
* task.then(response => response.json())
* .then(data => console.log(data));
*/
export default class Task {
/**
* Represents the state of a task that has not yet completed.
* @readonly
* @type {string}
*/
static get STATE_PENDING() { return "Pending"; }
/**
* Represents the state of a task that has completed successfully.
* @readonly
* @type {string}
*/
static get STATE_FULFILLED() { return "Fulfilled"; }
/**
* Represents the state of a task that has completed with an error.
* @readonly
* @type {string}
*/
static get STATE_REJECTED() { return "Rejected"; }
/**
* The function to execute when the task starts.
* @private
* @type {Function}
*/
#action;
/**
* The current state of the task.
* @private
* @type {string}
*/
#state;
/**
* Creates a new Task instance.
*
* @param {Function} action - The function to execute when the task starts.
* This can return a value or a Promise.
* @throws {TypeError} If action is not a function
*/
constructor(action) {
if (typeof action !== 'function') {
throw new TypeError('Task action must be a function');
}
this.#action = action;
this.#state = Task.STATE_PENDING;
}
/**
* Gets the current state of the task.
*
* @returns {string} The current state (Pending, Fulfilled, or Rejected)
*/
get state() {
return this.#state;
}
/**
* Registers a callback to be executed when the task completes successfully.
* This method also implicitly starts the task if it hasn't been started yet.
*
* @param {Function} action - The callback function to execute with the task's result
* @returns {Promise} A Promise that resolves with the result of the callback
*
* @example
* const task = new Task(() => 42);
* task.then(result => console.log(result)); // Outputs: 42
*/
then(action) {
if (this.promise === undefined) {
this.#execute();
}
return this.promise.then(action);
}
/**
* Explicitly starts the task execution.
*
* @returns {void}
* @throws {InvalidOperationError} If the task has already been started
*
* @example
* const task = new Task(() => {
* console.log("Task is running");
* });
* task.start(); // Task begins execution
*/
start() {
if (this.promise === undefined) {
this.#execute();
}
else {
throw new InvalidOperationError("Task has already been started.");
}
}
/**
* Waits synchronously for the task to complete.
*
* @returns {void}
*
* @warning This method uses a busy-wait approach which can block the JavaScript thread.
* It should only be used in environments where blocking is acceptable (e.g., Node.js
* with worker threads). Do not use this in browser environments as it will freeze the UI.
*
* @example
* // In a non-browser environment:
* const task = new Task(() => {
* return someExpensiveOperation();
* });
* task.start();
* task.wait(); // Blocks until the task completes
* console.log("Task is done!");
*/
wait() {
if (this.promise === undefined) {
this.start();
}
// Warning: This is a busy-wait loop that will block the JavaScript thread
while (this.#state === Task.STATE_PENDING) {
// Empty busy-wait loop
}
}
/**
* Creates a Promise that resolves after the specified delay.
*
* @param {number} ms - The delay in milliseconds
* @returns {Promise<void>} A Promise that resolves after the delay
*
* @example
* // Wait for 2 seconds
* await Task.delay(2000);
* console.log("2 seconds have passed");
*/
static delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Executes the task's action and tracks its state.
* @private
*/
#execute() {
this.promise = new Promise((resolve, reject) => {
try {
const result = this.#action();
// If the action returns a Promise, handle it properly
if (result instanceof Promise) {
result.then(value => {
this.#state = Task.STATE_FULFILLED;
resolve(value);
}).catch(error => {
this.#state = Task.STATE_REJECTED;
reject(error);
});
} else {
// For synchronous results
this.#state = Task.STATE_FULFILLED;
resolve(result);
}
}
catch(e) {
this.#state = Task.STATE_REJECTED;
reject(e);
}
});
}
}