@byloth/core
Version:
An unopinionated collection of useful functions and classes that I use widely in all my projects. 🔧
344 lines (323 loc) • 11.2 kB
text/typescript
import type { Callback } from "../types.js";
import type { FulfilledHandler, PromiseExecutor, RejectedHandler } from "./types.js";
/**
* A wrapper class representing an enhanced version of the native {@link Promise} object.
*
* It provides additional properties to check the state of the promise itself.
* The state can be either `pending`, `fulfilled` or `rejected` and is accessible through
* the {@link SmartPromise.isPending}, {@link SmartPromise.isFulfilled} & {@link SmartPromise.isRejected} properties.
*
* ---
*
* @example
* ```ts
* const promise = new SmartPromise<string>((resolve, reject) =>
* {
* setTimeout(() => resolve("Hello, World!"), 1_000);
* });
*
* console.log(promise.isPending); // true
* console.log(promise.isFulfilled); // false
*
* console.log(await promise); // "Hello, World!"
*
* console.log(promise.isPending); // false
* console.log(promise.isFulfilled); // true
* ```
*
* ---
*
* @template T The type of value the promise will eventually resolve to. Default is `void`.
*/
export default class SmartPromise<T = void> implements Promise<T>
{
/**
* Wraps a new {@link SmartPromise} object around an existing native {@link Promise} object.
*
* ---
*
* @example
* ```ts
* const request = fetch("https://api.example.com/data");
* const smartRequest = SmartPromise.FromPromise(request);
*
* console.log(request.isPending); // Throws an error: `isPending` is not a property of `Promise`.
* console.log(smartRequest.isPending); // true
*
* const response = await request;
* console.log(smartRequest.isFulfilled); // true
* ```
*
* ---
*
* @param promise The promise to wrap.
*
* @returns A new {@link SmartPromise} object that wraps the provided promise.
*/
public static FromPromise<T>(promise: Promise<T>): SmartPromise<T>
{
return new SmartPromise((resolve, reject) => promise.then(resolve, reject));
}
/**
* A flag indicating whether the promise is still pending or not.
*
* The protected property is the only one that can be modified directly by the derived classes.
* If you're looking for the public and readonly property, use the {@link SmartPromise.isPending} getter instead.
*/
protected _isPending: boolean;
/**
* A flag indicating whether the promise is still pending or not.
*/
public get isPending(): boolean
{
return this._isPending;
}
/**
* A flag indicating whether the promise has been fulfilled or not.
*
* The protected property is the only one that can be modified directly by the derived classes.
* If you're looking for the public and readonly property, use the {@link SmartPromise.isFulfilled} getter instead.
*/
protected _isFulfilled: boolean;
/**
* A flag indicating whether the promise has been fulfilled or not.
*/
public get isFulfilled(): boolean
{
return this._isFulfilled;
}
/**
* A flag indicating whether the promise has been rejected or not.
*
* The protected property is the only one that can be modified directly by the derived classes.
* If you're looking for the public and readonly property, use the {@link SmartPromise.isRejected} getter instead.
*/
protected _isRejected: boolean;
/**
* A flag indicating whether the promise has been rejected or not.
*/
public get isRejected(): boolean
{
return this._isRejected;
}
/**
* The native {@link Promise} object wrapped by this instance.
*/
protected readonly _promise: Promise<T>;
/**
* Initializes a new instance of the {@link SmartPromise} class.
*
* ---
*
* @example
* ```ts
* const promise = new SmartPromise<string>((resolve, reject) =>
* {
* setTimeout(() => resolve("Hello, World!"), 1_000);
* });
* ```
*
* ---
*
* @param executor
* The function responsible for eventually resolving or rejecting the promise.
* Similarly to the native {@link Promise} object, it's immediately executed after the promise is created.
*/
public constructor(executor: PromiseExecutor<T>)
{
this._isPending = true;
this._isFulfilled = false;
this._isRejected = false;
const _onFulfilled = (result: T): T =>
{
this._isPending = false;
this._isFulfilled = true;
return result;
};
const _onRejected = (reason: unknown): never =>
{
this._isPending = false;
this._isRejected = true;
throw reason;
};
this._promise = new Promise<T>(executor)
.then(_onFulfilled, _onRejected);
}
/**
* Creates a new {@link Promise} identical to the one wrapped by this instance, with a different reference.
*
* ---
*
* @example
* ```ts
* const promise = new SmartPromise<string>((resolve, reject) =>
* {
* setTimeout(() => resolve("Hello, World!"), 1_000);
* });
*
* console.log(await promise.then()); // "Hello, World!"
* ```
*
* ---
*
* @returns A new {@link Promise} identical to the original one.
*/
public then(onFulfilled?: null): Promise<T>;
/**
* Attaches a callback that executes right after the promise is fulfilled.
*
* The previous result of the promise is passed as the argument to the callback.
* The callback's return value is considered the new promise's result instead.
*
* ---
*
* @example
* ```ts
* const promise = new SmartPromise<string>((resolve, reject) =>
* {
* setTimeout(() => resolve("Hello, World!"), 1_000);
* });
*
* promise.then((result) => console.log(result)); // "Hello, World!"
* ```
*
* ---
*
* @template F The type of value the new promise will eventually resolve to. Default is `T`.
*
* @param onFulfilled The callback to execute once the promise is fulfilled.
*
* @returns A new {@link Promise} resolved with the return value of the callback.
*/
public then<F = T>(onFulfilled: FulfilledHandler<T, F>, onRejected?: null): Promise<F>;
/**
* Attaches callbacks that executes right after the promise is fulfilled or rejected.
*
* The previous result of the promise is passed as the argument to the fulfillment callback.
* The fulfillment callback's return value is considered the new promise's result instead.
*
* If an error is thrown during execution, the rejection callback is then executed instead.
*
* Also note that:
* - If the rejection callback runs properly, the error is considered handled.
* The rejection callback's return value is considered the new promise's result.
* - If the rejection callback throws an error, the new promise is rejected with that error.
*
* ---
*
* @example
* ```ts
* const promise = new SmartPromise((resolve, reject) =>
* {
* setTimeout(resolve, Math.random() * 1_000);
* setTimeout(reject, Math.random() * 1_000);
* });
*
* promise.then(() => console.log("OK!"), () => console.log("KO!")); // "OK!" or "KO!"
* ```
*
* ---
*
* @template F The type of value the new promise will eventually resolve to. Default is `T`.
* @template R The type of value the new promise will eventually resolve to. Default is `never`.
*
* @param onFulfilled The callback to execute once the promise is fulfilled.
* @param onRejected The callback to execute once the promise is rejected.
*
* @returns A new {@link Promise} resolved or rejected based on the callbacks.
*/
public then<F = T, R = never>(
onFulfilled: FulfilledHandler<T, F>, onRejected: RejectedHandler<unknown, R>
): Promise<F | R>;
public then<F = T, R = never>(
onFulfilled?: FulfilledHandler<T, F> | null, onRejected?: RejectedHandler<unknown, R> | null
): Promise<F | R>
{
return this._promise.then(onFulfilled, onRejected);
}
/**
* Creates a new {@link Promise} identical to the one wrapped by this instance, with a different reference.
*
* ---
*
* @example
* ```ts
* const promise = new SmartPromise((resolve, reject) =>
* {
* setTimeout(() => reject(new Exception("An unknown error occurred.")), 1_000);
* });
*
* promise.catch(); // "Uncaught Exception: An unknown error occurred."
* ```
*
* ---
*
* @returns A new {@link Promise} identical to the original one.
*/
public catch(onRejected?: null): Promise<T>;
/**
* Attaches a callback to handle the potential rejection of the promise.
* If it happens, the callback is then executed.
*
* Also note that:
* - If the callback runs properly, the error is considered handled.
* The callback's return value is considered the new promise's result.
* - If the callback throws an error, the new promise is rejected with that error.
*
* ---
*
* @example
* ```ts
* const promise = new SmartPromise((resolve, reject) =>
* {
* setTimeout(() => reject(new Exception("An unknown error occurred.")), 1_000);
* });
*
* promise.catch((reason) => console.error(reason)); // "Uncaught Exception: An unknown error occurred."
* ```
*
* ---
*
* @template R The type of value the new promise will eventually resolve to. Default is `T`.
*
* @param onRejected The callback to execute once the promise is rejected.
*
* @returns A new {@link Promise} able to catch and handle the potential error.
*/
public catch<R = never>(onRejected: RejectedHandler<unknown, R>): Promise<T | R>;
public catch<R = never>(onRejected?: RejectedHandler<unknown, R> | null): Promise<T | R>
{
return this._promise.catch(onRejected);
}
/**
* Attaches a callback that executes right after the promise is settled, regardless of the outcome.
*
* ---
*
* @example
* ```ts
* const promise = new SmartPromise((resolve, reject) =>
* {
* setTimeout(resolve, Math.random() * 1_000);
* setTimeout(reject, Math.random() * 1_000);
* });
*
*
* promise
* .then(() => console.log("OK!")) // Logs "OK!" if the promise is fulfilled.
* .catch(() => console.log("KO!")) // Logs "KO!" if the promise is rejected.
* .finally(() => console.log("Done!")); // Always logs "Done!".
* ```
*
* ---
*
* @param onFinally The callback to execute when once promise is settled.
*
* @returns A new {@link Promise} that executes the callback once the promise is settled.
*/
public finally(onFinally?: Callback | null): Promise<T>
{
return this._promise.finally(onFinally);
}
public readonly [Symbol.toStringTag]: string = "SmartPromise";
}