-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathset-timeout-async.lib.ts
More file actions
98 lines (87 loc) · 3.59 KB
/
set-timeout-async.lib.ts
File metadata and controls
98 lines (87 loc) · 3.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import { AbortError } from '../../abort-error';
import { copyAbortError } from '../../abort-error/abort-error.lib';
import { logger } from '../../../shared/logger';
import { requestOptionsSymbol } from './set-timeout-async.constants';
import { SetTimeoutAsyncAbortableRequestOptions } from './set-timeout-async.types';
/**
* Executes a handler function or evaluates a code string with a timeout,
* supporting abort signal for cancellation.
*
* @template T The return type of the handler function.
* @param {((signal: AbortSignal) => T | Promise<T>)} handler -
* A function that accepts an AbortSignal and returns a value or Promise.
* This function will be called with an AbortSignal to ensure cleanup upon interruption.
* @param {number} [delay] - Optional timeout in milliseconds. If not provided,
* the handler will be scheduled without a delay.
* @param {Object} [options] - Configuration options.
* @param {AbortSignal} [options.signal] - AbortSignal to cancel the timeout.
* If not provided, a new AbortController will be created internally.
* @param {any[]} [options.args] - Arguments to pass to the handler.
* @returns {Promise<T>} A promise that resolves with the handler's result or rejects
* with an AbortError if the operation is aborted, or with any error thrown by the handler.
*
* @example
* const controller = new AbortController();
*
* try {
* const data = await setTimeoutAsync(
* (signal) => fetch('/api/data', { signal }).then(res => res.json()),
* 5000,
* { signal: controller.signal }
* )
* } catch (error) {
* console.log(error.name) // 'AbortError' Saborter's Error
* }
*/
export const setTimeoutAsync = <T, A extends [unknown?, ...unknown[]] = []>(
handler: (signal: AbortSignal, requestOptions: SetTimeoutAsyncAbortableRequestOptions<A>) => T | Promise<T>,
delay?: number,
options?: {
signal?: AbortSignal;
args?: A;
[requestOptionsSymbol]?: SetTimeoutAsyncAbortableRequestOptions<A>;
}
): Promise<T> => {
const { args = [], signal = new AbortController().signal } = options ?? {};
const requestOptions = options?.[requestOptionsSymbol];
return new Promise<T>((resolve, reject) => {
let timeoutId: NodeJS.Timeout | undefined;
if (signal.aborted) {
if (!signal.reason?.message) {
logger.warn(`${setTimeoutAsync.name} -> no message indicating the reason for the signal interruption`, signal);
}
reject(
new AbortError(signal.reason?.message || 'The signal was interrupted before the timeout was initialized', {
initiator: setTimeoutAsync.name,
reason: signal.reason
})
);
}
const handleEventListener = () => {
clearTimeout(timeoutId);
if (signal.reason instanceof AbortError) {
return reject(copyAbortError(signal.reason, { initiator: setTimeoutAsync.name }));
}
const error = new AbortError(`The callback was interrupted`, {
initiator: setTimeoutAsync.name,
reason: signal.reason
});
reject(error);
};
timeoutId = setTimeout(() => {
try {
const mergeArgs = [...(requestOptions?.args ?? []), ...args];
const promise = handler(signal, { ...requestOptions, args: mergeArgs as never });
if (promise instanceof Promise) {
return promise.then(resolve).catch(reject);
}
return resolve(promise);
} catch (error) {
reject(error);
} finally {
signal?.removeEventListener('abort', handleEventListener);
}
}, delay);
signal?.addEventListener('abort', handleEventListener, { once: true });
});
};