dynamic-throttled-queue 2.1.1-rc.bcc09d3 → 2.2.0-rc.1849e68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/dist/dynamic-throttled-queue.d.ts +17 -0
- package/dist/dynamic-throttled-queue.js +17 -152
- package/dist/dynamic-throttled-queue.js.map +1 -1
- package/dist/rate-controller.d.ts +21 -0
- package/dist/rate-controller.js +40 -0
- package/dist/rate-controller.js.map +1 -0
- package/dist/scheduler.d.ts +3 -0
- package/dist/scheduler.js +145 -0
- package/dist/scheduler.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ Set `concurrency` to bound callbacks that are still awaiting asynchronous comple
|
|
|
43
43
|
| `retry` | `number` | `0` | Non-negative integer number of times to retry failed callbacks |
|
|
44
44
|
| `concurrency` | `number` | — | Maximum callbacks awaiting asynchronous completion; omit for no limit |
|
|
45
45
|
| `compact_threshold` | `number` | `512` | Non-negative integer minimum dead slots before internal queue compaction triggers; `0` compacts at the earliest eligible point |
|
|
46
|
+
| `rateStrategy` | `RateStrategy` | `linear` | Pure policy that requests the next rate and an optional backoff after each observation window |
|
|
46
47
|
| `onRateChange` | `(rate: number) => void` | — | Called when the current rate changes |
|
|
47
48
|
|
|
48
49
|
`retry`, `errors_per_interval`, and `compact_threshold` reject fractional and non-finite values. `errors_per_interval` must be at least `1`; `retry` and `compact_threshold` may be `0`.
|
|
@@ -117,6 +118,42 @@ for (let i = 0; i < 100; i++) {
|
|
|
117
118
|
}
|
|
118
119
|
```
|
|
119
120
|
|
|
121
|
+
### Rate strategies
|
|
122
|
+
|
|
123
|
+
The default `linear` strategy preserves the adaptive behavior above. You can import and pass it explicitly, or provide a custom pure strategy:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import {
|
|
127
|
+
createThrottledQueue,
|
|
128
|
+
linear,
|
|
129
|
+
type RateStrategy,
|
|
130
|
+
} from "dynamic-throttled-queue";
|
|
131
|
+
|
|
132
|
+
const conservative: RateStrategy = ({
|
|
133
|
+
currentRate,
|
|
134
|
+
minRate,
|
|
135
|
+
errorCount,
|
|
136
|
+
errorThreshold,
|
|
137
|
+
}) => ({
|
|
138
|
+
nextRate: errorCount >= errorThreshold ? Math.max(minRate, currentRate - 1) : currentRate,
|
|
139
|
+
shouldBackOff: errorCount >= errorThreshold,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const throttle = createThrottledQueue({
|
|
143
|
+
min_rpi: 1,
|
|
144
|
+
max_rpi: 10,
|
|
145
|
+
interval: 1000,
|
|
146
|
+
rateStrategy: conservative,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Equivalent to omitting rateStrategy:
|
|
150
|
+
createThrottledQueue({ min_rpi: 1, max_rpi: 10, interval: 1000, rateStrategy: linear });
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
A `RateStrategy` receives a frozen observation with `currentRate`, `minRate`, `maxRate`, `errorCount`, `errorThreshold`, `hasPendingWork`, and `wasBackedOff`; it returns `{ nextRate, shouldBackOff }`. Every queue starts at the midpoint of its configured range. The queue clamps finite integer `nextRate` values to its bounds. `shouldBackOff` requests a pause, which the queue performs only when `back_off` is `true`.
|
|
154
|
+
|
|
155
|
+
Strategies must return a finite integer `nextRate` and a boolean `shouldBackOff`. A malformed decision or a strategy exception permanently halts that queue, clears its timers, retains unstarted callbacks in `pending`, and surfaces the original error. Create a new queue instance to resume work.
|
|
156
|
+
|
|
120
157
|
### Backoff
|
|
121
158
|
|
|
122
159
|
```ts
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
export type RateStrategyObservation = Readonly<{
|
|
2
|
+
currentRate: number;
|
|
3
|
+
minRate: number;
|
|
4
|
+
maxRate: number;
|
|
5
|
+
errorCount: number;
|
|
6
|
+
errorThreshold: number;
|
|
7
|
+
hasPendingWork: boolean;
|
|
8
|
+
wasBackedOff: boolean;
|
|
9
|
+
}>;
|
|
10
|
+
export type RateStrategyDecision = Readonly<{
|
|
11
|
+
nextRate: number;
|
|
12
|
+
shouldBackOff: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
export type RateStrategy = (observation: RateStrategyObservation) => RateStrategyDecision;
|
|
15
|
+
export declare const linear: RateStrategy;
|
|
1
16
|
/** Return `false` to signal failure (increments error count, triggers retry if configured). */
|
|
2
17
|
export type ThrottleCallback = () => boolean | void | Promise<boolean | void>;
|
|
3
18
|
export type ThrottleOptions = {
|
|
@@ -14,6 +29,8 @@ export type ThrottleOptions = {
|
|
|
14
29
|
concurrency?: number;
|
|
15
30
|
/** Non-negative integer dead slots before queue compaction triggers. Default 512. */
|
|
16
31
|
compact_threshold?: number;
|
|
32
|
+
/** Policy used to request the next rate and any backoff after each observation window. */
|
|
33
|
+
rateStrategy?: RateStrategy;
|
|
17
34
|
onRateChange?: (rate: number) => void;
|
|
18
35
|
};
|
|
19
36
|
export type ThrottleFn = (callback: ThrottleCallback) => void;
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
import { createRateController } from "./rate-controller.js";
|
|
2
|
+
import { createScheduler } from "./scheduler.js";
|
|
3
|
+
export const linear = ({ minRate, maxRate, currentRate, errorCount, errorThreshold, hasPendingWork, wasBackedOff, }) => {
|
|
4
|
+
if (errorCount >= errorThreshold) {
|
|
5
|
+
return { nextRate: Math.max(minRate, currentRate - 1), shouldBackOff: true };
|
|
6
|
+
}
|
|
7
|
+
if (!wasBackedOff && errorCount === 0 && hasPendingWork) {
|
|
8
|
+
return { nextRate: Math.min(maxRate, currentRate + 1), shouldBackOff: false };
|
|
9
|
+
}
|
|
10
|
+
return { nextRate: currentRate, shouldBackOff: false };
|
|
11
|
+
};
|
|
1
12
|
export function createThrottledQueue(options) {
|
|
2
|
-
const { min_rpi, interval, max_rpi = min_rpi,
|
|
13
|
+
const { min_rpi, interval, max_rpi = min_rpi, concurrency, retry = 0, compact_threshold = 512 } = options;
|
|
3
14
|
const errors_per_interval = options.errors_per_interval ?? 5;
|
|
4
15
|
if (!Number.isInteger(min_rpi) || min_rpi < 1) {
|
|
5
16
|
throw new Error("min_rpi must be a positive integer");
|
|
@@ -22,156 +33,10 @@ export function createThrottledQueue(options) {
|
|
|
22
33
|
if (!Number.isInteger(compact_threshold) || compact_threshold < 0) {
|
|
23
34
|
throw new Error("compact_threshold must be a non-negative integer");
|
|
24
35
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
let isRunning = false;
|
|
31
|
-
let last_called = 0;
|
|
32
|
-
let timeout;
|
|
33
|
-
let dynTimeout;
|
|
34
|
-
let active_count = 0;
|
|
35
|
-
let isStopped = false;
|
|
36
|
-
const max_concurrency = concurrency ?? Infinity;
|
|
37
|
-
const queue = [];
|
|
38
|
-
let head = 0;
|
|
39
|
-
/** Halts timers. Retains unprocessed queue items; next enqueue resumes from where it left off. */
|
|
40
|
-
function halt() {
|
|
41
|
-
isRunning = false;
|
|
42
|
-
skippedLast = false;
|
|
43
|
-
clearTimeout(timeout);
|
|
44
|
-
timeout = undefined;
|
|
45
|
-
clearTimeout(dynTimeout);
|
|
46
|
-
dynTimeout = undefined;
|
|
47
|
-
if (head >= queue.length) {
|
|
48
|
-
queue.length = 0;
|
|
49
|
-
head = 0;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function stop() {
|
|
53
|
-
isStopped = true;
|
|
54
|
-
halt();
|
|
55
|
-
}
|
|
56
|
-
function handleResult(item, result) {
|
|
57
|
-
if (result === false) {
|
|
58
|
-
error_count++;
|
|
59
|
-
if (item.retries > 0) {
|
|
60
|
-
queue.push({ fn: item.fn, retries: item.retries - 1 });
|
|
61
|
-
if (!isRunning && !isStopped && queue.length > head) {
|
|
62
|
-
start();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
function handleSettlement(item, result, resume = false) {
|
|
68
|
-
active_count--;
|
|
69
|
-
handleResult(item, result);
|
|
70
|
-
// A released slot can unblock pending work; dequeue still observes rate pacing.
|
|
71
|
-
if (resume && isRunning && !skippedLast && queue.length > head) {
|
|
72
|
-
dequeue();
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
function dequeue() {
|
|
76
|
-
const threshold = last_called + dyn_interval;
|
|
77
|
-
const now = Date.now();
|
|
78
|
-
if (now < threshold) {
|
|
79
|
-
clearTimeout(timeout);
|
|
80
|
-
timeout = setTimeout(dequeue, threshold - now);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
// Retries appended by settlement are outside this batch and need another start opportunity.
|
|
84
|
-
const end = Math.min(head + dyn_requests_per_interval, queue.length);
|
|
85
|
-
let started = 0;
|
|
86
|
-
while (head < end && active_count < max_concurrency) {
|
|
87
|
-
const item = queue[head++];
|
|
88
|
-
active_count++;
|
|
89
|
-
if (started++ === 0) {
|
|
90
|
-
last_called = Date.now();
|
|
91
|
-
}
|
|
92
|
-
let result;
|
|
93
|
-
try {
|
|
94
|
-
result = item.fn();
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
handleSettlement(item, false);
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
if (result instanceof Promise) {
|
|
101
|
-
void result.then(value => handleSettlement(item, value, true), () => handleSettlement(item, false, true));
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
handleSettlement(item, result);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// ponytail: splice is O(n), amortized by only firing when head > half array length
|
|
108
|
-
if (head > compact_threshold && head > queue.length / 2) {
|
|
109
|
-
queue.splice(0, head);
|
|
110
|
-
head = 0;
|
|
111
|
-
}
|
|
112
|
-
if (head >= queue.length) {
|
|
113
|
-
halt();
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
if (active_count >= max_concurrency) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
timeout = setTimeout(dequeue, dyn_interval);
|
|
120
|
-
}
|
|
121
|
-
function applyRate(newRpi) {
|
|
122
|
-
if (newRpi === current_rpi)
|
|
123
|
-
return;
|
|
124
|
-
current_rpi = newRpi;
|
|
125
|
-
onRateChange?.(current_rpi);
|
|
126
|
-
if (evenly_spaced) {
|
|
127
|
-
dyn_interval = interval / current_rpi;
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
dyn_requests_per_interval = current_rpi;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
// ponytail: async callbacks resolve after adjustRate fires — error_count may lag by one interval under async load
|
|
134
|
-
function adjustRate() {
|
|
135
|
-
dynTimeout = undefined;
|
|
136
|
-
const wasSkipped = skippedLast;
|
|
137
|
-
skippedLast = false;
|
|
138
|
-
if (error_count >= errors_per_interval) {
|
|
139
|
-
applyRate(Math.max(min_rpi, current_rpi - 1));
|
|
140
|
-
if (isRunning && back_off && !wasSkipped) {
|
|
141
|
-
clearTimeout(timeout);
|
|
142
|
-
skippedLast = true;
|
|
143
|
-
timeout = setTimeout(dequeue, dyn_interval + interval);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
else if (!wasSkipped && error_count === 0 && queue.length > head) {
|
|
147
|
-
applyRate(Math.min(max_rpi, current_rpi + 1));
|
|
148
|
-
}
|
|
149
|
-
error_count = 0;
|
|
150
|
-
if (isRunning) {
|
|
151
|
-
dynTimeout = setTimeout(adjustRate, interval);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
function start() {
|
|
155
|
-
if (skippedLast)
|
|
156
|
-
return;
|
|
157
|
-
isRunning = true;
|
|
158
|
-
last_called = Date.now();
|
|
159
|
-
clearTimeout(timeout);
|
|
160
|
-
timeout = setTimeout(dequeue, dyn_interval);
|
|
161
|
-
if (!dynTimeout) {
|
|
162
|
-
dynTimeout = setTimeout(adjustRate, interval);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
function enqueue(callback) {
|
|
166
|
-
queue.push({ fn: callback, retries: retry });
|
|
167
|
-
isStopped = false;
|
|
168
|
-
if (!isRunning) {
|
|
169
|
-
start();
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
enqueue.stop = stop;
|
|
173
|
-
// ponytail: defineProperty needed for getter; stop is plain assignment
|
|
174
|
-
Object.defineProperty(enqueue, "pending", { get: () => queue.length - head });
|
|
175
|
-
return enqueue;
|
|
36
|
+
return createScheduler(options, createRateController({
|
|
37
|
+
min_rpi,
|
|
38
|
+
max_rpi,
|
|
39
|
+
errors_per_interval,
|
|
40
|
+
}, options.rateStrategy ?? linear));
|
|
176
41
|
}
|
|
177
42
|
//# sourceMappingURL=dynamic-throttled-queue.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-throttled-queue.js","sourceRoot":"","sources":["../src/dynamic-throttled-queue.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dynamic-throttled-queue.js","sourceRoot":"","sources":["../src/dynamic-throttled-queue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,6BAA6B;AAC5D,OAAO,EAAE,eAAe,EAAE,uBAAuB;AAmBjD,MAAM,CAAC,MAAM,MAAM,GAAiB,CAAC,EACnC,OAAO,EACP,OAAO,EACP,WAAW,EACX,UAAU,EACV,cAAc,EACd,cAAc,EACd,YAAY,GACb,EAAE,EAAE;IACH,IAAI,UAAU,IAAI,cAAc,EAAE,CAAC;QACjC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;IAC/E,CAAC;IACD,IAAI,CAAC,YAAY,IAAI,UAAU,KAAK,CAAC,IAAI,cAAc,EAAE,CAAC;QACxD,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,CAAC,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC;IAChF,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC;AACzD,CAAC,CAAC;AA+BF,MAAM,UAAU,oBAAoB,CAAC,OAAwB;IAC3D,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,EAAE,WAAW,EAAE,KAAK,GAAG,CAAC,EAAE,iBAAiB,GAAG,GAAG,EAAE,GAAG,OAAO,CAAC;IAE1G,MAAM,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,IAAI,CAAC,CAAC;IAE7D,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,OAAO,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IACD,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACxD,CAAC;IAED,IAAI,WAAW,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,WAAW,GAAG,CAAC,CAAC,EAAE,CAAC;QACrF,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAC5D,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,mBAAmB,CAAC,IAAI,mBAAmB,GAAG,CAAC,EAAE,CAAC;QACtE,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,iBAAiB,CAAC,IAAI,iBAAiB,GAAG,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,eAAe,CAAC,OAAO,EAAE,oBAAoB,CAAC;QACnD,OAAO;QACP,OAAO;QACP,mBAAmB;KACpB,EAAE,OAAO,CAAC,YAAY,IAAI,MAAM,CAAC,CAAC,CAAC;AACtC,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RateStrategy } from "./dynamic-throttled-queue.js";
|
|
2
|
+
type RateControllerOptions = {
|
|
3
|
+
min_rpi: number;
|
|
4
|
+
max_rpi: number;
|
|
5
|
+
errors_per_interval: number;
|
|
6
|
+
};
|
|
7
|
+
type Observation = {
|
|
8
|
+
hasPendingWork: boolean;
|
|
9
|
+
wasBackedOff: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type RateDecision = {
|
|
12
|
+
rate: number;
|
|
13
|
+
shouldBackOff: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type RateController = {
|
|
16
|
+
readonly rate: number;
|
|
17
|
+
recordCompletion: (result: boolean | void) => void;
|
|
18
|
+
observe: (observation: Observation) => RateDecision;
|
|
19
|
+
};
|
|
20
|
+
export declare function createRateController(options: RateControllerOptions, strategy: RateStrategy): RateController;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
function validateDecision(decision) {
|
|
2
|
+
if (typeof decision !== "object" || decision === null) {
|
|
3
|
+
throw new TypeError("rate strategy must return a decision object");
|
|
4
|
+
}
|
|
5
|
+
const candidate = decision;
|
|
6
|
+
if (!Number.isFinite(candidate.nextRate) || !Number.isInteger(candidate.nextRate)) {
|
|
7
|
+
throw new TypeError("rate strategy must return a finite integer nextRate");
|
|
8
|
+
}
|
|
9
|
+
if (typeof candidate.shouldBackOff !== "boolean") {
|
|
10
|
+
throw new TypeError("rate strategy must return a boolean shouldBackOff");
|
|
11
|
+
}
|
|
12
|
+
return candidate;
|
|
13
|
+
}
|
|
14
|
+
export function createRateController(options, strategy) {
|
|
15
|
+
let rate = Math.ceil((options.max_rpi + options.min_rpi) / 2);
|
|
16
|
+
let errorCount = 0;
|
|
17
|
+
return {
|
|
18
|
+
get rate() {
|
|
19
|
+
return rate;
|
|
20
|
+
},
|
|
21
|
+
recordCompletion(result) {
|
|
22
|
+
if (result === false)
|
|
23
|
+
errorCount++;
|
|
24
|
+
},
|
|
25
|
+
observe(observation) {
|
|
26
|
+
const decision = validateDecision(strategy(Object.freeze({
|
|
27
|
+
currentRate: rate,
|
|
28
|
+
minRate: options.min_rpi,
|
|
29
|
+
maxRate: options.max_rpi,
|
|
30
|
+
errorCount,
|
|
31
|
+
errorThreshold: options.errors_per_interval,
|
|
32
|
+
...observation,
|
|
33
|
+
})));
|
|
34
|
+
rate = Math.min(options.max_rpi, Math.max(options.min_rpi, decision.nextRate));
|
|
35
|
+
errorCount = 0;
|
|
36
|
+
return { rate, shouldBackOff: decision.shouldBackOff };
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=rate-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-controller.js","sourceRoot":"","sources":["../src/rate-controller.ts"],"names":[],"mappings":"AAwBA,SAAS,gBAAgB,CAAC,QAAiB;IACzC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtD,MAAM,IAAI,SAAS,CAAC,6CAA6C,CAAC,CAAC;IACrE,CAAC;IACD,MAAM,SAAS,GAAG,QAAgC,CAAC;IACnD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;QAClF,MAAM,IAAI,SAAS,CAAC,qDAAqD,CAAC,CAAC;IAC7E,CAAC;IACD,IAAI,OAAO,SAAS,CAAC,aAAa,KAAK,SAAS,EAAE,CAAC;QACjD,MAAM,IAAI,SAAS,CAAC,mDAAmD,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,OAA8B,EAAE,QAAsB;IACzF,IAAI,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9D,IAAI,UAAU,GAAG,CAAC,CAAC;IAEnB,OAAO;QACL,IAAI,IAAI;YACN,OAAO,IAAI,CAAC;QACd,CAAC;QACD,gBAAgB,CAAC,MAAsB;YACrC,IAAI,MAAM,KAAK,KAAK;gBAAE,UAAU,EAAE,CAAC;QACrC,CAAC;QACD,OAAO,CAAC,WAAwB;YAC9B,MAAM,QAAQ,GAAG,gBAAgB,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;gBACvD,WAAW,EAAE,IAAI;gBACjB,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,UAAU;gBACV,cAAc,EAAE,OAAO,CAAC,mBAAmB;gBAC3C,GAAG,WAAW;aACf,CAAC,CAAC,CAAC,CAAC;YACL,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC/E,UAAU,GAAG,CAAC,CAAC;YACf,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,CAAC,aAAa,EAAE,CAAC;QACzD,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
export function createScheduler(options, rateController) {
|
|
2
|
+
const { interval, evenly_spaced = true, retry = 0, concurrency, compact_threshold = 512, back_off = false, onRateChange, } = options;
|
|
3
|
+
let current_rpi = rateController.rate;
|
|
4
|
+
let dyn_interval = evenly_spaced ? interval / current_rpi : interval;
|
|
5
|
+
let dyn_requests_per_interval = evenly_spaced ? 1 : current_rpi;
|
|
6
|
+
let skippedLast = false;
|
|
7
|
+
let isRunning = false;
|
|
8
|
+
let last_called = 0;
|
|
9
|
+
let timeout;
|
|
10
|
+
let dynTimeout;
|
|
11
|
+
let active_count = 0;
|
|
12
|
+
let isStopped = false;
|
|
13
|
+
let hasStrategyFailure = false;
|
|
14
|
+
let strategyFailure;
|
|
15
|
+
const max_concurrency = concurrency ?? Infinity;
|
|
16
|
+
const queue = [];
|
|
17
|
+
let head = 0;
|
|
18
|
+
function halt() {
|
|
19
|
+
isRunning = false;
|
|
20
|
+
skippedLast = false;
|
|
21
|
+
clearTimeout(timeout);
|
|
22
|
+
timeout = undefined;
|
|
23
|
+
clearTimeout(dynTimeout);
|
|
24
|
+
dynTimeout = undefined;
|
|
25
|
+
if (head >= queue.length) {
|
|
26
|
+
queue.length = 0;
|
|
27
|
+
head = 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function stop() {
|
|
31
|
+
isStopped = true;
|
|
32
|
+
halt();
|
|
33
|
+
}
|
|
34
|
+
function handleResult(item, result) {
|
|
35
|
+
rateController.recordCompletion(result);
|
|
36
|
+
if (result === false && item.retries > 0) {
|
|
37
|
+
queue.push({ fn: item.fn, retries: item.retries - 1 });
|
|
38
|
+
if (!isRunning && !isStopped && queue.length > head)
|
|
39
|
+
start();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function handleSettlement(item, result, resume = false) {
|
|
43
|
+
active_count--;
|
|
44
|
+
handleResult(item, result);
|
|
45
|
+
if (resume && isRunning && !skippedLast && queue.length > head)
|
|
46
|
+
dequeue();
|
|
47
|
+
}
|
|
48
|
+
function dequeue() {
|
|
49
|
+
const threshold = last_called + dyn_interval;
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
if (now < threshold) {
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
timeout = setTimeout(dequeue, threshold - now);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const end = Math.min(head + dyn_requests_per_interval, queue.length);
|
|
57
|
+
let started = 0;
|
|
58
|
+
while (head < end && active_count < max_concurrency) {
|
|
59
|
+
const item = queue[head++];
|
|
60
|
+
active_count++;
|
|
61
|
+
if (started++ === 0)
|
|
62
|
+
last_called = Date.now();
|
|
63
|
+
let result;
|
|
64
|
+
try {
|
|
65
|
+
result = item.fn();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
handleSettlement(item, false);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (result instanceof Promise) {
|
|
72
|
+
void result.then(value => handleSettlement(item, value, true), () => handleSettlement(item, false, true));
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
handleSettlement(item, result);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (head > compact_threshold && head > queue.length / 2) {
|
|
79
|
+
queue.splice(0, head);
|
|
80
|
+
head = 0;
|
|
81
|
+
}
|
|
82
|
+
if (head >= queue.length) {
|
|
83
|
+
halt();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (active_count >= max_concurrency)
|
|
87
|
+
return;
|
|
88
|
+
timeout = setTimeout(dequeue, dyn_interval);
|
|
89
|
+
}
|
|
90
|
+
function applyRate(newRpi) {
|
|
91
|
+
if (newRpi === current_rpi)
|
|
92
|
+
return;
|
|
93
|
+
current_rpi = newRpi;
|
|
94
|
+
onRateChange?.(current_rpi);
|
|
95
|
+
if (evenly_spaced)
|
|
96
|
+
dyn_interval = interval / current_rpi;
|
|
97
|
+
else
|
|
98
|
+
dyn_requests_per_interval = current_rpi;
|
|
99
|
+
}
|
|
100
|
+
function adjustRate() {
|
|
101
|
+
dynTimeout = undefined;
|
|
102
|
+
const wasSkipped = skippedLast;
|
|
103
|
+
skippedLast = false;
|
|
104
|
+
let decision;
|
|
105
|
+
try {
|
|
106
|
+
decision = rateController.observe({ hasPendingWork: queue.length > head, wasBackedOff: wasSkipped });
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
hasStrategyFailure = true;
|
|
110
|
+
strategyFailure = error;
|
|
111
|
+
halt();
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
applyRate(decision.rate);
|
|
115
|
+
if (decision.shouldBackOff && back_off) {
|
|
116
|
+
clearTimeout(timeout);
|
|
117
|
+
skippedLast = true;
|
|
118
|
+
timeout = setTimeout(dequeue, dyn_interval + interval);
|
|
119
|
+
}
|
|
120
|
+
if (isRunning)
|
|
121
|
+
dynTimeout = setTimeout(adjustRate, interval);
|
|
122
|
+
}
|
|
123
|
+
function start() {
|
|
124
|
+
if (skippedLast)
|
|
125
|
+
return;
|
|
126
|
+
isRunning = true;
|
|
127
|
+
last_called = Date.now();
|
|
128
|
+
clearTimeout(timeout);
|
|
129
|
+
timeout = setTimeout(dequeue, dyn_interval);
|
|
130
|
+
if (!dynTimeout)
|
|
131
|
+
dynTimeout = setTimeout(adjustRate, interval);
|
|
132
|
+
}
|
|
133
|
+
function enqueue(callback) {
|
|
134
|
+
if (hasStrategyFailure)
|
|
135
|
+
throw strategyFailure;
|
|
136
|
+
queue.push({ fn: callback, retries: retry });
|
|
137
|
+
isStopped = false;
|
|
138
|
+
if (!isRunning)
|
|
139
|
+
start();
|
|
140
|
+
}
|
|
141
|
+
enqueue.stop = stop;
|
|
142
|
+
Object.defineProperty(enqueue, "pending", { get: () => queue.length - head });
|
|
143
|
+
return enqueue;
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dynamic-throttled-queue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0-rc.1849e68",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Dynamically throttles arbitrary code to execute between a minimum and maximum number of times per interval. Best for making throttled API requests.",
|
|
6
6
|
"files": [
|