From aadc76d1c7480bb8c69193a40008063db02006f9 Mon Sep 17 00:00:00 2001 From: Feoktist Shovchko Date: Fri, 28 Jul 2023 17:07:15 +0300 Subject: [PATCH 1/7] fix(eesl-utils): debounced unhandled rejection --- src/modules/esl-utils/async/debounce.ts | 72 ++++++++++++++++--------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/src/modules/esl-utils/async/debounce.ts b/src/modules/esl-utils/async/debounce.ts index 0c083fe61..c9c91b378 100644 --- a/src/modules/esl-utils/async/debounce.ts +++ b/src/modules/esl-utils/async/debounce.ts @@ -13,6 +13,43 @@ export interface Debounced { cancel(): void; } +class Debouncer { + public timeout: number | null = null; + + private deferred: Deferred> | null = null; + + private promiseRequested = false; + + constructor(private readonly fn: F, private readonly wait = 10, private readonly thisArg?: object) {} + + public debouncedSubject(that: any, ...args: any[]): void { + (typeof this.timeout === 'number') && clearTimeout(this.timeout); + this.timeout = window.setTimeout(() => { + const fn = this.fn.apply(this.thisArg || that, args); + this.promiseRequested && this.deferred?.resolve(fn); + this.resetPromise(); + }, this.wait); + } + + public cancel(): void { + (typeof this.timeout === 'number') && clearTimeout(this.timeout); + this.promiseRequested && this.deferred?.reject(); + this.resetPromise(); + } + + private resetPromise(): void { + this.timeout = null; + this.deferred = null; + this.promiseRequested = false; + } + + public get promise(): Promise | void> { + this.promiseRequested = true; + this.deferred = createDeferred(); + return this.deferred.promise; + } +} + /** * Creates a debounced function that implements {@link Debounced}. * Debounced function delays invoking func until after wait milliseconds have elapsed @@ -22,36 +59,21 @@ export interface Debounced { * @param wait - time to debounce * @param thisArg - optional context to call original function, use debounced method call context if not defined */ -// eslint-disable-next-line @typescript-eslint/ban-types export function debounce(fn: F, wait = 10, thisArg?: object): Debounced { - let timeout: number | null = null; - let deferred: Deferred> | null = null; - - function debouncedSubject(...args: any[]): void { - deferred = deferred || createDeferred(); - (typeof timeout === 'number') && clearTimeout(timeout); - timeout = window.setTimeout(() => { - timeout = null; - // fn.apply to save call context - deferred!.resolve(fn.apply(thisArg || this, args)); - deferred = null; - }, wait); - } - function cancel(): void { - (typeof timeout === 'number') && clearTimeout(timeout); - timeout = null; - deferred?.reject(); - deferred = null; - } + const instance = new Debouncer(fn, wait, thisArg); + + const debouncer = function (...args: Parameters): void { + instance.debouncedSubject(this, ...args); + }; - Object.defineProperty(debouncedSubject, 'promise', { - get: () => deferred ? deferred.promise : Promise.resolve() + Object.defineProperty(debouncer, 'promise', { + get: () => instance.promise }); - Object.defineProperty(debouncedSubject, 'cancel', { + Object.defineProperty(debouncer, 'cancel', { writable: false, enumerable: false, - value: cancel + value: () => instance.cancel() }); - return debouncedSubject as Debounced; + return debouncer as Debounced; } From 1a751e61316aafd192a777ed87bb4cf5ac095ea8 Mon Sep 17 00:00:00 2001 From: Feoktist Shovchko Date: Sat, 29 Jul 2023 17:24:16 +0300 Subject: [PATCH 2/7] chore(esl-utils): rework deferred method --- src/modules/esl-utils/async/debounce.ts | 72 +++++++------------ .../esl-utils/async/promise/defered.ts | 44 +++++++++--- .../async/test/promise/defered.test.ts | 39 ++++++++-- 3 files changed, 93 insertions(+), 62 deletions(-) diff --git a/src/modules/esl-utils/async/debounce.ts b/src/modules/esl-utils/async/debounce.ts index c9c91b378..0c083fe61 100644 --- a/src/modules/esl-utils/async/debounce.ts +++ b/src/modules/esl-utils/async/debounce.ts @@ -13,43 +13,6 @@ export interface Debounced { cancel(): void; } -class Debouncer { - public timeout: number | null = null; - - private deferred: Deferred> | null = null; - - private promiseRequested = false; - - constructor(private readonly fn: F, private readonly wait = 10, private readonly thisArg?: object) {} - - public debouncedSubject(that: any, ...args: any[]): void { - (typeof this.timeout === 'number') && clearTimeout(this.timeout); - this.timeout = window.setTimeout(() => { - const fn = this.fn.apply(this.thisArg || that, args); - this.promiseRequested && this.deferred?.resolve(fn); - this.resetPromise(); - }, this.wait); - } - - public cancel(): void { - (typeof this.timeout === 'number') && clearTimeout(this.timeout); - this.promiseRequested && this.deferred?.reject(); - this.resetPromise(); - } - - private resetPromise(): void { - this.timeout = null; - this.deferred = null; - this.promiseRequested = false; - } - - public get promise(): Promise | void> { - this.promiseRequested = true; - this.deferred = createDeferred(); - return this.deferred.promise; - } -} - /** * Creates a debounced function that implements {@link Debounced}. * Debounced function delays invoking func until after wait milliseconds have elapsed @@ -59,21 +22,36 @@ class Debouncer { * @param wait - time to debounce * @param thisArg - optional context to call original function, use debounced method call context if not defined */ +// eslint-disable-next-line @typescript-eslint/ban-types export function debounce(fn: F, wait = 10, thisArg?: object): Debounced { - const instance = new Debouncer(fn, wait, thisArg); - - const debouncer = function (...args: Parameters): void { - instance.debouncedSubject(this, ...args); - }; + let timeout: number | null = null; + let deferred: Deferred> | null = null; + + function debouncedSubject(...args: any[]): void { + deferred = deferred || createDeferred(); + (typeof timeout === 'number') && clearTimeout(timeout); + timeout = window.setTimeout(() => { + timeout = null; + // fn.apply to save call context + deferred!.resolve(fn.apply(thisArg || this, args)); + deferred = null; + }, wait); + } + function cancel(): void { + (typeof timeout === 'number') && clearTimeout(timeout); + timeout = null; + deferred?.reject(); + deferred = null; + } - Object.defineProperty(debouncer, 'promise', { - get: () => instance.promise + Object.defineProperty(debouncedSubject, 'promise', { + get: () => deferred ? deferred.promise : Promise.resolve() }); - Object.defineProperty(debouncer, 'cancel', { + Object.defineProperty(debouncedSubject, 'cancel', { writable: false, enumerable: false, - value: () => instance.cancel() + value: cancel }); - return debouncer as Debounced; + return debouncedSubject as Debounced; } diff --git a/src/modules/esl-utils/async/promise/defered.ts b/src/modules/esl-utils/async/promise/defered.ts index 4d75a14cf..f73ea260f 100644 --- a/src/modules/esl-utils/async/promise/defered.ts +++ b/src/modules/esl-utils/async/promise/defered.ts @@ -1,4 +1,3 @@ -/** Deferred object represents promise with it's resolve/reject methods */ export type Deferred = { /** Wrapped promise */ promise: Promise; @@ -8,16 +7,43 @@ export type Deferred = { reject: (arg?: any) => void; }; +/** + * LazyDeferred represents a promise with its resolve/reject methods. + * The underlying Promise is created lazily when accessed. + */ +class LazyDeferred implements Deferred { + private _promise: Promise; + private _resolve: (value: T) => void; + private _reject: (reason?: any) => void; + + private _promiseRequested: Promise; + private _resolveRequested: () => void; + + constructor() { + this._promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + this._promiseRequested = new Promise((resolve) => this._resolveRequested = resolve); + } + + public get promise(): Promise { + this._resolveRequested(); + return this._promise; + } + + public resolve(arg: T): void { + this._promiseRequested.then(() => this._resolve(arg)); + } + + public reject(arg?: any): void { + this._promiseRequested.then(() => this._reject(arg)); + } +} + /** * Creates Deferred Object that wraps promise and its resolve and reject callbacks */ export function createDeferred(): Deferred { - let reject: any; - let resolve: any; - // Both reject and resolve will be assigned anyway while the Promise constructing. - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - return {promise, resolve, reject}; + return new LazyDeferred(); } diff --git a/src/modules/esl-utils/async/test/promise/defered.test.ts b/src/modules/esl-utils/async/test/promise/defered.test.ts index d636ebf9b..03314dfca 100644 --- a/src/modules/esl-utils/async/test/promise/defered.test.ts +++ b/src/modules/esl-utils/async/test/promise/defered.test.ts @@ -1,14 +1,41 @@ import {createDeferred} from '../../promise/defered'; describe('async/promise/deferred', () => { - test('Deferred resolves promise when it is resolved', () => { + test('Should not reject promise if it wasn`t requested', async () => { + const deferred = createDeferred(); + const value = 'test-error'; + + deferred.reject(value); + + await Promise.resolve(); + expect.assertions(0); + }); + + test('Should reject promise if it was requested', async () => { + const deferred = createDeferred(); + const value = 'test-error'; + + deferred.reject(value); + expect.assertions(1); + + try { + await deferred.promise; + } catch (error) { + expect(error).toEqual(value); + } + }); + + test('Deferred resolves promise when it`s resolved and promise', () => { const def$$ = createDeferred(); - def$$.resolve(1); - return def$$.promise.then((n) => expect(n).toBe(1)); + const value = 'test-success'; + def$$.resolve(value); + expect(def$$.promise).resolves.toBe(value); }); - test('Deferred rejects promise when it is rejected', () => { + + test('Deferred rejected promise when it`s rejected and promise', () => { const def$$ = createDeferred(); - def$$.reject(1); - return def$$.promise.catch((n) => expect(n).toBe(1)); + const value = 'test-error'; + def$$.reject(value); + expect(def$$.promise).rejects.toBe(value); }); }); From 95923f0025062568195278d3fd4db13668aec464 Mon Sep 17 00:00:00 2001 From: Feoktist Shovchko Date: Sat, 29 Jul 2023 17:26:15 +0300 Subject: [PATCH 3/7] chore(esl-utils): return comment --- src/modules/esl-utils/async/promise/defered.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/esl-utils/async/promise/defered.ts b/src/modules/esl-utils/async/promise/defered.ts index f73ea260f..52a578035 100644 --- a/src/modules/esl-utils/async/promise/defered.ts +++ b/src/modules/esl-utils/async/promise/defered.ts @@ -1,3 +1,4 @@ +/** Deferred object represents promise with it's resolve/reject methods */ export type Deferred = { /** Wrapped promise */ promise: Promise; From 3636fc645315cf60dd34f3f3da494c024fcd1c4f Mon Sep 17 00:00:00 2001 From: Feoktist Shovchko Date: Sat, 29 Jul 2023 20:49:03 +0300 Subject: [PATCH 4/7] chore(esl-utils): code refactoring --- .../esl-utils/async/promise/defered.ts | 41 +++++++----- .../async/test/promise/defered.test.ts | 65 +++++++++++-------- 2 files changed, 61 insertions(+), 45 deletions(-) diff --git a/src/modules/esl-utils/async/promise/defered.ts b/src/modules/esl-utils/async/promise/defered.ts index 52a578035..93ca398b7 100644 --- a/src/modules/esl-utils/async/promise/defered.ts +++ b/src/modules/esl-utils/async/promise/defered.ts @@ -1,44 +1,51 @@ -/** Deferred object represents promise with it's resolve/reject methods */ -export type Deferred = { - /** Wrapped promise */ - promise: Promise; - /** Function that resolves wrapped promise */ - resolve: (arg: T) => void; - /** Function that rejects wrapped promise */ - reject: (arg?: any) => void; +type PromiseAction = { + type: 'resolve'; + arg: T; +} | { + type: 'reject'; + arg?: any; }; /** - * LazyDeferred represents a promise with its resolve/reject methods. + * Deferred represents a promise with its resolve/reject methods. * The underlying Promise is created lazily when accessed. */ -class LazyDeferred implements Deferred { +export class Deferred implements Deferred { private _promise: Promise; private _resolve: (value: T) => void; private _reject: (reason?: any) => void; - private _promiseRequested: Promise; - private _resolveRequested: () => void; + private _promiseCallback?: PromiseAction; + private _promiseRequested = false; constructor() { this._promise = new Promise((resolve, reject) => { this._resolve = resolve; this._reject = reject; }); - this._promiseRequested = new Promise((resolve) => this._resolveRequested = resolve); } + /** Wrapped promise */ public get promise(): Promise { - this._resolveRequested(); + const {_promiseCallback} = this; + if (!this._promiseRequested && _promiseCallback) { + const {arg, type} = _promiseCallback; + type === 'resolve' ? this._resolve(arg) : this._reject(arg); + } + this._promiseRequested = true; return this._promise; } + /** Function that resolves wrapped promise */ public resolve(arg: T): void { - this._promiseRequested.then(() => this._resolve(arg)); + this._promiseRequested && this._resolve(arg); + if (!this._promiseCallback) this._promiseCallback = {type: 'resolve', arg}; } + /** Function that rejects wrapped promise */ public reject(arg?: any): void { - this._promiseRequested.then(() => this._reject(arg)); + this._promiseRequested && this._reject(arg); + if (!this._promiseCallback) this._promiseCallback = {type: 'reject', arg}; } } @@ -46,5 +53,5 @@ class LazyDeferred implements Deferred { * Creates Deferred Object that wraps promise and its resolve and reject callbacks */ export function createDeferred(): Deferred { - return new LazyDeferred(); + return new Deferred(); } diff --git a/src/modules/esl-utils/async/test/promise/defered.test.ts b/src/modules/esl-utils/async/test/promise/defered.test.ts index 03314dfca..673e75ed8 100644 --- a/src/modules/esl-utils/async/test/promise/defered.test.ts +++ b/src/modules/esl-utils/async/test/promise/defered.test.ts @@ -1,41 +1,50 @@ import {createDeferred} from '../../promise/defered'; +import {promisifyTimeout} from '../../promise/timeout'; describe('async/promise/deferred', () => { - test('Should not reject promise if it wasn`t requested', async () => { - const deferred = createDeferred(); - const value = 'test-error'; - - deferred.reject(value); - - await Promise.resolve(); - expect.assertions(0); + test('Resolve of Deferred produces resolved promise', () => { + const def$$ = createDeferred(); + def$$.resolve(1); + return def$$.promise.then((n) => expect(n).toBe(1)); }); - test('Should reject promise if it was requested', async () => { - const deferred = createDeferred(); - const value = 'test-error'; - - deferred.reject(value); - expect.assertions(1); - - try { - await deferred.promise; - } catch (error) { - expect(error).toEqual(value); - } + test('Rejected Deferred produces rejected promise', () => { + const def$$ = createDeferred(); + def$$.reject(1); + return def$$.promise.catch((n) => expect(n).toBe(1)); }); - test('Deferred resolves promise when it`s resolved and promise', () => { + test('Deferred resolves initially requested promise when resolved', () => { const def$$ = createDeferred(); - const value = 'test-success'; - def$$.resolve(value); - expect(def$$.promise).resolves.toBe(value); + def$$.promise; + def$$.resolve(1); + return def$$.promise.then((n) => expect(n).toBe(1)); }); - test('Deferred rejected promise when it`s rejected and promise', () => { + test('Deferred rejects initially requested promise when rejected', () => { const def$$ = createDeferred(); - const value = 'test-error'; - def$$.reject(value); - expect(def$$.promise).rejects.toBe(value); + def$$.promise; + def$$.reject(1); + return def$$.promise.catch((n) => expect(n).toBe(1)); + }); + + describe('Rejected Deferred does not leads 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 does not leads to uncaught', async () => { + const def$$ = createDeferred(); + def$$.reject(1); + await promisifyTimeout(0); + expect(throwFn).not.toBeCalled(); + }); }); }); From e056ecf0ae65a8a668fd30890aceb4514fde9c90 Mon Sep 17 00:00:00 2001 From: Feoktist Shovchko Date: Sat, 29 Jul 2023 20:59:34 +0300 Subject: [PATCH 5/7] chore(esl-utils): orphographic refactoring --- src/modules/esl-utils/async/test/promise/defered.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/esl-utils/async/test/promise/defered.test.ts b/src/modules/esl-utils/async/test/promise/defered.test.ts index 673e75ed8..ea63f5e74 100644 --- a/src/modules/esl-utils/async/test/promise/defered.test.ts +++ b/src/modules/esl-utils/async/test/promise/defered.test.ts @@ -28,7 +28,7 @@ describe('async/promise/deferred', () => { return def$$.promise.catch((n) => expect(n).toBe(1)); }); - describe('Rejected Deferred does not leads to uncaught in promise', () => { + describe('Rejected Deferred doesn`t lead to uncaught in promise', () => { const throwFn = jest.fn((reason) => {throw reason;}); beforeAll(() => { @@ -40,7 +40,7 @@ describe('async/promise/deferred', () => { process.off('unhandledRejection', throwFn); }); - test('Deferred does not leads to uncaught', async () => { + test('Deferred doesn`t lead to uncaught', async () => { const def$$ = createDeferred(); def$$.reject(1); await promisifyTimeout(0); From 492504cd86ff9422015c64366797cd7316595821 Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Fri, 4 Aug 2023 15:58:55 +0200 Subject: [PATCH 6/7] refactor(esl-utils): rework and simplify Deferred implementation --- .../esl-utils/async/promise/defered.ts | 72 ++++++++----------- .../async/test/promise/defered.test.ts | 27 +++++++ 2 files changed, 56 insertions(+), 43 deletions(-) diff --git a/src/modules/esl-utils/async/promise/defered.ts b/src/modules/esl-utils/async/promise/defered.ts index 93ca398b7..2ee80e189 100644 --- a/src/modules/esl-utils/async/promise/defered.ts +++ b/src/modules/esl-utils/async/promise/defered.ts @@ -1,57 +1,43 @@ -type PromiseAction = { - type: 'resolve'; - arg: T; -} | { - type: 'reject'; - arg?: any; -}; +import {memoize} from '../../decorators/memoize'; -/** - * Deferred represents a promise with its resolve/reject methods. - * The underlying Promise is created lazily when accessed. - */ -export class Deferred implements Deferred { - private _promise: Promise; - private _resolve: (value: T) => void; - private _reject: (reason?: any) => void; +/** A deferred object represents promise with it's resolve/reject methods */ +export class Deferred { + protected _status: 'pending' | 'resolved' | 'rejected' = 'pending'; + protected _value: T | undefined; + protected _callbacks: [(arg: T) => void, (arg?: any) => void]; - private _promiseCallback?: PromiseAction; - private _promiseRequested = false; - - constructor() { - this._promise = new Promise((resolve, reject) => { - this._resolve = resolve; - this._reject = reject; + /** @returns promise based on {@link Deferred} state*/ + @memoize() + public get promise(): Promise { + if (this._status === 'resolved') return Promise.resolve(this._value as T); + if (this._status === 'rejected') return Promise.reject(this._value); + return new Promise((res, rej) => { + this._callbacks = [res, rej]; }); } - /** Wrapped promise */ - public get promise(): Promise { - const {_promiseCallback} = this; - if (!this._promiseRequested && _promiseCallback) { - const {arg, type} = _promiseCallback; - type === 'resolve' ? this._resolve(arg) : this._reject(arg); + /** Resolves deferred promise */ + public resolve(arg: T): Deferred { + if (this._status === 'pending') { + this._value = arg; + this._status = 'resolved'; + this._callbacks && this._callbacks[0](arg); } - this._promiseRequested = true; - return this._promise; + return this; } - /** Function that resolves wrapped promise */ - public resolve(arg: T): void { - this._promiseRequested && this._resolve(arg); - if (!this._promiseCallback) this._promiseCallback = {type: 'resolve', arg}; - } - - /** Function that rejects wrapped promise */ - public reject(arg?: any): void { - this._promiseRequested && this._reject(arg); - if (!this._promiseCallback) this._promiseCallback = {type: 'reject', arg}; + /** Rejects deferred promise */ + public reject(arg?: any): Deferred { + 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 - */ +/** Creates Deferred Object that wraps promise and its resolve and reject callbacks */ export function createDeferred(): Deferred { return new Deferred(); } diff --git a/src/modules/esl-utils/async/test/promise/defered.test.ts b/src/modules/esl-utils/async/test/promise/defered.test.ts index ea63f5e74..07b6d0deb 100644 --- a/src/modules/esl-utils/async/test/promise/defered.test.ts +++ b/src/modules/esl-utils/async/test/promise/defered.test.ts @@ -47,4 +47,31 @@ describe('async/promise/deferred', () => { 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); + }); + }); }); From 4fb5a0af34b9255f5b2bfbe97f62ed8eba511ed9 Mon Sep 17 00:00:00 2001 From: "ala'n (Alexey Stsefanovich)" Date: Tue, 8 Aug 2023 14:20:54 +0200 Subject: [PATCH 7/7] docs(esl-utils): update `defered.ts` tsdoc text Co-authored-by: Anastasiya Lesun <72765981+NastaLeo@users.noreply.github.com> --- src/modules/esl-utils/async/promise/defered.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/esl-utils/async/promise/defered.ts b/src/modules/esl-utils/async/promise/defered.ts index 2ee80e189..49ffe82a6 100644 --- a/src/modules/esl-utils/async/promise/defered.ts +++ b/src/modules/esl-utils/async/promise/defered.ts @@ -1,6 +1,6 @@ import {memoize} from '../../decorators/memoize'; -/** A deferred object represents promise with it's resolve/reject methods */ +/** Deferred object that represents promise with its resolve/reject methods */ export class Deferred { protected _status: 'pending' | 'resolved' | 'rejected' = 'pending'; protected _value: T | undefined;