feat(delay): Support AbortSignal to delay for improved cancellation (#52)

* feat: Support AbortSignal to delay for improved cancellation

* docs: add AbortSignal in delay

* refactor: add once setting in addEventListener

* fix: abortError sentence

* feat: separate error file
This commit is contained in:
hanna 2024-06-15 15:20:42 +09:00 committed by GitHub
parent 50c358589c
commit 8d80869fef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 129 additions and 11 deletions

View File

@ -2,17 +2,19 @@
코드의 실행을 주어진 밀리세컨드만큼 지연시켜요.
이 함수는 특정한 시간 이후에 Resolve되는 Promise를 반환해요. async/await 함수를 사용하는 경우에 함수의 실행을 잠깐 일시정지시킬 수 있어요.
이 함수는 특정한 시간 이후에 Resolve되는 Promise를 반환해요. async/await 함수를 사용하는 경우에 함수의 실행을 잠깐 일시정지시킬 수 있어요. 또한, 선택 옵션으로 지연을 취소할 수 있는 AbortSignal을 지원해요.
## 인터페이스
```typescript
function delay(ms: number): Promise<void>;
function delay(ms: number, options?: DelayOptions): Promise<void>;
```
### 파라미터
- `ms` (`number`): 코드 실행을 지연시킬 밀리세컨드.
- `options` (`DelayOptions`, optional): 옵션 객체.
- `signal` (`AbortSignal`, optional): 지연을 취소하기 위한 선택적 `AbortSignal`.
### 반환 값
@ -20,6 +22,8 @@ function delay(ms: number): Promise<void>;
## 예시
### 기본 사용법
```typescript
async function foo() {
console.log('시작');
@ -29,3 +33,19 @@ async function foo() {
foo();
```
### AbortSignal 사용법
```typescript
async function foo() {
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => controller.abort(), 50); // 50ms 후 지연을 취소
try {
await delay(1000, { signal });
} catch (error) {
console.log(error); // 'The operation was aborted' 로깅
}
}
```

View File

@ -4,16 +4,19 @@ Delays the execution of code for a specified number of milliseconds.
This function returns a Promise that resolves after the specified delay, allowing you to use it
with async/await to pause execution.
It also supports an optional AbortSignal to cancel the delay.
## Signature
```typescript
function delay(ms: number): Promise<void>;
function delay(ms: number, options?: DelayOptions): Promise<void>;
```
### Parameters
- `ms` (`number`): The number of milliseconds to delay.
- `options` (`DelayOptions`, optional): An options object.
- `signal` (`AbortSignal`, optional): An optional `AbortSignal` to cancel the delay.
### Returns
@ -21,6 +24,8 @@ function delay(ms: number): Promise<void>;
## Examples
### Basic Usage
```typescript
async function foo() {
console.log('Start');
@ -30,3 +35,19 @@ async function foo() {
foo();
```
### Using with an AbortSignal
```typescript
async function foo() {
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => controller.abort(), 50); // Will cancel the delay after 50ms
try {
await delay(1000, { signal });
} catch (error) {
console.log(error); // Will log 'The operation was aborted'
}
}
```

6
src/error/AbortError.ts Normal file
View File

@ -0,0 +1,6 @@
export class AbortError extends Error {
constructor(message = 'The operation was aborted') {
super(message);
this.name = 'AbortError';
}
}

1
src/error/index.ts Normal file
View File

@ -0,0 +1 @@
export { AbortError } from './AbortError';

View File

@ -126,7 +126,7 @@ describe('debounce', () => {
await delay(debounceMs);
expect(func).not.toHaveBeenCalled();
})
});
it('should not add multiple abort event listeners', async () => {
const func = vi.fn();

View File

@ -67,11 +67,9 @@ export function debounce<F extends (...args: any[]) => void>(
clearTimeout(timeoutId);
timeoutId = null;
}
signal?.removeEventListener('abort', onAbort);
};
signal?.addEventListener('abort', onAbort);
signal?.addEventListener('abort', onAbort, { once: true });
return debounced;
}

View File

@ -1,4 +1,5 @@
export * from './array';
export * from './error';
export * from './function';
export * from './math';
export * from './object';

View File

@ -1,5 +1,5 @@
import { performance } from 'node:perf_hooks';
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { delay } from './delay';
describe('delay', () => {
@ -10,4 +10,40 @@ describe('delay', () => {
expect(end - start).greaterThanOrEqual(99);
});
it('should cancel the delay if aborted via AbortSignal', async () => {
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => controller.abort(), 50);
await expect(delay(100, { signal })).rejects.toThrow('The operation was aborted');
});
it('should not call the delay if it is already aborted by AbortSignal', async () => {
const controller = new AbortController();
const { signal } = controller;
const spy = vi.spyOn(global, 'setTimeout');
controller.abort();
await expect(delay(100, { signal })).rejects.toThrow('The operation was aborted');
expect(spy).not.toHaveBeenCalled();
spy.mockRestore();
});
it('should clear timeout when aborted by AbortSignal', async () => {
const controller = new AbortController();
const { signal } = controller;
const spy = vi.spyOn(global, 'clearTimeout');
const promise = delay(100, { signal });
controller.abort();
await expect(promise).rejects.toThrow('The operation was aborted');
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
});

View File

@ -1,3 +1,9 @@
import { AbortError } from '../error/AbortError';
interface DelayOptions {
signal?: AbortSignal;
}
/**
* Delays the execution of code for a specified number of milliseconds.
*
@ -5,6 +11,8 @@
* with async/await to pause execution.
*
* @param {number} ms - The number of milliseconds to delay.
* @param {DelayOptions} options - The options object.
* @param {AbortSignal} options.signal - An optional AbortSignal to cancel the delay.
* @returns {Promise<void>} A Promise that resolves after the specified delay.
*
* @example
@ -15,9 +23,36 @@
* }
*
* foo();
*
* // With AbortSignal
* const controller = new AbortController();
* const { signal } = controller;
*
* setTimeout(() => controller.abort(), 50); // Will cancel the delay after 50ms
* try {
* await delay(100, { signal });
* } catch (error) {
* console.error(error); // Will log 'AbortError'
* }
* }
*/
export function delay(ms: number): Promise<void> {
return new Promise(resolve => {
setTimeout(resolve, ms);
export function delay(ms: number, { signal }: DelayOptions = {}): Promise<void> {
return new Promise((resolve, reject) => {
const abortError = () => {
reject(new AbortError());
};
const abortHandler = () => {
clearTimeout(timeoutId);
abortError();
};
if (signal?.aborted) {
return abortError();
}
const timeoutId = setTimeout(resolve, ms);
signal?.addEventListener('abort', abortHandler, { once: true });
});
}