mirror of
https://github.com/toss/es-toolkit.git
synced 2024-11-24 03:32:58 +03:00
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:
parent
50c358589c
commit
8d80869fef
@ -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' 로깅
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -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
6
src/error/AbortError.ts
Normal 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
1
src/error/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { AbortError } from './AbortError';
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
export * from './array';
|
||||
export * from './error';
|
||||
export * from './function';
|
||||
export * from './math';
|
||||
export * from './object';
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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 });
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user