Source: util/promise.ts

/**
 * @author Michael Hasenstein <hasenstein@yahoo.com>
 * @copyright REFINIO GmbH 2017
 * @license CC-BY-NC-SA-2.5; portions MIT License
 * @version 0.0.1
 */

/**
 * @module
 */

/**
 * Type describing the resolve() callback function received by the `new Promise()` creation
 * callback function.
 * Copied from TypeScript's lib.es2015.promise.d.ts
 * @typedef {Function} PromiseResolveCb
 * @param {Promise<T>|T} [value] - The value the promise will be resolved with
 * @returns {undefined} Returns `undefined`
 */
export type PromiseResolveCb<T> = (value?: T | PromiseLike<T>) => void;

/**
 * Type describing the reject() callback function received by the new Promise() creation
 * callback function.
 * Copied from TypeScript's lib.es2015.promise.d.ts
 * @typedef {Function} PromiseRejectCb
 * @param {Error} reason - The error the promise is rejected with
 * @returns {undefined} Returns `undefined`
 */
export type PromiseRejectCb = (reason?: any) => void;

/**
 * See {@link util/promise.module:ts.createTrackingPromise|util/promise.createTrackingPromise}
 * @global
 * @typedef {object} TrackingPromiseObj
 * @property {Promise<T>} promise - The tracking promise
 * @property {PromiseResolveCb<T>} resolve - The method that resolves the tracking promise
 * @property {PromiseRejectCb} reject - The method that rejects the tracking promise
 */
export interface TrackingPromiseObj<T> {
    promise: Promise<T>;
    resolve: PromiseResolveCb<T>;
    reject: PromiseRejectCb;
}

/**
 * Return value of promises passed through
 * [`Promise.allSettled`](https://github.com/tc39/proposal-promise-allSettled)
 * The `status` is 'fulfilled' and there is `value` with the value the promise resolved with, or
 * the `status` is 'rejected' and there is a `reason` with the value of the promise's rejection,
 * usually an `Error` object.
 * @global
 * @typedef {object} AllSettled
 * @property {*} [value] - The value of the individual settled promise if it was fulfilled
 * @property {Error} [reason] - Should be an `Error` object as a *best practice*, but not mandatory
 * @property {('fulfilled'|'rejected')} status
 */
export type AllSettled<T> = {value: T; status: 'fulfilled'} | {reason: Error; status: 'rejected'};

/**
 * The possible string constant values of {@link AllSettled|`Promise.allSettled` results}.
 * It is made available as export from
 * {@link util/promise.module:ts|`util/promise`}
 * to avoid inline repetition of these constants.
 * @global
 * @typedef {object} AllSettledStatus
 * @property {'fulfilled'} FULFILLED
 * @property {'rejected'} REJECTED
 */
export const AllSettledStatus = {
    FULFILLED: 'fulfilled',
    REJECTED: 'rejected'
} as const;

import {createError} from '../errors';
import {createMessageBus} from '../message-bus';
import {isFunction, isObject} from './type-checks-basic';

const MessageBus = createMessageBus('util/promise');

/**
 * This function returns a promise that resolves after `delay` milliseconds. It resolves with
 * the `passThroughArg`, if one is supplied, or undefined if none is supplied. Example use
 * case: In an IMAP message retrieval application we use this function to create a delay between
 * retries after disconnects from the IMAP server to avoid triggering an alert on the server for
 * too many and too frequent connections.
 * @static
 * @async
 * @param {number} [delay=0] - Delay in milliseconds before the returned promise resolves. If this
 * parameter is omitted, a value of 0 is used, meaning execute "immediately", or more
 * accurately, as soon as possible. Note that in either case, the actual delay may be longer
 * than intended (it uses `setTimeout` which does not make any guarantees).
 * @param {T} [passThroughArg] - Pass-through argument to resolve with, can be used to insert
 * "wait()" in a chain to pass through a value to the next promise function.
 * @returns {Promise<T>}
 */
export function wait<T>(delay: number = 0, passThroughArg?: T): Promise<T> {
    return new Promise(resolve => {
        setTimeout(resolve, delay, passThroughArg);
    });
}

