Skip to content

Commit

Permalink
feat(delay): Support AbortSignal to delay for improved cancellation (#52
Browse files Browse the repository at this point in the history
)

* 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
  • Loading branch information
Hanna922 authored Jun 15, 2024
1 parent 50c3585 commit 8d80869
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 11 deletions.
24 changes: 22 additions & 2 deletions docs/ko/reference/promise/delay.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,28 @@

코드의 실행을 주어진 밀리세컨드만큼 지연시켜요.

이 함수는 특정한 시간 이후에 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`.

### 반환 값

(`Promise<void>`): 정해진 지연시간 이후에 Resolve될 Promise.

## 예시

### 기본 사용법

```typescript
async function foo() {
console.log('시작');
Expand All @@ -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' 로깅
}
}
```
23 changes: 22 additions & 1 deletion docs/reference/promise/delay.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,28 @@ 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

(`Promise<void>`): A Promise that resolves after the specified delay.

## Examples

### Basic Usage

```typescript
async function foo() {
console.log('Start');
Expand All @@ -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 changes: 6 additions & 0 deletions src/error/AbortError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class AbortError extends Error {
constructor(message = 'The operation was aborted') {
super(message);
this.name = 'AbortError';
}
}
1 change: 1 addition & 0 deletions src/error/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AbortError } from './AbortError';
2 changes: 1 addition & 1 deletion src/function/debounce.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 1 addition & 3 deletions src/function/debounce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './array';
export * from './error';
export * from './function';
export * from './math';
export * from './object';
Expand Down
38 changes: 37 additions & 1 deletion src/promise/delay.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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();
});
});
41 changes: 38 additions & 3 deletions src/promise/delay.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { AbortError } from '../error/AbortError';

interface DelayOptions {
signal?: AbortSignal;
}

/**
* 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.
*
* @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
Expand All @@ -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 });
});
}

0 comments on commit 8d80869

Please sign in to comment.