Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(esl-utils): debounced unhandled rejection #1839

Merged
merged 8 commits into from
Aug 8, 2023
60 changes: 40 additions & 20 deletions src/modules/esl-utils/async/promise/defered.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
/** Deferred object represents promise with it's resolve/reject methods */
export type Deferred<T> = {
/** Wrapped promise */
promise: Promise<T>;
/** Function that resolves wrapped promise */
resolve: (arg: T) => void;
/** Function that rejects wrapped promise */
reject: (arg?: any) => void;
};
import {memoize} from '../../decorators/memoize';

/**
* Creates Deferred Object that wraps promise and its resolve and reject callbacks
*/
/** Deferred object that represents promise with its resolve/reject methods */
export class Deferred<T> {
protected _status: 'pending' | 'resolved' | 'rejected' = 'pending';
protected _value: T | undefined;
protected _callbacks: [(arg: T) => void, (arg?: any) => void];

/** @returns promise based on {@link Deferred} state*/
@memoize()
public get promise(): Promise<T> {
if (this._status === 'resolved') return Promise.resolve(this._value as T);
if (this._status === 'rejected') return Promise.reject(this._value);
return new Promise<T>((res, rej) => {
this._callbacks = [res, rej];
});
}

/** Resolves deferred promise */
public resolve(arg: T): Deferred<T> {
ala-n marked this conversation as resolved.
Show resolved Hide resolved
if (this._status === 'pending') {
this._value = arg;
this._status = 'resolved';
this._callbacks && this._callbacks[0](arg);
}
return this;
}

/** Rejects deferred promise */
public reject(arg?: any): Deferred<T> {
ala-n marked this conversation as resolved.
Show resolved Hide resolved
if (this._status === 'pending') {
this._value = arg;
this._status = 'rejected';
this._callbacks && this._callbacks[1](arg);
}
return this;
}
}

/** Creates Deferred Object that wraps promise and its resolve and reject callbacks */
export function createDeferred<T>(): Deferred<T> {
let reject: any;
let resolve: any;
// Both reject and resolve will be assigned anyway while the Promise constructing.
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return {promise, resolve, reject};
return new Deferred();
}
67 changes: 65 additions & 2 deletions src/modules/esl-utils/async/test/promise/defered.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,77 @@
import {createDeferred} from '../../promise/defered';
import {promisifyTimeout} from '../../promise/timeout';

describe('async/promise/deferred', () => {
test('Deferred resolves promise when it is resolved', () => {
test('Resolve of Deferred produces resolved promise', () => {
const def$$ = createDeferred();
def$$.resolve(1);
return def$$.promise.then((n) => expect(n).toBe(1));
});
test('Deferred rejects promise when it is rejected', () => {

test('Rejected Deferred produces rejected promise', () => {
const def$$ = createDeferred();
def$$.reject(1);
return def$$.promise.catch((n) => expect(n).toBe(1));
});

test('Deferred resolves initially requested promise when resolved', () => {
const def$$ = createDeferred();
def$$.promise;
def$$.resolve(1);
return def$$.promise.then((n) => expect(n).toBe(1));
});

test('Deferred rejects initially requested promise when rejected', () => {
const def$$ = createDeferred();
def$$.promise;
def$$.reject(1);
return def$$.promise.catch((n) => expect(n).toBe(1));
});

describe('Rejected Deferred doesn`t lead to uncaught in promise', () => {
const throwFn = jest.fn((reason) => {throw reason;});

beforeAll(() => {
process.env.LISTENING_TO_UNHANDLED_REJECTION = String(true);
process.on('unhandledRejection', throwFn);
});

afterAll(() => {
process.off('unhandledRejection', throwFn);
});

test('Deferred doesn`t lead to uncaught', async () => {
const def$$ = createDeferred();
def$$.reject(1);
await promisifyTimeout(0);
expect(throwFn).not.toBeCalled();
});
});

describe('Resolved/rejected Deferred is finalized', () => {
test('Resolved Deferred can not be re-resolved', () => {
const def$$ = createDeferred();
def$$.resolve(1);
def$$.resolve(2);
expect(def$$.promise).resolves.toBe(1);
});
test('Resolved Deferred can not be rejected', () => {
const def$$ = createDeferred();
def$$.resolve(1);
def$$.reject();
expect(def$$.promise).resolves.toBe(1);
});
test('Rejected Deferred can not be re-resolved', () => {
const def$$ = createDeferred();
def$$.reject(1);
def$$.reject(2);
expect(def$$.promise).rejects.toBe(1);
});
test('Rejected Deferred can not be rejected', () => {
const def$$ = createDeferred();
def$$.reject(1);
def$$.resolve(2);
expect(def$$.promise).rejects.toBe(1);
});
});
});