/**
 * This function delivers a rejection if a given promise does not resolve or reject within the
 * given time. The advantage of using this function over using a Promise.race() of ones promise
 * against the promise-helper wait() function above is that it cancels the timer if the promise
 * resolves before the timer fires. While a lingering timer has no influence on the value of the
 * promise a program won't end as long as there are timers still running - so if you have a
 * long-running timeout your app will be running for at least that long, even if it is actually
 * finished a split second later. Canceling the timer prevents that problem with negligible
 * effort and no other side-effects.
 *
 * **IMPORTANT:** A timeout does not cancel the function that created the promise! It merely
 * returns the rejection from the timeout instead of waiting for the result of the original promise.
 * @static
 * @async
 * @param {number} [delay=Infinity] - Timeout in milliseconds. If the promise from the function
 * has not resolved by then we return a rejected promise. If this parameter is omitted the
 * promise is returned as-is because we assume "Infinity" as default delay value so that it is
 * useless to even start the timer.<br>
 * **Note #1:** A timeout of 0 is not allowed and will result in an error because that value is
 * unpredictable. Does it mean to fail right away? If it really does is not certain because it
 * depends on which task is in front of the JS runtime internal microtask- or in the event loop
 * queue. We don't know where the promise we received will end up, it may already be resolved, or
 * it may be scheduled later in the event queue.<br>
 * **Note #2:** The actual delay may be longer than intended (it uses `setTimeout` which does not
 * make any guarantees).
 * @param {Promise<T>} promise - The promise that the timeout (if one is given) is applied to.
 * @param {string} [txt='[${delay} ms]'] - Text to include in the Error message after a timeout
 * @returns {Promise<T>} Returns the promise resulting from `Promise.race`-ing the received promise
 * against (or with) a timeout promise. Whichever of the two is fulfilled first wins. If the
 * timeout promise wins the returned promise is rejected with an `Error` whose `name` is
 * "TimeoutError".
 */
export async function timeout<T>(
    delay: number = Infinity,
    promise: Promise<T>,
    txt: string = `[${delay} ms]`
): Promise<T> {
    if (delay === 0) {
        // If the given promise fails we would end up with an uncaught promise rejection since
        // this function is supposed to do the error-catching (Promise.race does that even if
        // the timeout wins). We ignore any promise error because 1) we don't have to the to
        // wait for it (this local error here is immediate, the promise may take an
        // indeterminate amount of time), and 2) the local error has precedence.
        promise.catch(ignore => undefined);
        throw createError('UPR-TO1');
    }

    if (delay === Infinity) {
        return await promise;
    }

    return await Promise.race([
        promise,
        wait(delay).then(() => Promise.reject(createError('UPR-TO', {name: 'TimeoutError', txt})))
    ]);
}

/*
 * CURRENTLY UNUSED
 *
 * Promises are missing an "any" function that resolves with the first success of the given
 *  promises, and fails if all promises fail.
 * Note: I tried "fancier" versions of Promise.any that can be found on the web. They had issues
 * and were less readable. I create an additional promise which is a bit of waste, but I prefer
 * that for clarity, time is no issue anyway since async code is used for slow I/O
 * operations.
 * @static
 * @async
 * @param {Promise[]} promises An array of promises only one of which needs to succeed for overall
 * success.
 * @returns {Promise<*>} Returns a promise that resolves with the result of the first resolving
 * promise, or rejects once all of them have rejected. In the rejected case the individual
 * rejections remain hidden, we instead return our own generic reject reason.
 */
// export function anyPromise (promises: Array<Promise<*>>): Promise<*> {
//     return new Promise((resolve, reject): void => {
//         // By counting _down_ I can easily compare with 0 instead of triggering a computation
//         // for the .length property. A micro-optimization that really does not matter because
//         // using a native Promise is slow compared to that, and the loop will likely always be a
//         // very short one.
//         let pending = promises.length;
//
//         promises.forEach((promise: Promise<*>): void => {
//             promise
//             // Key to how this works: Only the *first* call to resolve or reject matters, any
//             // subsequent calls have no effect. Also: keep in mind that this resolve() function
//             // is the one from the promise created in this function, the one the caller of
//             // anyPromise() is holding. So if *any* of the promises in array "promises" succeeds
//             // then the local promise's resolve() is called.
//             .then(resolve)
//             // On the other hand, reject() is basically disabled by the countDown,
//             // and can only be called if each one of the promises in the given array fails.
//             .catch((err: Error) => {
//                 pending -= 1;
//                 if (pending === 0) {
//                     // Create a new error message - because which one of the failed promises
//                     // would we use instead? Collect them? Probably useless effort, so instead
//                     // just create our own error message. Using this function shows errors are
//                     // expected, so knowing the message(s) is unlikely to have any benefit.
//                     // ERROR TEXT 'Cannot get fulfillment value from any promise'
//                     reject(createError('UP-ANY-P1'));
//                 }
//             });
//         });
//     });
// }

