UNPKG

universal-common

Version:

Library that provides useful missing base class library functionality.

195 lines (180 loc) 5.93 kB
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); } }); } }