-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.ts
115 lines (107 loc) · 2.97 KB
/
index.ts
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
106
107
108
109
110
111
112
113
114
115
import Denque from "denque";
class Modrate {
#interval: number;
#limit: number;
#latest = new Denque<number>();
#running = 0;
#waiting = new Denque<(arg0?: unknown) => void>();
#timeouts = new Denque<NodeJS.Timeout>();
constructor(interval: number, limit: number) {
this.#interval = interval;
this.#limit = limit;
}
#poll() {
if (this.#waiting.size() <= this.#timeouts.size()) return;
if (this.#running + this.#timeouts.size() >= this.#limit) return;
if (
this.#latest.size() >=
this.#limit - this.#running - this.#timeouts.size()
) {
const oldest = this.#latest.shift()!;
const now = Date.now();
if (now - oldest < this.#interval) {
const timeout = setTimeout(
() => {
this.#timeouts.shift();
this.#poll();
},
oldest + this.#interval - now
);
this.#timeouts.push(timeout);
return;
}
}
++this.#running;
this.#waiting.shift()!();
}
/**
* Wait until execution is possible.
*
* @param signal To abort waiting. If aborted, the execution isn't counted.
* @throws {DOMException} If aborted.
* @returns A promise resolving with a callback to notify execution done.
* Pass false to the callback if the execution shouldn't be counted.
*
* @example
* const done = await modr.wait(AbortSignal.timeout(1000));
* // operations...
* if (ok) done();
* else done(false);
*
*/
async wait(signal?: AbortSignal) {
await new Promise((resolve, reject) => {
signal?.throwIfAborted();
signal?.addEventListener(
"abort",
() => {
reject(signal.reason);
const index = this.#waiting.toArray().indexOf(resolve);
if (index === -1) return;
this.#waiting.removeOne(index);
if (this.#waiting.size() < this.#timeouts.size()) {
clearTimeout(this.#timeouts.pop());
}
},
{ once: true }
);
this.#waiting.push(resolve);
this.#poll();
});
return (count?: boolean) => {
--this.#running;
if (count === false) {
clearTimeout(this.#timeouts.pop());
} else {
this.#latest.push(Date.now());
}
this.#poll();
};
}
}
/**
* Creates a throttled version of the given function, whose executions are
* scheduled to fit the limit.
*
* @param fn Function to throttle.
* @param interval Time frame, in milliseconds, during which a specific number
* of executions are permitted.
* @param limit Maximum number of executions within any interval.
* @returns Throttled function.
*/
function wrap<T extends (...args: any[]) => ReturnType<T>>(
fn: T,
interval: number,
limit: number
) {
const modrate = new Modrate(interval, limit);
return async function (this: any, ...args: Parameters<T>) {
const done = await modrate.wait();
try {
return await fn.apply(this, args);
} finally {
done();
}
};
}
export = { wrap, Modrate };