/**
 * Make sure the given functions are executed **sequentially**. It will work for any
 * function. The first function gets executed immediately, the rest is chained one by one and -
 * since these are promises - each of the functions executes in a different iteration of the
 * Javascript event loop even if the given functions are synchronous.
 * *Note:* If any promise in the chain is rejected (directly or by throwing an error)
 * *subsequent functions are not impacted and will still be run*! The serializer's function
 * merely is to ensure that none of the given functions are ever being executed at the same time.
 * @private
 * @param {...Function} functions - Functions that return promises or values, in the latter case
 * the values are wrapped in a promise.
 * @returns {Promise<T>} Returns the last promise added to the chain. Anything attached via
 * .then(..) to the returned promise is guaranteed to execute only after everything in the chain
 * has been executed.
 */
function serialize<T>(...functions: Array<(...args: any[]) => Promise<T>>): Promise<T> {
    // Get and EXECUTE the first function to make it the start of the chain. If it does not return
    // a promise the returned value will be turned into one below.
    const fn = functions.shift();

    if (fn === undefined) {
        throw createError('UP-SER1');
    }

    // Types: At this point Promise<T>, but below we convert rejections to values, and
    // rejections should always be Error (convention, the type is wrong if somebody
    let p: Promise<Error | T> = fn();

    // If the function did not produce a "thenable", convert its result to a promise. Checking for
    // "object" first solves the problem of the user function returning a falsy value.
    if (!isObject(p) || !isFunction(p.then)) {
        p = Promise.resolve(p);
    }

    // We will return the actual (promise-) value *before* possible rejections are stripped
    // below: Stripping rejections is essential when we get multiple functions, since we
    // declared the purpose of serialization merely is to ensure they are not executed at the
    // same time - the functions themselves are *not* considered a chain though! The
    // serialization often is for technical reasons, not logical ones. Therefore, we will not
    // stop the chain we produce for serialization when a member rejects!
    let lastPromise: Promise<any> = p;

    // We cannot let a rejected promise interrupt the entire chain: Serialization is meant to
    // ensure those operations happen in sequence and not in parallel, but the functions
    // (promises) may be otherwise completely unrelated. For example, they may all be using the
    // same resource (e.g. storage) but come from completely different "threads". So ignoring
    // rejected promises here has nothing to do with what goes on where those promises are
    // actually created and used, which is why we insert the catch() only here, internally. The
    // promises returned by the serializer remain uncaught!
    p = p.catch((err: Error) => err);

    while (functions.length > 0) {
        p = p.then(functions.shift());

        // See the comment above the same statement just above the while-loop for the same
        // statements.
        lastPromise = p;
        p = p.catch(err => err);
    }

    return lastPromise;
}

/**
 * Store a promise for a given type (string). All promises of the same type are chained to the
 * previous one.
 * @private
 * @type {Map<string, Promise<*>>}
 */
const promiseTypes: Map<string, Promise<any>> = new Map();

/**
 * Make sure the given functions are executed **sequentially**. The "type" argument means we
 * can add functions later, even much later and from a completely different location in the
 * code. The purpose may be to coordinate access to a shared external resource without having to
 * know which part of the code uses it.
 * We remember the chain we build for that given type string. If there is more than one function
 * in the argument list we serialize them in the specified order.
 * *Note:* If any promise in the chain is rejected (directly or by throwing an error)
 * *subsequent functions are not impacted and will still be run*! The serializer's function
 * merely is to ensure that none of the given functions are ever being executed at the same time.
 * @static
 * @async
 * @param {string} type - The given promise-producing functions are serialized together with any
 * other ones we may already have serialized previously under the given `type` string.
 * @param {...Function} functions - List of arguments of type `(...args) => Promise<T>` &mdash;
 * Functions that return promises or values, in the latter case the values are wrapped in a
 * promise to be serializable. That means that **synchronous functions will become asynchronous.**
 * @returns {Promise<T>} Returns the last promise added to the chain. Anything attached via
 * .then(..) to the returned promise is guaranteed to execute only after the chain is completed.
 */
