Repository: exadel-inc/esl
Pull request #2391 (2024-05-07 09:48:53)

feat(esl-utils): add ability to reject promisifyEvent by using AbortSignal
Closes: #2390 




Patch:
diff --git a/src/modules/esl-utils/async/promise/event.ts b/src/modules/esl-utils/async/promise/event.ts
index 892e8d062..b20bc99b5 100644
--- a/src/modules/esl-utils/async/promise/event.ts
+++ b/src/modules/esl-utils/async/promise/event.ts
@@ -24,15 +24,27 @@ export function promisifyEvent(
   options?: boolean | AddEventListenerOptions
 ): Promise<Event> {
   return new Promise((resolve, reject) => {
+    const signal = (options as AddEventListenerOptions)?.signal;
+    if (signal?.aborted) return rejectOnSignal();
+
     function eventCallback(e?: Event): void {
       target.removeEventListener(event, eventCallback, options);
       e ? resolve(e) : reject(new Error('Rejected by timeout'));
+      signalCallback();
+    }
+    function signalCallback(e?: Event): void {
+      signal?.removeEventListener('abort', signalCallback);
+      e && rejectOnSignal();
+    }
+    function rejectOnSignal(): void {
+      reject(new Error('Rejected by abort signal'));
     }
 
     target.addEventListener(event, eventCallback, options);
     if (typeof timeout === 'number' && timeout >= 0) {
       setTimeout(eventCallback, timeout);
     }
+    signal?.addEventListener('abort', signalCallback);
   });
 }
 


Test patch:
diff --git a/src/modules/esl-utils/async/test/promise/event.test.ts b/src/modules/esl-utils/async/test/promise/event.test.ts
index 381731aa0..9a71c0c52 100644
--- a/src/modules/esl-utils/async/test/promise/event.test.ts
+++ b/src/modules/esl-utils/async/test/promise/event.test.ts
@@ -20,11 +20,11 @@ describe('async/promise/event', () => {
       return expect(spy).toBeCalledWith('test', expect.any(Function), undefined);
     });
 
-    test('Rejected by timeout if it is exceeded', () => {
+    test('Rejected by timeout if it is exceeded', async () => {
       const el = document.createElement('div');
       const promise$ = promisifyEvent(el, 'test', 10);
       jest.advanceTimersByTime(100);
-      return expect(promise$).rejects.toBeInstanceOf(Error);
+      await expect(promise$).rejects.toThrow(new Error('Rejected by timeout'));
     });
     test('Listener unsubscribed if promise was rejected by timeout', async () => {
       const el = document.createElement('div');
@@ -37,6 +37,56 @@ describe('async/promise/event', () => {
         expect(spy).toBeCalledWith('test', expect.any(Function), undefined);
       }
     });
+    describe('AbortSignal handling', () => {
+      test('Rejected when was passed options with signal and invoked abort', async () => {
+        const el = document.createElement('div');
+        const controller = new AbortController();
+        const promise$ = promisifyEvent(el, 'test', null, {signal: controller.signal});
+        controller.abort();
+        await expect(promise$).rejects.toThrow(new Error('Rejected by abort signal'));
+      });
+      test('Rejected when was passed options with signal in aborted state', async () => {
+        const el = document.createElement('div');
+        const promise$ = promisifyEvent(el, 'test', null, {signal: AbortSignal.abort()});
+        await expect(promise$).rejects.toThrow(new Error('Rejected by abort signal'));
+      });
+      test('AbortSignal listener unsubscribed if the promise was resolved by event', async () => {
+        const el = document.createElement('div');
+        const controller = new AbortController();
+        const signal = controller.signal;
+        const spy = jest.spyOn(signal, 'removeEventListener');
+        const promise$ = promisifyEvent(el, 'test', null, {signal});
+        el.dispatchEvent(new CustomEvent('test'));
+        await promise$;
+        return expect(spy).toBeCalledWith('abort', expect.any(Function));
+      });
+      test('AbortSignal listener unsubscribed if the promise was rejected by timeout', async () => {
+        const el = document.createElement('div');
+        const controller = new AbortController();
+        const signal = controller.signal;
+        const spy = jest.spyOn(signal, 'removeEventListener');
+        const promise$ = promisifyEvent(el, 'test', 10, {signal});
+        jest.advanceTimersByTime(100);
+        try {
+          await promise$;
+        } catch {
+          expect(spy).toBeCalledWith('abort', expect.any(Function));
+        }
+      });
+      test('AbortSignal listener unsubscribed if the promise was rejected by signal', async () => {
+        const el = document.createElement('div');
+        const controller = new AbortController();
+        const signal = controller.signal;
+        const spy = jest.spyOn(signal, 'removeEventListener');
+        const promise$ = promisifyEvent(el, 'test', null, {signal});
+        controller.abort();
+        try {
+          await promise$;
+        } catch {
+          expect(spy).toBeCalledWith('abort', expect.any(Function));
+        }
+      });
+    });
   });
 
   describe('promisifyMarker', () => {