This repository was archived by the owner on Feb 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
Copy pathwith-timeout-option.js
105 lines (84 loc) · 2.74 KB
/
with-timeout-option.js
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
99
100
101
102
103
104
105
/* eslint-disable no-unreachable */
import { TimeoutController } from 'timeout-abort-controller'
import { anySignal } from 'any-signal'
import parseDuration from 'parse-duration'
import { TimeoutError } from './errors.js'
/**
* @template {any[]} Args
* @template {Promise<any> | AsyncIterable<any>} R - The return type of `fn`
* @param {(...args:Args) => R} fn
* @param {number} [optionsArgIndex]
* @returns {(...args:Args) => R}
*/
export function withTimeoutOption (fn, optionsArgIndex) {
// eslint-disable-next-line
return /** @returns {R} */(/** @type {Args} */...args) => {
const options = args[optionsArgIndex == null ? args.length - 1 : optionsArgIndex]
if (!options || !options.timeout) return fn(...args)
const timeout = typeof options.timeout === 'string'
? parseDuration(options.timeout)
: options.timeout
const controller = new TimeoutController(timeout)
options.signal = anySignal([options.signal, controller.signal])
const fnRes = fn(...args)
// eslint-disable-next-line promise/param-names
const timeoutPromise = new Promise((_resolve, reject) => {
controller.signal.addEventListener('abort', () => {
reject(new TimeoutError())
})
})
const start = Date.now()
const maybeThrowTimeoutError = () => {
if (controller.signal.aborted) {
throw new TimeoutError()
}
const timeTaken = Date.now() - start
// if we have starved the event loop by adding microtasks, we could have
// timed out already but the TimeoutController will never know because it's
// setTimeout will not fire until we stop adding microtasks
if (timeTaken > timeout) {
controller.abort()
throw new TimeoutError()
}
}
// @ts-expect-error
if (fnRes[Symbol.asyncIterator]) {
// @ts-expect-error
return (async function * () {
// @ts-expect-error
const it = fnRes[Symbol.asyncIterator]()
try {
while (true) {
const { value, done } = await Promise.race([it.next(), timeoutPromise])
if (done) {
break
}
maybeThrowTimeoutError()
yield value
}
} catch (/** @type {any} */ err) {
maybeThrowTimeoutError()
throw err
} finally {
controller.clear()
if (it.return) {
it.return()
}
}
})()
}
// @ts-expect-error
return (async () => {
try {
const res = await Promise.race([fnRes, timeoutPromise])
maybeThrowTimeoutError()
return res
} catch (/** @type {any} */ err) {
maybeThrowTimeoutError()
throw err
} finally {
controller.clear()
}
})()
}
}