export async function serializeWithType<T>(
    type: string,
    ...functions: ReadonlyArray<(...args: any[]) => Promise<T>>
): Promise<T> {
    if (functions.length === 0) {
        throw createError('UP-SERWT1');
    }

    let p = promiseTypes.get(type);

    if (p instanceof Promise) {
        // Add new elements to an existing serialization chain's last promise-producing element.
        // They may all have resolved already or not, the promises take care of that for us.
        // "serialize" expects only functions, so we have to wrap the last promise in one - it
        // will immediately be unwrapped by serialize(), which executes the first function, before
        // adding the new function(s) to the chain. This function-returning function fixes p in
        // its scope, so that the function it creates always returns the current value of p.
        p = serialize(
            (
                pp => () =>
                    pp
            )(p),
            ...functions
        );
    } else {
        // There are no previous elements in this chain.
        p = serialize(...functions);
    }

    // Remember the last promise in the chain
    promiseTypes.set(type, p);

    // Ensure that the Map does not grow endlessly: Each new "type" string creates a new entry.
    // If (ID or object) hashes are used over time more and more unused entries would
    // accumulate. To prevent that from happening, when the promise we just added is fulfilled
    // (one way or the other) we remove it - *if and only if* it still is the last entry for the
    // given type.
    // This creates a new dangling promise which would cause an "uncaught promise rejection" if
    // the original promise rejects, so we have to catch and ignore that case.
    p.finally(() => {
        if (promiseTypes.get(type) === p) {
            promiseTypes.delete(type);
        }
    }).catch(() => undefined);

    return await p.catch(err => {
        // ENABLE ASYNC. STACK TRACE (esp. V8 runtime)
        // In those runtimes that support zero-cost async stack traces with async/await, the
        // serializer interrupts the chain. Since it's really cheap and easy and with no cost
        // for the happy path we help this process along. This might end up being called
        // multiple times. The stack trace in each case will consist of the section of the call
        // chain that lies between the original call and the call to the serializer. Here we
        // assemble them into one stack trace.
        // Example for the scenario I just described:
        //
        // FileNotFoundError: SB-READ2: File not found: 5e93...06d8 [vmaps]
        // at createError (lib/errors.js:264:15)
        // at /home/mha/Projects/core/lib/system/storage-base.js:127:37
        // at async readUTF8TextFile (lib/system/storage-base.js:125:10)
        // at async getNthLineSerializedCb (lib/version-map-query.js:29:21)
        // Error: FileNotFoundError: SB-READ2: File not found: 5e93...06d8 [vmaps]
        // at /home/mha/Projects/core/lib/util/promise.js:102:15
        // at async serializeWithType (lib/util/promise.js:101:10)
        // at async getNthVersionMapEntry (lib/version-map-query.js:54:17)
        // at async getObjectByIdHash (lib/storage-versioned-objects.js:90:7)
        // at async getObjectByIdObj (lib/storage-versioned-objects.js:103:10)
        // at async loadModuleFromOneStorage (lib/module-loader.js:79:7)
        // at async loadModule (lib/module-loader.js:150:16)
        // Error: FileNotFoundError: SB-READ2: File not found: 5e93...06d8 [vmaps]
        // at /home/mha/Projects/core/lib/util/promise.js:102:15
        // at async serializeWithType (lib/util/promise.js:101:10)
        // at async load (lib/module-loader.js:177:10)
        // at async Context.testFn (test/microdata-exploder-test.js:216:25)
        const e = new Error(err);

        Object.assign(e, err, {
            stack: `${err.stack}\n${e.stack}`
        });

        throw e;
    });
}

/**
 * Implementation of [`Promise.allSettled`](https://github.com/tc39/proposal-promise-allSettled).
 * This function never rejects and all promises fulfill, the fulfilled/rejected status is
 * instead supplied in a meta-object of the always-fulfilled promise.
 * **NOTE:** Unlike the proposed function our implementation *guarantees* that rejection values
 * are instances of `Error`. By invoking our centralized `createError()` when the value is not
 * an `Error` we have a special error (with unique code) to get notified of such issues -
 * rejections *should* really always be in the form of errors, for consistency and also for the
 * stack trace.
 * @static
 * @async
 * @param {Array<Promise<T>>} promises - An array of Promise objects
 * @returns {Promise<AllSettled<T>>}
 */
