All files / blong-gogo/src timeout.ts

0% Statements 0/104
0% Branches 0/1
0% Functions 0/1
0% Lines 0/104

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105                                                                                                                                                                                                                 
import type {IMeta} from '@feasibleone/blong/types';
import hrtime from 'browser-process-hrtime';

type HRTime = ReturnType<typeof hrtime>;
const now = (): HRTime => hrtime();

const isAfter = (time: HRTime, timeout: HRTime): boolean =>
    Array.isArray(timeout) &&
    (time[0] > timeout[0] || (time[0] === timeout[0] && time[1] > timeout[1]));

interface IEnd {
    (error?: Error): void;
    checkTimeout: (time: HRTime) => void;
}

class Timeout {
    #calls: Set<IEnd> = new Set();
    #interval: NodeJS.Timeout | undefined;

    protected clean(): void {
        Array.from(this.#calls).forEach((end: {checkTimeout: (time: HRTime) => void}) =>
            end.checkTimeout(now()),
        );
    }

    protected startWait(
        onTimeout: (error: Error) => void,
        timeout: HRTime,
        createTimeoutError: () => Error,
        set?: Set<IEnd>,
    ): IEnd {
        this.#interval = this.#interval || setInterval(this.clean.bind(this), 500);
        const end: IEnd = (error?: Error) => {
            this.endWait(end, set);
            if (error) onTimeout(error);
        };
        end.checkTimeout = time => isAfter(time, timeout) && end(createTimeoutError());
        this.#calls.add(end);
        set?.add(end);
        return end;
    }

    protected endWait(end: IEnd, set?: Set<IEnd>): void {
        this.#calls.delete(end);
        if (set) set.delete(end);
        if (this.#calls.size <= 0 && this.#interval) {
            clearInterval(this.#interval as NodeJS.Timeout);
            this.#interval = undefined;
        }
    }

    protected startPromise(
        params: unknown,
        fn: (params: unknown) => Promise<unknown>,
        $meta: IMeta,
        error: () => Error,
        set: Set<IEnd>,
    ): Promise<unknown> {
        if (Array.isArray($meta && $meta.timeout)) {
            return new Promise((resolve, reject) => {
                const endWait = this.startWait(
                    waitError => {
                        $meta.mtid = 'error';
                        if ($meta.dispatch) {
                            Promise.resolve($meta.dispatch(waitError, $meta)).catch(() => {});
                            resolve(false);
                        } else {
                            resolve([waitError, $meta]);
                        }
                    },
                    $meta.timeout as HRTime,
                    error,
                    set,
                );
                Promise.resolve(params)
                    .then(fn)
                    .then(result => {
                        endWait();
                        resolve(result);
                        return result;
                    })
                    .catch(fnError => {
                        endWait();
                        reject(fnError);
                    });
            });
        } else {
            return Promise.resolve(params).then(fn);
        }
    }

    public startRequest(
        $meta: IMeta,
        error: () => Error,
        onTimeout: (error: Error) => void,
    ): IEnd | false {
        return (
            Array.isArray($meta && $meta.timeout) &&
            this.startWait(onTimeout, $meta.timeout as HRTime, error)
        );
    }
}

export default new Timeout();