rate-budget 0.1.0
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/LICENSE +21 -0
- package/README.md +238 -0
- package/dist/cjs/index.js +21 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/limits.js +185 -0
- package/dist/cjs/limits.js.map +1 -0
- package/dist/cjs/memory-cooldown.js +81 -0
- package/dist/cjs/memory-cooldown.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/retry.js +82 -0
- package/dist/cjs/retry.js.map +1 -0
- package/dist/cjs/token-budget.js +46 -0
- package/dist/cjs/token-budget.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/limits.d.ts +91 -0
- package/dist/esm/limits.d.ts.map +1 -0
- package/dist/esm/limits.js +170 -0
- package/dist/esm/limits.js.map +1 -0
- package/dist/esm/memory-cooldown.d.ts +38 -0
- package/dist/esm/memory-cooldown.d.ts.map +1 -0
- package/dist/esm/memory-cooldown.js +76 -0
- package/dist/esm/memory-cooldown.js.map +1 -0
- package/dist/esm/retry.d.ts +29 -0
- package/dist/esm/retry.d.ts.map +1 -0
- package/dist/esm/retry.js +75 -0
- package/dist/esm/retry.js.map +1 -0
- package/dist/esm/token-budget.d.ts +19 -0
- package/dist/esm/token-budget.d.ts.map +1 -0
- package/dist/esm/token-budget.js +41 -0
- package/dist/esm/token-budget.js.map +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hikuroshi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# rate-budget
|
|
2
|
+
|
|
3
|
+
`rate-budget` is a small, dependency-free toolkit for calculating request rate
|
|
4
|
+
limits, quota thresholds, token or cost budgets, cooldowns, and retries.
|
|
5
|
+
|
|
6
|
+
It is provider-agnostic and storage-agnostic. The package does not manage your
|
|
7
|
+
database, Redis keys, queues, locks, or HTTP clients. Your application owns the
|
|
8
|
+
state; `rate-budget` only calculates deterministic decisions from the counters
|
|
9
|
+
and events you pass in.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install rate-budget
|
|
15
|
+
pnpm add rate-budget
|
|
16
|
+
yarn add rate-budget
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- Request spacing for limits such as RPM, RPH, or any count-per-window limit.
|
|
22
|
+
- Sliding-window cost checks for tokens, bytes, credits, points, or custom units.
|
|
23
|
+
- Daily quota thresholds such as RPD with configurable safety percentages.
|
|
24
|
+
- Per-request token or cost budget planning.
|
|
25
|
+
- Flexible budget splitting for context, input, output, metadata, and similar allocations.
|
|
26
|
+
- Lightweight in-memory actor cooldowns for single-process use cases.
|
|
27
|
+
- Async retry helper with configurable status codes, message fragments, and delays.
|
|
28
|
+
- ESM and CommonJS exports.
|
|
29
|
+
- TypeScript declarations.
|
|
30
|
+
- Zero runtime dependencies.
|
|
31
|
+
|
|
32
|
+
## Imports
|
|
33
|
+
|
|
34
|
+
ESM:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import {
|
|
38
|
+
buildRequestLimitProfile,
|
|
39
|
+
calculateWindowUsageWaitMs,
|
|
40
|
+
} from "rate-budget";
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
CommonJS:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
const {
|
|
47
|
+
buildRequestLimitProfile,
|
|
48
|
+
calculateWindowUsageWaitMs,
|
|
49
|
+
} = require("rate-budget");
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Core Model
|
|
53
|
+
|
|
54
|
+
`rate-budget` is designed around pure calculations:
|
|
55
|
+
|
|
56
|
+
1. Read current usage and recent events from your own storage.
|
|
57
|
+
2. Pass those values into `rate-budget`.
|
|
58
|
+
3. If the decision requires waiting, delay or enqueue in your application.
|
|
59
|
+
4. If the request is allowed, reserve or log the request atomically.
|
|
60
|
+
5. After the request finishes, update usage in your storage.
|
|
61
|
+
|
|
62
|
+
This keeps the package lightweight while still fitting distributed systems,
|
|
63
|
+
serverless applications, workers, bots, and API gateways.
|
|
64
|
+
|
|
65
|
+
## RPM, TPM, and RPD Example
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import {
|
|
69
|
+
buildRequestLimitProfile,
|
|
70
|
+
calculateCooldownWaitMs,
|
|
71
|
+
calculateWindowUsageWaitMs,
|
|
72
|
+
evaluateCountLimit,
|
|
73
|
+
} from "rate-budget";
|
|
74
|
+
|
|
75
|
+
const profile = buildRequestLimitProfile({
|
|
76
|
+
rpmLimit: 15,
|
|
77
|
+
tpmLimit: 250_000,
|
|
78
|
+
rpdLimit: 500,
|
|
79
|
+
dailyThresholdPercent: 90,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const dailyDecision = evaluateCountLimit({
|
|
83
|
+
usedCount: usageToday.requestCount,
|
|
84
|
+
limit: profile.rpdLimit,
|
|
85
|
+
thresholdPercent: profile.dailyThresholdPercent,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!dailyDecision.allowed) {
|
|
89
|
+
throw new Error("Daily quota threshold reached.");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const cooldownWaitMs = calculateCooldownWaitMs({
|
|
93
|
+
lastAcceptedAt: lastApiRequest?.createdAt,
|
|
94
|
+
cooldownMs: profile.cooldownMs,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const tokenWaitMs = calculateWindowUsageWaitMs({
|
|
98
|
+
incomingCost: estimatedRequestTokens,
|
|
99
|
+
limit: profile.tpmLimit,
|
|
100
|
+
entries: recentApiRequests.map((request) => ({
|
|
101
|
+
occurredAt: request.createdAt,
|
|
102
|
+
cost: request.totalTokens,
|
|
103
|
+
})),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const waitMs = Math.max(cooldownWaitMs, tokenWaitMs);
|
|
107
|
+
|
|
108
|
+
if (waitMs > 0) {
|
|
109
|
+
await waitOrQueue(waitMs);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Sliding-Window Cost Limit
|
|
114
|
+
|
|
115
|
+
Use `evaluateWindowUsage` when you need the full decision, or
|
|
116
|
+
`calculateWindowUsageWaitMs` when you only need the wait time.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { evaluateWindowUsage } from "rate-budget";
|
|
120
|
+
|
|
121
|
+
const decision = evaluateWindowUsage({
|
|
122
|
+
incomingCost: 500,
|
|
123
|
+
limit: 10_000,
|
|
124
|
+
windowMs: 60_000,
|
|
125
|
+
safetyBufferMs: 1_000,
|
|
126
|
+
entries: [
|
|
127
|
+
{ occurredAt: "2026-05-21 14:00:10", cost: 4_000 },
|
|
128
|
+
{ occurredAt: "2026-05-21 14:00:30", cost: 6_200 },
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!decision.allowed && decision.retryAfterMs !== null) {
|
|
133
|
+
await delay(decision.retryAfterMs);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`cost` can represent tokens, weighted requests, bytes, credits, or any other
|
|
138
|
+
unit that should be limited over time.
|
|
139
|
+
|
|
140
|
+
## Token Budget Planning
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import {
|
|
144
|
+
calculatePerRequestTokenBudget,
|
|
145
|
+
calculateTokenBudgetPortion,
|
|
146
|
+
distributeFlexibleTokenBudget,
|
|
147
|
+
} from "rate-budget";
|
|
148
|
+
|
|
149
|
+
const totalBudget = calculatePerRequestTokenBudget({
|
|
150
|
+
tokensPerWindow: 250_000,
|
|
151
|
+
requestSpacingMs: 5_000,
|
|
152
|
+
minimumTokens: 96,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const inputBudget = calculateTokenBudgetPortion(totalBudget, 0.5);
|
|
156
|
+
|
|
157
|
+
const flexible = distributeFlexibleTokenBudget({
|
|
158
|
+
parentBudget: inputBudget,
|
|
159
|
+
availableTokens: inputBudget - systemPromptTokens - userPromptTokens,
|
|
160
|
+
partitions: [
|
|
161
|
+
{ name: "context", targetRatio: 0.55, maxRatio: 0.6 },
|
|
162
|
+
{ name: "metadata", targetRatio: 0.15, maxRatio: 0.2 },
|
|
163
|
+
],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
console.log(flexible.context, flexible.metadata);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## In-Memory Actor Cooldowns
|
|
170
|
+
|
|
171
|
+
Use `InMemoryActorCooldownLimiter` for local process cooldowns, such as UX
|
|
172
|
+
cooldowns in a bot or simple API server. For multi-instance systems, store the
|
|
173
|
+
cooldown state in Redis or a database and use the pure helpers instead.
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { InMemoryActorCooldownLimiter } from "rate-budget";
|
|
177
|
+
|
|
178
|
+
const limiter = new InMemoryActorCooldownLimiter({
|
|
179
|
+
sameActorCooldownMs: 3_000,
|
|
180
|
+
differentActorCooldownMs: 1_000,
|
|
181
|
+
stateTtlMs: 5 * 60_000,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const decision = limiter.consume("guild:123", "user:456");
|
|
185
|
+
|
|
186
|
+
if (!decision.allowed) {
|
|
187
|
+
console.log(`Retry after ${decision.retryAfterMs}ms`);
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Retry
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import { retryAsync, isRetryableError } from "rate-budget";
|
|
195
|
+
|
|
196
|
+
const response = await retryAsync({
|
|
197
|
+
retryAttempts: 2,
|
|
198
|
+
delayMs: ({ attemptIndex }) => 1_000 * (attemptIndex + 1),
|
|
199
|
+
shouldRetry: (error) =>
|
|
200
|
+
isRetryableError(error, {
|
|
201
|
+
statusCodes: [429, 500, 502, 503, 504],
|
|
202
|
+
messageIncludes: ["rate limit", "temporarily unavailable"],
|
|
203
|
+
}),
|
|
204
|
+
task: () => fetchProvider(),
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## API Overview
|
|
209
|
+
|
|
210
|
+
- `buildRequestLimitProfile(input)`: creates a normalized RPM, TPM, RPD, threshold, cooldown, and inactive cooldown profile.
|
|
211
|
+
- `calculateLimitSpacingMs(limitPerWindow, options)`: calculates safe request spacing inside a time window.
|
|
212
|
+
- `calculateInactiveLimitSpacingMs(limitPerWindow, options)`: calculates inactive cooldown spacing with a multiplier.
|
|
213
|
+
- `calculateThresholdCount(limit, thresholdPercent)`: calculates a safe quota threshold.
|
|
214
|
+
- `evaluateCountLimit(input)`: evaluates count-based quota usage.
|
|
215
|
+
- `calculateCooldownWaitMs(input)`: calculates remaining cooldown from the last accepted timestamp.
|
|
216
|
+
- `evaluateWindowUsage(input)`: evaluates sliding-window cost usage and returns a detailed decision.
|
|
217
|
+
- `calculateWindowUsageWaitMs(input)`: returns only the wait time for sliding-window cost usage.
|
|
218
|
+
- `calculatePerRequestTokenBudget(input)`: calculates cost or token budget per request.
|
|
219
|
+
- `calculateTokenBudgetPortion(totalBudget, ratio)`: returns a ratio-based budget portion.
|
|
220
|
+
- `distributeFlexibleTokenBudget(input)`: allocates flexible budget across named partitions.
|
|
221
|
+
- `InMemoryActorCooldownLimiter`: local in-memory cooldown per scope and actor.
|
|
222
|
+
- `retryAsync(input)`: retries an async task.
|
|
223
|
+
- `isRetryableError(error, options)`: checks status codes and message fragments.
|
|
224
|
+
- `delay(ms)`: small promise-based delay helper.
|
|
225
|
+
|
|
226
|
+
## Build and Test
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
pnpm --filter rate-budget build
|
|
230
|
+
pnpm --filter rate-budget test
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
From the package directory:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
npm run build
|
|
237
|
+
npm test
|
|
238
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./limits.js"), exports);
|
|
18
|
+
__exportStar(require("./memory-cooldown.js"), exports);
|
|
19
|
+
__exportStar(require("./retry.js"), exports);
|
|
20
|
+
__exportStar(require("./token-budget.js"), exports);
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,8CAA4B;AAC5B,uDAAqC;AACrC,6CAA2B;AAC3B,oDAAkC"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_INACTIVE_COOLDOWN_MULTIPLIER = exports.DEFAULT_LIMIT_SAFETY_BUFFER_MS = exports.DEFAULT_LIMIT_WINDOW_MS = void 0;
|
|
4
|
+
exports.toPositiveInteger = toPositiveInteger;
|
|
5
|
+
exports.toNonNegativeInteger = toNonNegativeInteger;
|
|
6
|
+
exports.clampPercent = clampPercent;
|
|
7
|
+
exports.toTimestampMs = toTimestampMs;
|
|
8
|
+
exports.calculateLimitSpacingMs = calculateLimitSpacingMs;
|
|
9
|
+
exports.calculateInactiveLimitSpacingMs = calculateInactiveLimitSpacingMs;
|
|
10
|
+
exports.calculateThresholdCount = calculateThresholdCount;
|
|
11
|
+
exports.evaluateCountLimit = evaluateCountLimit;
|
|
12
|
+
exports.calculateCooldownWaitMs = calculateCooldownWaitMs;
|
|
13
|
+
exports.evaluateWindowUsage = evaluateWindowUsage;
|
|
14
|
+
exports.calculateWindowUsageWaitMs = calculateWindowUsageWaitMs;
|
|
15
|
+
exports.buildRequestLimitProfile = buildRequestLimitProfile;
|
|
16
|
+
exports.DEFAULT_LIMIT_WINDOW_MS = 60000;
|
|
17
|
+
exports.DEFAULT_LIMIT_SAFETY_BUFFER_MS = 1000;
|
|
18
|
+
exports.DEFAULT_INACTIVE_COOLDOWN_MULTIPLIER = 2;
|
|
19
|
+
function normalizeFiniteNumber(value, fallback) {
|
|
20
|
+
return Number.isFinite(value) ? value : fallback;
|
|
21
|
+
}
|
|
22
|
+
function toPositiveInteger(value, fallback = 1) {
|
|
23
|
+
return Math.max(1, Math.floor(normalizeFiniteNumber(value, fallback)));
|
|
24
|
+
}
|
|
25
|
+
function toNonNegativeInteger(value, fallback = 0) {
|
|
26
|
+
return Math.max(0, Math.floor(normalizeFiniteNumber(value, fallback)));
|
|
27
|
+
}
|
|
28
|
+
function clampPercent(value, fallback = 100) {
|
|
29
|
+
const safeValue = normalizeFiniteNumber(value, fallback);
|
|
30
|
+
return Math.min(100, Math.max(0, safeValue));
|
|
31
|
+
}
|
|
32
|
+
function toTimestampMs(value) {
|
|
33
|
+
if (value === null || value === undefined) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === "number") {
|
|
37
|
+
return Number.isFinite(value) ? value : null;
|
|
38
|
+
}
|
|
39
|
+
const timestamp = value instanceof Date ? value.getTime() : Date.parse(value);
|
|
40
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
41
|
+
}
|
|
42
|
+
function calculateLimitSpacingMs(limitPerWindow, options = {}) {
|
|
43
|
+
const limit = toPositiveInteger(limitPerWindow);
|
|
44
|
+
const windowMs = toPositiveInteger(options.windowMs ?? exports.DEFAULT_LIMIT_WINDOW_MS, exports.DEFAULT_LIMIT_WINDOW_MS);
|
|
45
|
+
const safetyBufferMs = toNonNegativeInteger(options.safetyBufferMs ?? exports.DEFAULT_LIMIT_SAFETY_BUFFER_MS, exports.DEFAULT_LIMIT_SAFETY_BUFFER_MS);
|
|
46
|
+
return Math.ceil(windowMs / limit + safetyBufferMs);
|
|
47
|
+
}
|
|
48
|
+
function calculateInactiveLimitSpacingMs(limitPerWindow, options = {}) {
|
|
49
|
+
const multiplier = Math.max(1, normalizeFiniteNumber(options.multiplier ?? exports.DEFAULT_INACTIVE_COOLDOWN_MULTIPLIER, exports.DEFAULT_INACTIVE_COOLDOWN_MULTIPLIER));
|
|
50
|
+
return Math.ceil(calculateLimitSpacingMs(limitPerWindow, options) * multiplier);
|
|
51
|
+
}
|
|
52
|
+
function calculateThresholdCount(limit, thresholdPercent = 100) {
|
|
53
|
+
const safeLimit = toPositiveInteger(limit);
|
|
54
|
+
const safeThresholdPercent = clampPercent(thresholdPercent);
|
|
55
|
+
return Math.max(1, Math.ceil(safeLimit * (safeThresholdPercent / 100)));
|
|
56
|
+
}
|
|
57
|
+
function evaluateCountLimit(input) {
|
|
58
|
+
const usedCount = toNonNegativeInteger(input.usedCount);
|
|
59
|
+
const thresholdCount = calculateThresholdCount(input.limit, input.thresholdPercent);
|
|
60
|
+
const remainingCount = Math.max(0, thresholdCount - usedCount);
|
|
61
|
+
if (remainingCount <= 0) {
|
|
62
|
+
return {
|
|
63
|
+
allowed: false,
|
|
64
|
+
usedCount,
|
|
65
|
+
thresholdCount,
|
|
66
|
+
remainingCount: 0,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
allowed: true,
|
|
71
|
+
usedCount,
|
|
72
|
+
thresholdCount,
|
|
73
|
+
remainingCount,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function calculateCooldownWaitMs(input) {
|
|
77
|
+
const lastAcceptedAt = toTimestampMs(input.lastAcceptedAt);
|
|
78
|
+
if (lastAcceptedAt === null) {
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
const now = normalizeFiniteNumber(input.now ?? Date.now(), Date.now());
|
|
82
|
+
const cooldownMs = toNonNegativeInteger(input.cooldownMs);
|
|
83
|
+
return Math.max(0, cooldownMs - (now - lastAcceptedAt));
|
|
84
|
+
}
|
|
85
|
+
function normalizeWindowEntries(entries, entriesSortedOldestFirst, input) {
|
|
86
|
+
const normalized = entries.flatMap((entry) => {
|
|
87
|
+
const occurredAtMs = toTimestampMs(entry.occurredAt);
|
|
88
|
+
if (occurredAtMs === null ||
|
|
89
|
+
occurredAtMs < input.now - input.windowMs ||
|
|
90
|
+
occurredAtMs > input.now) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
return [
|
|
94
|
+
{
|
|
95
|
+
occurredAtMs,
|
|
96
|
+
cost: toNonNegativeInteger(entry.cost),
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
});
|
|
100
|
+
if (entriesSortedOldestFirst) {
|
|
101
|
+
return normalized;
|
|
102
|
+
}
|
|
103
|
+
return normalized.sort((left, right) => left.occurredAtMs - right.occurredAtMs);
|
|
104
|
+
}
|
|
105
|
+
function evaluateWindowUsage(input) {
|
|
106
|
+
const limit = toPositiveInteger(input.limit);
|
|
107
|
+
const incomingCost = toNonNegativeInteger(input.incomingCost);
|
|
108
|
+
const now = normalizeFiniteNumber(input.now ?? Date.now(), Date.now());
|
|
109
|
+
const windowMs = toPositiveInteger(input.windowMs ?? exports.DEFAULT_LIMIT_WINDOW_MS, exports.DEFAULT_LIMIT_WINDOW_MS);
|
|
110
|
+
const entries = normalizeWindowEntries(input.entries, input.entriesSortedOldestFirst ?? true, { now, windowMs });
|
|
111
|
+
const currentCost = input.currentCost === undefined
|
|
112
|
+
? entries.reduce((sum, entry) => sum + entry.cost, 0)
|
|
113
|
+
: toNonNegativeInteger(input.currentCost);
|
|
114
|
+
const projectedCost = currentCost + incomingCost;
|
|
115
|
+
if (incomingCost > limit) {
|
|
116
|
+
return {
|
|
117
|
+
allowed: false,
|
|
118
|
+
reason: "single_request_exceeds_limit",
|
|
119
|
+
projectedCost,
|
|
120
|
+
limit,
|
|
121
|
+
retryAfterMs: null,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (projectedCost <= limit) {
|
|
125
|
+
return {
|
|
126
|
+
allowed: true,
|
|
127
|
+
reason: "within_limit",
|
|
128
|
+
projectedCost,
|
|
129
|
+
limit,
|
|
130
|
+
retryAfterMs: 0,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const safetyBufferMs = toNonNegativeInteger(input.safetyBufferMs ?? exports.DEFAULT_LIMIT_SAFETY_BUFFER_MS, exports.DEFAULT_LIMIT_SAFETY_BUFFER_MS);
|
|
134
|
+
let remainingCost = projectedCost;
|
|
135
|
+
for (const entry of entries) {
|
|
136
|
+
remainingCost -= entry.cost;
|
|
137
|
+
if (remainingCost <= limit) {
|
|
138
|
+
return {
|
|
139
|
+
allowed: false,
|
|
140
|
+
reason: "window_exhausted",
|
|
141
|
+
projectedCost,
|
|
142
|
+
limit,
|
|
143
|
+
retryAfterMs: Math.max(0, windowMs - (now - entry.occurredAtMs) + safetyBufferMs),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
allowed: false,
|
|
149
|
+
reason: "window_exhausted",
|
|
150
|
+
projectedCost,
|
|
151
|
+
limit,
|
|
152
|
+
retryAfterMs: windowMs,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function calculateWindowUsageWaitMs(input) {
|
|
156
|
+
const decision = evaluateWindowUsage(input);
|
|
157
|
+
if (decision.allowed) {
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
return (decision.retryAfterMs ??
|
|
161
|
+
toPositiveInteger(input.windowMs ?? exports.DEFAULT_LIMIT_WINDOW_MS, exports.DEFAULT_LIMIT_WINDOW_MS));
|
|
162
|
+
}
|
|
163
|
+
function buildRequestLimitProfile(input) {
|
|
164
|
+
const rpmLimit = toPositiveInteger(input.rpmLimit);
|
|
165
|
+
const tpmLimit = toPositiveInteger(input.tpmLimit);
|
|
166
|
+
const rpdLimit = toPositiveInteger(input.rpdLimit);
|
|
167
|
+
const dailyThresholdPercent = clampPercent(input.dailyThresholdPercent ?? 100);
|
|
168
|
+
const cooldownOptions = {
|
|
169
|
+
windowMs: input.windowMs,
|
|
170
|
+
safetyBufferMs: input.safetyBufferMs,
|
|
171
|
+
};
|
|
172
|
+
return {
|
|
173
|
+
rpmLimit,
|
|
174
|
+
tpmLimit,
|
|
175
|
+
rpdLimit,
|
|
176
|
+
dailyThresholdPercent,
|
|
177
|
+
dailyThresholdRequests: calculateThresholdCount(rpdLimit, dailyThresholdPercent),
|
|
178
|
+
cooldownMs: calculateLimitSpacingMs(rpmLimit, cooldownOptions),
|
|
179
|
+
inactiveCooldownMs: calculateInactiveLimitSpacingMs(rpmLimit, {
|
|
180
|
+
...cooldownOptions,
|
|
181
|
+
multiplier: input.inactiveCooldownMultiplier,
|
|
182
|
+
}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=limits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.js","sourceRoot":"","sources":["../../src/limits.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InMemoryActorCooldownLimiter = void 0;
|
|
4
|
+
exports.consumeActorCooldown = consumeActorCooldown;
|
|
5
|
+
const limits_js_1 = require("./limits.js");
|
|
6
|
+
function consumeActorCooldown(currentState, input) {
|
|
7
|
+
const now = Number.isFinite(input.now) ? input.now ?? Date.now() : Date.now();
|
|
8
|
+
if (!currentState) {
|
|
9
|
+
return {
|
|
10
|
+
allowed: true,
|
|
11
|
+
state: {
|
|
12
|
+
lastAcceptedAt: now,
|
|
13
|
+
lastActorId: input.actorId,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const requiredCooldownMs = currentState.lastActorId === input.actorId
|
|
18
|
+
? (0, limits_js_1.toNonNegativeInteger)(input.sameActorCooldownMs)
|
|
19
|
+
: (0, limits_js_1.toNonNegativeInteger)(input.differentActorCooldownMs);
|
|
20
|
+
const elapsedMs = now - currentState.lastAcceptedAt;
|
|
21
|
+
if (elapsedMs < requiredCooldownMs) {
|
|
22
|
+
return {
|
|
23
|
+
allowed: false,
|
|
24
|
+
retryAfterMs: requiredCooldownMs - elapsedMs,
|
|
25
|
+
state: currentState,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
allowed: true,
|
|
30
|
+
state: {
|
|
31
|
+
lastAcceptedAt: now,
|
|
32
|
+
lastActorId: input.actorId,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
class InMemoryActorCooldownLimiter {
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.states = new Map();
|
|
39
|
+
this.touches = 0;
|
|
40
|
+
this.sameActorCooldownMs = (0, limits_js_1.toNonNegativeInteger)(options.sameActorCooldownMs);
|
|
41
|
+
this.differentActorCooldownMs = (0, limits_js_1.toNonNegativeInteger)(options.differentActorCooldownMs);
|
|
42
|
+
this.stateTtlMs = (0, limits_js_1.toNonNegativeInteger)(options.stateTtlMs ?? 5 * 60000);
|
|
43
|
+
this.cleanupInterval = Math.max(1, (0, limits_js_1.toNonNegativeInteger)(options.cleanupInterval ?? 256, 256));
|
|
44
|
+
}
|
|
45
|
+
consume(scopeId, actorId, now = Date.now()) {
|
|
46
|
+
this.cleanup(now);
|
|
47
|
+
const decision = consumeActorCooldown(this.states.get(scopeId), {
|
|
48
|
+
actorId,
|
|
49
|
+
now,
|
|
50
|
+
sameActorCooldownMs: this.sameActorCooldownMs,
|
|
51
|
+
differentActorCooldownMs: this.differentActorCooldownMs,
|
|
52
|
+
});
|
|
53
|
+
if (decision.allowed) {
|
|
54
|
+
this.states.set(scopeId, decision.state);
|
|
55
|
+
}
|
|
56
|
+
return decision;
|
|
57
|
+
}
|
|
58
|
+
reset(scopeId) {
|
|
59
|
+
if (scopeId) {
|
|
60
|
+
this.states.delete(scopeId);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.states.clear();
|
|
64
|
+
}
|
|
65
|
+
size() {
|
|
66
|
+
return this.states.size;
|
|
67
|
+
}
|
|
68
|
+
cleanup(now) {
|
|
69
|
+
this.touches += 1;
|
|
70
|
+
if (this.touches % this.cleanupInterval !== 0) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
for (const [scopeId, state] of this.states.entries()) {
|
|
74
|
+
if (now - state.lastAcceptedAt > this.stateTtlMs) {
|
|
75
|
+
this.states.delete(scopeId);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.InMemoryActorCooldownLimiter = InMemoryActorCooldownLimiter;
|
|
81
|
+
//# sourceMappingURL=memory-cooldown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-cooldown.js","sourceRoot":"","sources":["../../src/memory-cooldown.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.delay = delay;
|
|
4
|
+
exports.getErrorStatus = getErrorStatus;
|
|
5
|
+
exports.getErrorMessage = getErrorMessage;
|
|
6
|
+
exports.isRetryableError = isRetryableError;
|
|
7
|
+
exports.retryAsync = retryAsync;
|
|
8
|
+
const limits_js_1 = require("./limits.js");
|
|
9
|
+
function delay(ms) {
|
|
10
|
+
const safeMs = (0, limits_js_1.toNonNegativeInteger)(ms);
|
|
11
|
+
if (safeMs <= 0) {
|
|
12
|
+
return Promise.resolve();
|
|
13
|
+
}
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
setTimeout(resolve, safeMs);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function getErrorStatus(error) {
|
|
19
|
+
if (!error || typeof error !== "object" || !("status" in error)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const status = error.status;
|
|
23
|
+
return typeof status === "number" && Number.isFinite(status) ? status : null;
|
|
24
|
+
}
|
|
25
|
+
function getErrorMessage(error) {
|
|
26
|
+
if (error instanceof Error) {
|
|
27
|
+
return error.message;
|
|
28
|
+
}
|
|
29
|
+
if (typeof error === "string") {
|
|
30
|
+
return error;
|
|
31
|
+
}
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
function isRetryableError(error, options = {}) {
|
|
35
|
+
const statusCodes = options.statusCodes ?? [429, 500, 503];
|
|
36
|
+
const status = getErrorStatus(error);
|
|
37
|
+
if (status !== null && statusCodes.includes(status)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
const message = getErrorMessage(error).toLowerCase();
|
|
41
|
+
return (options.messageIncludes ?? []).some((fragment) => message.includes(fragment.toLowerCase()));
|
|
42
|
+
}
|
|
43
|
+
function resolveRetryDelay(delayMs, input) {
|
|
44
|
+
if (typeof delayMs === "function") {
|
|
45
|
+
return (0, limits_js_1.toNonNegativeInteger)(delayMs(input));
|
|
46
|
+
}
|
|
47
|
+
return (0, limits_js_1.toNonNegativeInteger)(delayMs ?? 0);
|
|
48
|
+
}
|
|
49
|
+
async function retryAsync(input) {
|
|
50
|
+
const retryAttempts = (0, limits_js_1.toNonNegativeInteger)(input.retryAttempts);
|
|
51
|
+
const maxAttempts = retryAttempts + 1;
|
|
52
|
+
let lastError = null;
|
|
53
|
+
for (let attemptIndex = 0; attemptIndex < maxAttempts; attemptIndex += 1) {
|
|
54
|
+
try {
|
|
55
|
+
return await input.task({ attemptIndex });
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
lastError = error;
|
|
59
|
+
if (attemptIndex >= maxAttempts - 1) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
const shouldRetry = input.shouldRetry
|
|
63
|
+
? input.shouldRetry(error, { attemptIndex })
|
|
64
|
+
: isRetryableError(error);
|
|
65
|
+
if (!shouldRetry) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
const nextDelayMs = resolveRetryDelay(input.delayMs, {
|
|
69
|
+
attemptIndex,
|
|
70
|
+
error,
|
|
71
|
+
});
|
|
72
|
+
await input.onRetry?.({
|
|
73
|
+
attemptIndex,
|
|
74
|
+
nextDelayMs,
|
|
75
|
+
error,
|
|
76
|
+
});
|
|
77
|
+
await delay(nextDelayMs);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
throw lastError;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=retry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/retry.ts"],"names":[],"mappings":"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"}
|