export async function allSettled<T>(promises: Iterable<Promise<T>>): Promise<Array<AllSettled<T>>> {
    return await Promise.all(
        // Array.isArray refinement loses the array's type
        // TypeScript issue https://github.com/microsoft/TypeScript/issues/32497
        (Array.isArray(promises) ? promises : [...promises]).map((promise: Promise<T>) =>
            promise.then(
                value => ({
                    value,
                    status: AllSettledStatus.FULFILLED
                }),
                reason => ({
                    reason: reason instanceof Error ? reason : createError('UP-SETTL1', {reason}),
                    status: AllSettledStatus.REJECTED
                })
            )
        )
    );
}

/**
 * @private
 * @param {Function} fn - A function returning a promise
 * @param {number} [delay=600] - Delay between retries in milliseconds
 * @param {number} [delayMultiplier=1] - The delay is multiplied by this number at each attempt
 * @param {number} [retries=3] - How many times to retry
 * @param {function(Error):boolean} [shouldRetry]
 * @returns {Promise<*>}
 */
async function retryFn<T>(
    fn: (...args: any[]) => Promise<T>,
    delay: number,
    delayMultiplier: number,
    retries: number,
    shouldRetry: (err: Error) => boolean
): Promise<T> {
    try {
        return await fn();
    } catch (err) {
        if (retries > 0 && shouldRetry(err)) {
            MessageBus.send('debug', `Retries left: ${String(retries - 1)} because of ${err}`);
            await wait(delay * delayMultiplier);
            return await retryFn(
                fn,
                delay * delayMultiplier,
                delayMultiplier,
                retries - 1,
                shouldRetry
            );
        }

        throw err;
    }
}

/**
 * The function returning a promise is executed again when the promise rejects until it either
 * resolves or the maximum number of retries is reached.
 * @static
 * @async
 * @param {function(*):Promise<T>} fn - A function of type `(...args) => Promise<T>`
 * @param {object} [options]
 * @param {number} [options.delay=600] - Delay between retries in milliseconds
 * @param {number} [options.retries=3] - How many times to retry
 * @param {function(Error):boolean} [options.shouldRetry] - This optional function can return
 * `false` after checking the `Error` object to prevent additional retries
 * @param {number} [options.delayMultiplier=1] - The delay is multiplied by this number at each
 * attempt
 * @returns {Promise<T>} Resolves with whatever the given function fn returns
 */
export async function retry<T>(
    fn: (...args: any[]) => Promise<T>,
    {
        // Defaults when an object is provided but not all properties
        delay = 600,
        retries = 3,
        delayMultiplier = 1,
        shouldRetry = () => true
    }: {
        delay?: number;
        retries?: number;
        delayMultiplier?: number;
        shouldRetry?: (err: Error) => boolean;
    } = {
        // Defaults when no object is provided at all
        delay: 600,
        retries: 3,
        delayMultiplier: 1,
        shouldRetry: () => true
    }
): Promise<T> {
    // Deliberately outside the promise: This should happen during development only because this
    // is "static", not dependent on user data, and a forgotten catch() could lead to an error
    // raised inside the promise to be silently dropped depending on how the code was written.
    if (!isFunction(fn)) {
        throw createError('UP-RETR1', {type: typeof fn});
    }

    return await retryFn(fn, delay, delayMultiplier, retries, shouldRetry);
}

/**
 * This function creates a "passive tracking promise".
 *
 * ## Usage
 *
 * This function returns the promise as well as its resolve/reject callbacks, normally hidden
 * inside the promise and used by code normally run through the promise, which a tracking
 * promise does not have.
 *
 * You keep the promise and give the resolve/reject callbacks to the function that you want to
 * track, where previously you would have gotten an event emitter to subscribe to or given a
 * callback function to. That code, instead of emitting an event or calling your callback
 * function, now calls the resolve or the reject callback of the promise.
 *
 * ## Explanation
 *
 * Normally promises control all the code that produces their success or failure result. This
 * tracking promise however does not.
 *
 * Tracking promises are useful when a "3rd party" otherwise not involved with direct control
 * wants to know the final outcome of an asynchronous procedure. For example, if we have a
 * stream the code controlling the stream will not use a promise. There may however be a third
 * party that only wants to know when the stream ends but does not care what happens during the
 * stream.
 *
 * While we could expose the details of the stream, the "error" event and the "finish" (or
 * "end") events, for example, we find it much more convenient and also a nice abstraction
 * across more than just streams to use this *tracking promise* device.
 *
 * **In addition, unlike event handlers the promise keeps the state once it has been set, so
 * subscribing after the state-changing event already happened still gives the correct result.**
 * If we rely on event handlers instead we would have to have an additional public property or
 * method to access the current state.
 *
 * Errors reported through the tracking promise also may not be the actual errors but a generic
 * one, after all, code using the tracking promise is not involved in any details.
 *
 * The procedure to create a tracking promise is bloody: The visceral functions usually hidden
 * deep inside the promise's peritoneum are forcefully exposed to the outside, one might say the
 * whole promise is turned inside out. We know this is not "standard" but after giving it a lot
 * of thought we still find the concept appealing for the given use case.
 *
 * ## Default rejection handler
 *
 * Tracking promises receive a default rejection `catch()` handler that discards and ignores any
 * errors.
 *
 * **This has no influence if any handler, for rejection or for success, is attached to the
 * promise at all.**
 *
 * It is used only in case the tracking promise is not used, if nothing is attached to it. This
 * can happen, for example, if a tracking promise is an optional property in an API-object to
 * indicate overall end with failure or success of a process. We use it for file streams on the
 * system level, for example.
 *
 * If the promise is used this default handler has no function and does not influence the
 * behavior of the promise. That means any rejection handler will still catch the rejection, and
 * any success handler creates a new promise that might reject if the underlying promise rejects
 * and therefore always needs to be coupled with a rejection handler as usual, see
 * {@link https://stackoverflow.com/questions/42460039/promise-reject-causes-uncaught-in-promise-warning} for an explanation.
 *
 * ## Reasons for a tracking promise:
 *
 * - Hide internals: Is it an event based process like a stream, or something completely custom?
 *   It does not matter, if it fits the pattern "some async. process we are not involved in but
 *   would like to know when it ends - transmitting a result or failure is an additional feature.
 *
 * - Code that already uses promises or async/await to coordinate asynchronous activities can
 *   seamlessly use the promise instead of having extra (non-promise) code mixed with the
 *   promise-based one.
 *
 * - Tracking promises are used in place of events (error, end) or callbacks. When the asynchronous
 *   process uses those, but the code that wants either or both of 1) synchronization and 2) the
 *   result (success value, error) without controlling the process (in which case there would be
 *   no choice) may prefer a promise.
 *
 * ## When *not* to use a tracking promise:
 *
 * - The code that wants to use a tracking promise is actually directly responsible for the
 *   asynchronous process. In that case it should use the actual constructs (e.g. events).
 *
 * - When it doesn't feel right :-)
 * @static
 * @returns {TrackingPromiseObj<T>} Returns an object with the tracking promise and its `resolve`
 * and `reject` methods
 */
export function createTrackingPromise<T>(): TrackingPromiseObj<T> {
    // The variables will immediately be assigned to just below when the newly created promise
    // synchronously executes the function it is given. Reminder: Promise creation itself is
    // synchronous - only the resolution and of course any asynchronous functions are
    // asynchronous. If we were to wait for the resolution of the promise it would not happen in
    // this "tick" of the Javascript runtime even if the promise contained no asynchronous code,
    // but the promise function itself is run synchronously in the current tick!
    let resolver;
    let rejecter;

    // TRACKING PROMISE (passive)
    // Note that the promise is "bare metal", it does not have any code apart from what is
    // needed to export its internal resolve/reject callbacks.
    const promise = new Promise<T>((resolve, reject) => {
        resolver = resolve;
        rejecter = reject;
    });

    // Prevent "Unhandled promise rejection" errors if the "promise" property is not used (i.e. the
    // tracking promise was created in vain, but the code that uses it to signal something might
    // still reject it)
    promise.catch(ignore => undefined);

    return {
        promise,
        // Type casts: We know that the PROMISE CREATION ABOVE IS SYNCHRONOUS!
        resolve: resolver as unknown as PromiseResolveCb<T>,
        reject: rejecter as unknown as PromiseRejectCb
    };
}