kitcn 0.16.0 → 0.17.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/dist/aggregate/index.d.ts +1 -1
- package/dist/auth/client/index.js +1 -1
- package/dist/auth/index.js +19 -21
- package/dist/auth/nextjs/index.d.ts +1 -1
- package/dist/auth/nextjs/index.js +4 -4
- package/dist/{auth-store-ssZDPa37.js → auth-store-BnGZxmnY.js} +4 -1
- package/dist/{backend-core-DqPydYyx.mjs → backend-core-BsKP1LVg.mjs} +204 -164
- package/dist/{builder-DBgto1yn.js → builder-f4F_NRvK.js} +245 -153
- package/dist/{caller-factory-NEfgD5E0.js → caller-factory-DHywSoGZ.js} +7 -5
- package/dist/cli.mjs +14 -7
- package/dist/crpc/index.js +1 -127
- package/dist/{middleware-Bg-PdtrI.js → middleware-Cgrv2jIu.js} +1 -1
- package/dist/orm/index.d.ts +1 -1
- package/dist/orm/index.js +486 -121
- package/dist/plugins/index.js +1 -1
- package/dist/{procedure-caller-9m6NBxQu.js → procedure-caller-Rj6z3ai7.js} +1 -1
- package/dist/{procedure-name-Cy1AxayA.d.ts → procedure-name-Bo5KMcqc.d.ts} +15 -3
- package/dist/{query-context-ydn9kb6P.js → query-context-C90vNlc9.js} +131 -30
- package/dist/query-options-C_eBSIXG.js +247 -0
- package/dist/ratelimit/index.d.ts +26 -6
- package/dist/ratelimit/index.js +427 -100
- package/dist/ratelimit/react/index.d.ts +14 -0
- package/dist/ratelimit/react/index.js +149 -16
- package/dist/react/index.d.ts +3 -1
- package/dist/react/index.js +48 -15
- package/dist/rsc/index.js +22 -33
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +3 -3
- package/dist/solid/index.js +19 -5
- package/dist/watcher.mjs +2 -2
- package/dist/{where-clause-compiler-WF9UcrAB.d.ts → where-clause-compiler-BRhLW1dp.d.ts} +51 -0
- package/package.json +1 -1
- package/skills/kitcn/SKILL.md +1 -0
- package/skills/kitcn/references/features/create-plugins.md +1 -1
- package/skills/kitcn/references/features/orm.md +11 -1
- package/skills/kitcn/references/features/ratelimit.md +105 -0
- package/skills/kitcn/references/setup/server.md +1 -1
- package/dist/query-options-C96zLANM.js +0 -121
package/dist/ratelimit/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { u as requireMutationCtx } from "../api-entry-N3nBOlI2.js";
|
|
2
|
-
import { _ as CRPCError } from "../builder-
|
|
3
|
-
import { t as definePlugin } from "../middleware-
|
|
2
|
+
import { _ as CRPCError } from "../builder-f4F_NRvK.js";
|
|
3
|
+
import { t as definePlugin } from "../middleware-Cgrv2jIu.js";
|
|
4
4
|
import { v } from "convex/values";
|
|
5
5
|
import { mutationGeneric, queryGeneric } from "convex/server";
|
|
6
6
|
|
|
@@ -30,58 +30,121 @@ function toMs(duration) {
|
|
|
30
30
|
const DEFAULT_SHARDS = 1;
|
|
31
31
|
function fixedWindow(limit, window, options) {
|
|
32
32
|
validatePositive(limit, "limit");
|
|
33
|
-
const shards = normalizeShards(options?.shards);
|
|
34
33
|
const capacity = options?.capacity ?? limit;
|
|
35
34
|
validatePositive(capacity, "capacity");
|
|
36
|
-
|
|
35
|
+
validateMaxReserved(options?.maxReserved);
|
|
36
|
+
return assertShardBudget({
|
|
37
37
|
kind: "fixedWindow",
|
|
38
38
|
limit,
|
|
39
39
|
window: toMs(window),
|
|
40
40
|
capacity,
|
|
41
41
|
maxReserved: options?.maxReserved,
|
|
42
42
|
start: options?.start,
|
|
43
|
-
shards
|
|
44
|
-
};
|
|
43
|
+
shards: normalizeShards(options?.shards)
|
|
44
|
+
});
|
|
45
45
|
}
|
|
46
46
|
function slidingWindow(limit, window, options) {
|
|
47
47
|
validatePositive(limit, "limit");
|
|
48
|
-
|
|
48
|
+
validateMaxReserved(options?.maxReserved);
|
|
49
|
+
return assertShardBudget({
|
|
49
50
|
kind: "slidingWindow",
|
|
50
51
|
limit,
|
|
51
52
|
window: toMs(window),
|
|
52
53
|
maxReserved: options?.maxReserved,
|
|
53
54
|
shards: normalizeShards(options?.shards)
|
|
54
|
-
};
|
|
55
|
+
});
|
|
55
56
|
}
|
|
56
57
|
function tokenBucket(refillRate, interval, maxTokens, options) {
|
|
57
58
|
validatePositive(refillRate, "refillRate");
|
|
58
59
|
validatePositive(maxTokens, "maxTokens");
|
|
59
|
-
|
|
60
|
+
validateMaxReserved(options?.maxReserved);
|
|
61
|
+
return assertShardBudget({
|
|
60
62
|
kind: "tokenBucket",
|
|
61
63
|
refillRate,
|
|
62
64
|
interval: toMs(interval),
|
|
63
65
|
maxTokens,
|
|
64
66
|
maxReserved: options?.maxReserved,
|
|
65
67
|
shards: normalizeShards(options?.shards)
|
|
66
|
-
};
|
|
68
|
+
});
|
|
67
69
|
}
|
|
68
70
|
function applyDynamicLimit(algorithm, dynamicLimit) {
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
+
if (dynamicLimit === null) return algorithm;
|
|
72
|
+
validatePositive(dynamicLimit, "dynamicLimit");
|
|
73
|
+
if (algorithm.kind === "tokenBucket") return assertShardBudget({
|
|
71
74
|
...algorithm,
|
|
72
75
|
refillRate: dynamicLimit,
|
|
73
76
|
maxTokens: algorithm.maxTokens === algorithm.refillRate ? dynamicLimit : algorithm.maxTokens
|
|
74
|
-
};
|
|
75
|
-
if (algorithm.kind === "fixedWindow") return {
|
|
77
|
+
});
|
|
78
|
+
if (algorithm.kind === "fixedWindow") return assertShardBudget({
|
|
76
79
|
...algorithm,
|
|
77
80
|
limit: dynamicLimit,
|
|
78
81
|
capacity: algorithm.capacity === algorithm.limit ? dynamicLimit : algorithm.capacity
|
|
82
|
+
});
|
|
83
|
+
return assertShardBudget({
|
|
84
|
+
...algorithm,
|
|
85
|
+
limit: dynamicLimit
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Narrow the configured budget down to the slice one shard owns.
|
|
90
|
+
*
|
|
91
|
+
* Every shard stores its own row and spends only what it owns, so without this
|
|
92
|
+
* the effective limit would be multiplied by the shard count. Shares are dealt
|
|
93
|
+
* so they add back up to the configured budget exactly — see {@link shardShare}.
|
|
94
|
+
*/
|
|
95
|
+
function shardAlgorithm(algorithm, shard) {
|
|
96
|
+
const { shards } = algorithm;
|
|
97
|
+
if (shards <= 1) return algorithm;
|
|
98
|
+
const share = (value) => shardShare(value, shards, shard);
|
|
99
|
+
const maxReserved = algorithm.maxReserved === void 0 ? void 0 : share(algorithm.maxReserved);
|
|
100
|
+
if (algorithm.kind === "tokenBucket") {
|
|
101
|
+
const maxTokens = share(algorithm.maxTokens);
|
|
102
|
+
return {
|
|
103
|
+
...algorithm,
|
|
104
|
+
refillRate: algorithm.refillRate * (maxTokens / algorithm.maxTokens),
|
|
105
|
+
maxTokens,
|
|
106
|
+
maxReserved,
|
|
107
|
+
shards: 1
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (algorithm.kind === "fixedWindow") return {
|
|
111
|
+
...algorithm,
|
|
112
|
+
limit: share(algorithm.limit),
|
|
113
|
+
capacity: share(algorithm.capacity),
|
|
114
|
+
maxReserved,
|
|
115
|
+
shards: 1
|
|
79
116
|
};
|
|
80
117
|
return {
|
|
81
118
|
...algorithm,
|
|
82
|
-
limit:
|
|
119
|
+
limit: share(algorithm.limit),
|
|
120
|
+
maxReserved,
|
|
121
|
+
shards: 1
|
|
83
122
|
};
|
|
84
123
|
}
|
|
124
|
+
/** Tokens a shard-level config may spend, ignoring the per-window refill. */
|
|
125
|
+
function algorithmBudget(algorithm) {
|
|
126
|
+
return algorithm.kind === "tokenBucket" ? algorithm.maxTokens : algorithm.limit;
|
|
127
|
+
}
|
|
128
|
+
/** Tokens the algorithm can hold, including fixed-window burst capacity. */
|
|
129
|
+
function algorithmCapacity(algorithm) {
|
|
130
|
+
if (algorithm.kind === "fixedWindow") return algorithm.capacity;
|
|
131
|
+
return algorithmBudget(algorithm);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Deal a whole-token budget across `shards` so the shares sum back to `total`.
|
|
135
|
+
*
|
|
136
|
+
* A plain `total / shards` strands the fractional part of every shard: a shard
|
|
137
|
+
* holding `2.5` tokens only ever grants two whole requests and refills to `2.5`
|
|
138
|
+
* again, so `limit: 5` over two shards would enforce `4` forever. Whole budgets
|
|
139
|
+
* are dealt as `floor` plus one extra token to the first `total % shards` shards
|
|
140
|
+
* instead. Fractional budgets have no whole-token floor to hit, so they keep the
|
|
141
|
+
* even split.
|
|
142
|
+
*/
|
|
143
|
+
function shardShare(total, shards, shard) {
|
|
144
|
+
const whole = Math.floor(total);
|
|
145
|
+
const fractional = total - whole;
|
|
146
|
+
return Math.floor(whole / shards) + (shard < whole % shards ? 1 : 0) + (shard === 0 ? fractional : 0);
|
|
147
|
+
}
|
|
85
148
|
function normalizeShards(shards) {
|
|
86
149
|
if (shards === void 0) return DEFAULT_SHARDS;
|
|
87
150
|
const rounded = Math.round(shards);
|
|
@@ -91,14 +154,98 @@ function normalizeShards(shards) {
|
|
|
91
154
|
function validatePositive(value, field) {
|
|
92
155
|
if (!Number.isFinite(value) || value <= 0) throw new Error(`${field} must be a positive number`);
|
|
93
156
|
}
|
|
157
|
+
function validateMaxReserved(value) {
|
|
158
|
+
if (value !== void 0 && (!Number.isFinite(value) || value < 0)) throw new Error("maxReserved must be a non-negative finite number");
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Reject budgets that leave a shard with nothing to spend.
|
|
162
|
+
*
|
|
163
|
+
* Shares are dealt, so the smallest shard holds `floor(budget / shards)`. Once
|
|
164
|
+
* that reaches zero the shard is dead weight: it absorbs writes it can never
|
|
165
|
+
* grant, and the caller sees unexplained denials. Fail where the budget is
|
|
166
|
+
* configured instead — including the runtime override, which rewrites the
|
|
167
|
+
* budget after the builders ran.
|
|
168
|
+
*/
|
|
169
|
+
function assertShardBudget(algorithm) {
|
|
170
|
+
const { shards } = algorithm;
|
|
171
|
+
if (algorithm.kind === "tokenBucket") {
|
|
172
|
+
validateShardBudget(algorithm.maxTokens, "maxTokens", shards);
|
|
173
|
+
return algorithm;
|
|
174
|
+
}
|
|
175
|
+
validateShardBudget(algorithm.limit, "limit", shards);
|
|
176
|
+
if (algorithm.kind === "fixedWindow") validateShardBudget(algorithm.capacity, "capacity", shards);
|
|
177
|
+
return algorithm;
|
|
178
|
+
}
|
|
179
|
+
function validateShardBudget(value, field, shards) {
|
|
180
|
+
if (value / shards < 1) throw new Error(`${field} (${value}) must be at least shards (${shards}). Sharding deals ${field} across the shards, and a share below one token cannot serve a request. Lower shards or raise ${field}.`);
|
|
181
|
+
}
|
|
94
182
|
|
|
95
183
|
//#endregion
|
|
96
184
|
//#region src/ratelimit/core/calculate-rate-limit.ts
|
|
97
185
|
function calculateRatelimit(state, algorithm, now, count) {
|
|
186
|
+
const shardStates = state?.shards;
|
|
187
|
+
if (algorithm.shards > 1 && count > maximumShardCapacity(algorithm)) return {
|
|
188
|
+
...calculateSingleRatelimit(state, algorithm, now, count),
|
|
189
|
+
retryAfter: Number.POSITIVE_INFINITY,
|
|
190
|
+
reset: Number.POSITIVE_INFINITY
|
|
191
|
+
};
|
|
192
|
+
if (algorithm.shards > 1 && shardStates?.length === algorithm.shards) return calculateShardedRatelimit(shardStates, algorithm, now, count);
|
|
193
|
+
return calculateSingleRatelimit(state, algorithm, now, count);
|
|
194
|
+
}
|
|
195
|
+
function calculateSingleRatelimit(state, algorithm, now, count) {
|
|
98
196
|
if (algorithm.kind === "fixedWindow") return calculateFixedWindow(state, algorithm, now, count);
|
|
99
197
|
if (algorithm.kind === "tokenBucket") return calculateTokenBucket(state, algorithm, now, count);
|
|
100
198
|
return calculateSlidingWindow(state, algorithm, now, count);
|
|
101
199
|
}
|
|
200
|
+
function maximumShardCapacity(algorithm) {
|
|
201
|
+
let maximum = 0;
|
|
202
|
+
for (let shard = 0; shard < algorithm.shards; shard += 1) maximum = Math.max(maximum, algorithmCapacity(shardAlgorithm(algorithm, shard)));
|
|
203
|
+
return maximum;
|
|
204
|
+
}
|
|
205
|
+
function calculateShardedRatelimit(shardStates, algorithm, now, count) {
|
|
206
|
+
const candidates = shardStates.map(({ shard, state: shardState }) => {
|
|
207
|
+
const perShard = shardAlgorithm(algorithm, shard);
|
|
208
|
+
return {
|
|
209
|
+
baseline: calculateRatelimit(shardState, perShard, now, 0),
|
|
210
|
+
requested: calculateRatelimit(shardState, perShard, now, count),
|
|
211
|
+
shard
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
const selected = [...count === 0 ? candidates : candidates.filter((candidate) => candidate.requested.retryAfter === void 0)].sort((a, b) => b.requested.remainingRaw - a.requested.remainingRaw)[0];
|
|
215
|
+
const projected = candidates.map((candidate) => ({
|
|
216
|
+
evaluated: count !== 0 && candidate.shard === selected?.shard ? candidate.requested : candidate.baseline,
|
|
217
|
+
shard: candidate.shard
|
|
218
|
+
}));
|
|
219
|
+
const retryValues = candidates.flatMap((candidate) => candidate.requested.retryAfter === void 0 ? [] : [candidate.requested.retryAfter]);
|
|
220
|
+
const retryAfter = selected ? void 0 : Math.min(...retryValues);
|
|
221
|
+
const remaining = selected ? projected.reduce((total, candidate) => total + candidate.evaluated.remaining, 0) : 0;
|
|
222
|
+
const remainingRaw = selected ? projected.reduce((total, candidate) => total + Math.max(0, candidate.evaluated.remainingRaw), 0) : Math.max(...candidates.map((candidate) => candidate.requested.remainingRaw));
|
|
223
|
+
const auxTimestamps = projected.flatMap((candidate) => candidate.evaluated.state.auxTs === void 0 ? [] : [candidate.evaluated.state.auxTs]);
|
|
224
|
+
return {
|
|
225
|
+
state: {
|
|
226
|
+
value: selected || count === 0 ? projected.reduce((total, candidate) => total + candidate.evaluated.state.value, 0) : remainingRaw,
|
|
227
|
+
ts: Math.max(...projected.map((candidate) => candidate.evaluated.state.ts)),
|
|
228
|
+
...auxTimestamps.length > 0 ? {
|
|
229
|
+
auxValue: projected.reduce((total, candidate) => total + (candidate.evaluated.state.auxValue ?? 0), 0),
|
|
230
|
+
auxTs: Math.max(...auxTimestamps)
|
|
231
|
+
} : {},
|
|
232
|
+
shards: projected.map(({ evaluated, shard }) => ({
|
|
233
|
+
shard,
|
|
234
|
+
state: {
|
|
235
|
+
value: evaluated.state.value,
|
|
236
|
+
ts: evaluated.state.ts,
|
|
237
|
+
auxValue: evaluated.state.auxValue,
|
|
238
|
+
auxTs: evaluated.state.auxTs
|
|
239
|
+
}
|
|
240
|
+
}))
|
|
241
|
+
},
|
|
242
|
+
retryAfter,
|
|
243
|
+
remaining,
|
|
244
|
+
remainingRaw,
|
|
245
|
+
reset: Math.min(...candidates.map((candidate) => candidate.requested.reset)),
|
|
246
|
+
limit: algorithmBudget(algorithm)
|
|
247
|
+
};
|
|
248
|
+
}
|
|
102
249
|
function calculateTokenBucket(state, config, now, count) {
|
|
103
250
|
const ratePerMs = config.refillRate / config.interval;
|
|
104
251
|
const initial = state ?? {
|
|
@@ -107,7 +254,8 @@ function calculateTokenBucket(state, config, now, count) {
|
|
|
107
254
|
};
|
|
108
255
|
const elapsed = Math.max(0, now - initial.ts);
|
|
109
256
|
const nextValue = Math.min(initial.value + elapsed * ratePerMs, config.maxTokens) - count;
|
|
110
|
-
|
|
257
|
+
let retryAfter;
|
|
258
|
+
if (nextValue < 0) retryAfter = count > config.maxTokens ? Number.POSITIVE_INFINITY : Math.ceil(-nextValue / ratePerMs);
|
|
111
259
|
return {
|
|
112
260
|
state: {
|
|
113
261
|
value: nextValue,
|
|
@@ -115,6 +263,7 @@ function calculateTokenBucket(state, config, now, count) {
|
|
|
115
263
|
},
|
|
116
264
|
retryAfter,
|
|
117
265
|
remaining: Math.max(0, Math.floor(nextValue)),
|
|
266
|
+
remainingRaw: nextValue,
|
|
118
267
|
reset: retryAfter ? now + retryAfter : now,
|
|
119
268
|
limit: config.maxTokens
|
|
120
269
|
};
|
|
@@ -129,7 +278,8 @@ function calculateFixedWindow(state, config, now, count) {
|
|
|
129
278
|
const replenished = Math.min(initial.value + config.limit * elapsedWindows, config.capacity);
|
|
130
279
|
const ts = initial.ts + elapsedWindows * config.window;
|
|
131
280
|
const nextValue = replenished - count;
|
|
132
|
-
|
|
281
|
+
let retryAfter;
|
|
282
|
+
if (nextValue < 0) retryAfter = count > config.capacity ? Number.POSITIVE_INFINITY : ts + config.window * Math.ceil(-nextValue / config.limit) - now;
|
|
133
283
|
return {
|
|
134
284
|
state: {
|
|
135
285
|
value: nextValue,
|
|
@@ -137,7 +287,8 @@ function calculateFixedWindow(state, config, now, count) {
|
|
|
137
287
|
},
|
|
138
288
|
retryAfter,
|
|
139
289
|
remaining: Math.max(0, Math.floor(nextValue)),
|
|
140
|
-
|
|
290
|
+
remainingRaw: nextValue,
|
|
291
|
+
reset: retryAfter === Number.POSITIVE_INFINITY ? Number.POSITIVE_INFINITY : ts + config.window,
|
|
141
292
|
limit: config.limit
|
|
142
293
|
};
|
|
143
294
|
}
|
|
@@ -157,7 +308,8 @@ function calculateSlidingWindow(state, config, now, count) {
|
|
|
157
308
|
const projectedCurrent = currentCount + count;
|
|
158
309
|
const projectedUsed = projectedCurrent + previousCount * previousWeight;
|
|
159
310
|
const remaining = config.limit - projectedUsed;
|
|
160
|
-
|
|
311
|
+
let retryAfter;
|
|
312
|
+
if (remaining < 0) retryAfter = count > config.limit ? Number.POSITIVE_INFINITY : Math.max(1, config.window - elapsedInWindow);
|
|
161
313
|
return {
|
|
162
314
|
state: {
|
|
163
315
|
value: projectedCurrent,
|
|
@@ -167,10 +319,22 @@ function calculateSlidingWindow(state, config, now, count) {
|
|
|
167
319
|
},
|
|
168
320
|
retryAfter,
|
|
169
321
|
remaining: Math.max(0, Math.floor(remaining)),
|
|
170
|
-
|
|
322
|
+
remainingRaw: remaining,
|
|
323
|
+
reset: retryAfter === Number.POSITIVE_INFINITY ? Number.POSITIVE_INFINITY : windowStart + config.window,
|
|
171
324
|
limit: config.limit
|
|
172
325
|
};
|
|
173
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Convert a {@link RatelimitSnapshot} back into the `RatelimitState` shape that
|
|
329
|
+
* {@link calculateRatelimit} consumes.
|
|
330
|
+
*
|
|
331
|
+
* Snapshot `value` is always "tokens left". Fixed window and token bucket store
|
|
332
|
+
* that directly, but sliding window state stores the used count, so it has to be
|
|
333
|
+
* inverted before it can be replayed.
|
|
334
|
+
*/
|
|
335
|
+
function snapshotToState(snapshot) {
|
|
336
|
+
return { ...snapshot.state };
|
|
337
|
+
}
|
|
174
338
|
function alignWindowStart(now, window, start = 0) {
|
|
175
339
|
const offsetNow = now - start;
|
|
176
340
|
return start + Math.floor(offsetNow / window) * window;
|
|
@@ -178,18 +342,27 @@ function alignWindowStart(now, window, start = 0) {
|
|
|
178
342
|
|
|
179
343
|
//#endregion
|
|
180
344
|
//#region src/ratelimit/core/cache.ts
|
|
345
|
+
/**
|
|
346
|
+
* Remembers exhausted shards so a request routed to one skips its read.
|
|
347
|
+
*
|
|
348
|
+
* Entries are per shard, requested count, and reservation mode, never per
|
|
349
|
+
* identifier: a shard can reject a large or ordinary request while still
|
|
350
|
+
* serving a smaller or reserved one, and its peers may still hold tokens.
|
|
351
|
+
*/
|
|
352
|
+
const MAX_BLOCK_VARIANTS_PER_IDENTIFIER = 32;
|
|
181
353
|
var EphemeralBlockCache = class {
|
|
182
354
|
constructor(cache) {
|
|
183
355
|
this.cache = cache;
|
|
184
356
|
}
|
|
185
|
-
isBlocked(identifier) {
|
|
186
|
-
const
|
|
357
|
+
isBlocked(identifier, shard, count, reserve) {
|
|
358
|
+
const key = shardKey(identifier, shard, count, reserve);
|
|
359
|
+
const reset = this.cache.get(key);
|
|
187
360
|
if (!reset) return {
|
|
188
361
|
blocked: false,
|
|
189
362
|
reset: 0
|
|
190
363
|
};
|
|
191
364
|
if (reset <= Date.now()) {
|
|
192
|
-
this.cache.delete(
|
|
365
|
+
this.cache.delete(key);
|
|
193
366
|
return {
|
|
194
367
|
blocked: false,
|
|
195
368
|
reset: 0
|
|
@@ -200,16 +373,45 @@ var EphemeralBlockCache = class {
|
|
|
200
373
|
reset
|
|
201
374
|
};
|
|
202
375
|
}
|
|
203
|
-
blockUntil(identifier, reset) {
|
|
204
|
-
|
|
376
|
+
blockUntil(identifier, shard, count, reserve, reset) {
|
|
377
|
+
if (!Number.isFinite(reset)) return;
|
|
378
|
+
this.pruneExpired();
|
|
379
|
+
const key = shardKey(identifier, shard, count, reserve);
|
|
380
|
+
if (!this.cache.has(key)) this.evictOldestVariant(identifier);
|
|
381
|
+
this.cache.set(key, reset);
|
|
205
382
|
}
|
|
206
383
|
clear(identifier) {
|
|
207
|
-
|
|
384
|
+
const prefix = identifierPrefix(identifier);
|
|
385
|
+
for (const key of this.cache.keys()) if (key.startsWith(prefix)) this.cache.delete(key);
|
|
386
|
+
}
|
|
387
|
+
clearAll() {
|
|
388
|
+
this.cache.clear();
|
|
208
389
|
}
|
|
209
390
|
size() {
|
|
210
391
|
return this.cache.size;
|
|
211
392
|
}
|
|
393
|
+
pruneExpired() {
|
|
394
|
+
const now = Date.now();
|
|
395
|
+
for (const [key, reset] of this.cache) if (reset <= now) this.cache.delete(key);
|
|
396
|
+
}
|
|
397
|
+
evictOldestVariant(identifier) {
|
|
398
|
+
const prefix = identifierPrefix(identifier);
|
|
399
|
+
let variants = 0;
|
|
400
|
+
let oldest;
|
|
401
|
+
for (const key of this.cache.keys()) {
|
|
402
|
+
if (!key.startsWith(prefix)) continue;
|
|
403
|
+
oldest ??= key;
|
|
404
|
+
variants += 1;
|
|
405
|
+
}
|
|
406
|
+
if (variants >= MAX_BLOCK_VARIANTS_PER_IDENTIFIER && oldest) this.cache.delete(oldest);
|
|
407
|
+
}
|
|
212
408
|
};
|
|
409
|
+
function shardKey(identifier, shard, count, reserve) {
|
|
410
|
+
return `${identifierPrefix(identifier)}${shard}:${count}:${reserve ? 1 : 0}`;
|
|
411
|
+
}
|
|
412
|
+
function identifierPrefix(identifier) {
|
|
413
|
+
return `${identifier.length}:${identifier}:`;
|
|
414
|
+
}
|
|
213
415
|
function createReadDedupeCache() {
|
|
214
416
|
const cache = /* @__PURE__ */ new Map();
|
|
215
417
|
return {
|
|
@@ -483,6 +685,7 @@ var Ratelimit = class Ratelimit {
|
|
|
483
685
|
blockCache;
|
|
484
686
|
blockCacheSource;
|
|
485
687
|
checkCache = createReadDedupeCache();
|
|
688
|
+
cacheGeneration = 0;
|
|
486
689
|
constructor(config) {
|
|
487
690
|
this.config = config;
|
|
488
691
|
this.store = new ConvexRatelimitStore(config.db);
|
|
@@ -518,23 +721,22 @@ var Ratelimit = class Ratelimit {
|
|
|
518
721
|
}
|
|
519
722
|
async resetUsedTokens(identifier) {
|
|
520
723
|
await this.store.deleteStates(this.prefix, identifier);
|
|
724
|
+
this.cacheGeneration += 1;
|
|
521
725
|
this.checkCache.clear();
|
|
522
|
-
if (this.blockCache) this.blockCache.clear(identifier);
|
|
726
|
+
if (this.blockCache) this.blockCache.clear(this.blockKey(identifier));
|
|
523
727
|
clearProtection(this.prefix, identifier);
|
|
524
728
|
}
|
|
525
729
|
async getRemaining(identifier) {
|
|
526
|
-
const
|
|
527
|
-
const evaluated =
|
|
528
|
-
value: value.value,
|
|
529
|
-
ts: value.ts
|
|
530
|
-
}, value.config, Date.now(), 0);
|
|
730
|
+
const algorithm = await this.resolveAlgorithm();
|
|
731
|
+
const evaluated = await this.readShards(identifier, algorithm, Array.from({ length: algorithm.shards }, (_, shard) => shard));
|
|
531
732
|
return {
|
|
532
|
-
remaining: Math.max(0, evaluated.remaining),
|
|
533
|
-
reset: evaluated.reset,
|
|
534
|
-
limit:
|
|
733
|
+
remaining: Math.max(0, evaluated.reduce((total, shard) => total + shard.evaluated.remaining, 0)),
|
|
734
|
+
reset: Math.min(...evaluated.map((shard) => shard.evaluated.reset)),
|
|
735
|
+
limit: algorithmBudget(algorithm)
|
|
535
736
|
};
|
|
536
737
|
}
|
|
537
738
|
async getValue(identifier, options) {
|
|
739
|
+
const cacheGeneration = this.cacheGeneration;
|
|
538
740
|
const cacheKey = `${identifier}:${options?.sampleShards ?? 0}`;
|
|
539
741
|
const cached = this.checkCache.get(cacheKey);
|
|
540
742
|
if (cached) {
|
|
@@ -544,30 +746,70 @@ var Ratelimit = class Ratelimit {
|
|
|
544
746
|
const algorithm = await this.resolveAlgorithm();
|
|
545
747
|
const sampleShards = Math.max(1, Math.min(options?.sampleShards ?? 1, algorithm.shards));
|
|
546
748
|
const shards = pickSampleShards(algorithm.shards, sampleShards);
|
|
547
|
-
const
|
|
548
|
-
let
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
749
|
+
const samples = await this.readShards(identifier, algorithm, shards);
|
|
750
|
+
let sampledRemaining = 0;
|
|
751
|
+
let sampledCapacity = 0;
|
|
752
|
+
let sampledStateValue = 0;
|
|
753
|
+
let sampledAuxValue = 0;
|
|
754
|
+
let latestTs = null;
|
|
755
|
+
let latestAuxTs = null;
|
|
756
|
+
let fullestShard = null;
|
|
757
|
+
let fullestRemaining = Number.NEGATIVE_INFINITY;
|
|
758
|
+
for (const { evaluated, perShard, shard } of samples) {
|
|
759
|
+
sampledRemaining += evaluated.remainingRaw;
|
|
760
|
+
sampledCapacity += algorithmCapacity(perShard);
|
|
761
|
+
sampledStateValue += evaluated.state.value;
|
|
762
|
+
sampledAuxValue += evaluated.state.auxValue ?? 0;
|
|
763
|
+
latestTs = latestTs === null ? evaluated.state.ts : Math.max(latestTs, evaluated.state.ts);
|
|
764
|
+
if (evaluated.state.auxTs !== void 0) latestAuxTs = latestAuxTs === null ? evaluated.state.auxTs : Math.max(latestAuxTs, evaluated.state.auxTs);
|
|
765
|
+
if (evaluated.remainingRaw > fullestRemaining) {
|
|
766
|
+
fullestRemaining = evaluated.remainingRaw;
|
|
767
|
+
fullestShard = shard;
|
|
768
|
+
}
|
|
558
769
|
}
|
|
559
|
-
const result =
|
|
560
|
-
value: algorithm
|
|
561
|
-
ts: now,
|
|
770
|
+
const result = fullestShard === null ? {
|
|
771
|
+
value: algorithmCapacity(algorithm),
|
|
772
|
+
ts: Date.now(),
|
|
562
773
|
shard: 0,
|
|
563
|
-
config: algorithm
|
|
774
|
+
config: algorithm,
|
|
775
|
+
state: {
|
|
776
|
+
value: algorithm.kind === "slidingWindow" ? 0 : algorithmCapacity(algorithm),
|
|
777
|
+
ts: Date.now(),
|
|
778
|
+
shards: []
|
|
779
|
+
}
|
|
780
|
+
} : {
|
|
781
|
+
value: scaleToGlobal(sampledRemaining, sampledCapacity, algorithmCapacity(algorithm)),
|
|
782
|
+
ts: latestTs ?? Date.now(),
|
|
783
|
+
shard: fullestShard,
|
|
784
|
+
config: algorithm,
|
|
785
|
+
state: {
|
|
786
|
+
value: scaleToGlobal(sampledStateValue, sampledCapacity, algorithmCapacity(algorithm)),
|
|
787
|
+
ts: latestTs ?? Date.now(),
|
|
788
|
+
...algorithm.kind === "slidingWindow" ? {
|
|
789
|
+
auxValue: scaleToGlobal(sampledAuxValue, sampledCapacity, algorithmCapacity(algorithm)),
|
|
790
|
+
auxTs: latestAuxTs ?? (latestTs ?? Date.now()) - algorithm.window
|
|
791
|
+
} : {},
|
|
792
|
+
shards: samples.map(({ evaluated, shard }) => ({
|
|
793
|
+
shard,
|
|
794
|
+
state: {
|
|
795
|
+
value: evaluated.state.value,
|
|
796
|
+
ts: evaluated.state.ts,
|
|
797
|
+
auxValue: evaluated.state.auxValue,
|
|
798
|
+
auxTs: evaluated.state.auxTs
|
|
799
|
+
}
|
|
800
|
+
}))
|
|
801
|
+
}
|
|
564
802
|
};
|
|
565
|
-
this.checkCache.set(cacheKey, Promise.resolve(result));
|
|
803
|
+
if (cacheGeneration === this.cacheGeneration) this.checkCache.set(cacheKey, Promise.resolve(result));
|
|
566
804
|
return result;
|
|
567
805
|
}
|
|
568
806
|
async setDynamicLimit(options) {
|
|
569
807
|
if (!this.dynamicLimits) throw new Error("dynamicLimits must be enabled in the Ratelimit constructor to use setDynamicLimit()");
|
|
808
|
+
applyDynamicLimit(this.limiter, options.limit === false ? null : options.limit);
|
|
570
809
|
await this.store.setDynamicLimit(this.prefix, options.limit);
|
|
810
|
+
this.cacheGeneration += 1;
|
|
811
|
+
this.checkCache.clear();
|
|
812
|
+
this.blockCache?.clearAll();
|
|
571
813
|
}
|
|
572
814
|
async getDynamicLimit() {
|
|
573
815
|
if (!this.dynamicLimits) throw new Error("dynamicLimits must be enabled in the Ratelimit constructor to use getDynamicLimit()");
|
|
@@ -584,7 +826,22 @@ var Ratelimit = class Ratelimit {
|
|
|
584
826
|
value: v.number(),
|
|
585
827
|
ts: v.number(),
|
|
586
828
|
shard: v.number(),
|
|
587
|
-
config: v.any()
|
|
829
|
+
config: v.any(),
|
|
830
|
+
state: v.object({
|
|
831
|
+
value: v.number(),
|
|
832
|
+
ts: v.number(),
|
|
833
|
+
auxValue: v.optional(v.number()),
|
|
834
|
+
auxTs: v.optional(v.number()),
|
|
835
|
+
shards: v.optional(v.array(v.object({
|
|
836
|
+
shard: v.number(),
|
|
837
|
+
state: v.object({
|
|
838
|
+
value: v.number(),
|
|
839
|
+
ts: v.number(),
|
|
840
|
+
auxValue: v.optional(v.number()),
|
|
841
|
+
auxTs: v.optional(v.number())
|
|
842
|
+
})
|
|
843
|
+
})))
|
|
844
|
+
})
|
|
588
845
|
}),
|
|
589
846
|
handler: async (ctx, args) => {
|
|
590
847
|
const identifier = await resolveIdentifier(options?.identifier, ctx, args.identifier);
|
|
@@ -606,6 +863,7 @@ var Ratelimit = class Ratelimit {
|
|
|
606
863
|
});
|
|
607
864
|
}
|
|
608
865
|
async evaluate(identifier, request, consume) {
|
|
866
|
+
const cacheGeneration = this.cacheGeneration;
|
|
609
867
|
const deniedValue = this.enableProtection ? pickDeniedValue({
|
|
610
868
|
prefix: this.prefix,
|
|
611
869
|
identifier,
|
|
@@ -615,7 +873,7 @@ var Ratelimit = class Ratelimit {
|
|
|
615
873
|
if (deniedValue) return {
|
|
616
874
|
success: false,
|
|
617
875
|
ok: false,
|
|
618
|
-
limit:
|
|
876
|
+
limit: algorithmBudget(this.limiter),
|
|
619
877
|
remaining: 0,
|
|
620
878
|
reset: Date.now() + 6e4,
|
|
621
879
|
pending: Promise.resolve(),
|
|
@@ -623,47 +881,73 @@ var Ratelimit = class Ratelimit {
|
|
|
623
881
|
deniedValue
|
|
624
882
|
};
|
|
625
883
|
const algorithm = await this.resolveAlgorithm();
|
|
626
|
-
const count =
|
|
627
|
-
const reserveRequested =
|
|
628
|
-
if (
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
884
|
+
const count = normalizeCount(request);
|
|
885
|
+
const reserveRequested = Boolean(request?.reserve);
|
|
886
|
+
if (count > maximumRequestSize(algorithm, reserveRequested)) return {
|
|
887
|
+
success: false,
|
|
888
|
+
ok: false,
|
|
889
|
+
limit: algorithmBudget(algorithm),
|
|
890
|
+
remaining: 0,
|
|
891
|
+
reset: 0,
|
|
892
|
+
pending: Promise.resolve(),
|
|
893
|
+
reason: "requestTooLarge"
|
|
894
|
+
};
|
|
895
|
+
const picked = pickCandidateShards(algorithm.shards);
|
|
896
|
+
const attempted = /* @__PURE__ */ new Set();
|
|
897
|
+
let blockedUntil = Number.POSITIVE_INFINITY;
|
|
898
|
+
const findOpen = (shards) => {
|
|
899
|
+
const open = [];
|
|
900
|
+
for (const shard of shards) {
|
|
901
|
+
attempted.add(shard);
|
|
902
|
+
const blocked = this.blockCache && count > 0 ? this.blockCache.isBlocked(this.blockKey(identifier), shard, count, reserveRequested) : void 0;
|
|
903
|
+
if (blocked?.blocked) blockedUntil = Math.min(blockedUntil, blocked.reset);
|
|
904
|
+
else open.push(shard);
|
|
905
|
+
}
|
|
906
|
+
return open;
|
|
907
|
+
};
|
|
641
908
|
const now = Date.now();
|
|
642
|
-
const candidates = await this.evaluateCandidates(identifier, algorithm, now, count, reserveRequested);
|
|
909
|
+
const candidates = await this.evaluateCandidates(identifier, algorithm, findOpen(picked), now, count, reserveRequested);
|
|
910
|
+
if (!candidates.some((candidate) => candidate.success)) {
|
|
911
|
+
const fallback = Array.from({ length: algorithm.shards }, (_, shard) => shard).filter((shard) => !attempted.has(shard));
|
|
912
|
+
candidates.push(...await this.evaluateCandidates(identifier, algorithm, findOpen(fallback), now, count, reserveRequested));
|
|
913
|
+
}
|
|
914
|
+
if (candidates.length === 0) return {
|
|
915
|
+
success: false,
|
|
916
|
+
ok: false,
|
|
917
|
+
limit: algorithmBudget(algorithm),
|
|
918
|
+
remaining: 0,
|
|
919
|
+
reset: blockedUntil,
|
|
920
|
+
pending: Promise.resolve(),
|
|
921
|
+
reason: "cacheBlock"
|
|
922
|
+
};
|
|
923
|
+
if (consume && this.blockCache && count > 0 && cacheGeneration === this.cacheGeneration) {
|
|
924
|
+
for (const candidate of candidates) if (!candidate.success) this.blockCache.blockUntil(this.blockKey(identifier), candidate.shard, count, reserveRequested, now + (candidate.retryAfter ?? 1));
|
|
925
|
+
}
|
|
643
926
|
const successful = candidates.filter((candidate) => candidate.success);
|
|
644
927
|
if (successful.length > 0) {
|
|
645
|
-
const best = successful.sort((a, b) => b.evaluated.
|
|
646
|
-
if (consume && count !== 0)
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
928
|
+
const best = successful.sort((a, b) => b.evaluated.remainingRaw - a.evaluated.remainingRaw)[0];
|
|
929
|
+
if (consume && count !== 0) {
|
|
930
|
+
await this.store.upsertState({
|
|
931
|
+
name: this.prefix,
|
|
932
|
+
key: identifier,
|
|
933
|
+
shard: best.shard,
|
|
934
|
+
state: best.evaluated.state
|
|
935
|
+
});
|
|
936
|
+
this.cacheGeneration += 1;
|
|
937
|
+
}
|
|
653
938
|
clearProtection(this.prefix, identifier);
|
|
654
939
|
this.checkCache.clear();
|
|
655
940
|
return {
|
|
656
941
|
success: true,
|
|
657
942
|
ok: true,
|
|
658
|
-
limit:
|
|
659
|
-
remaining: best.evaluated.
|
|
943
|
+
limit: algorithmBudget(algorithm),
|
|
944
|
+
remaining: Math.max(0, Math.floor(scaleToGlobal(best.evaluated.remainingRaw, algorithmCapacity(shardAlgorithm(algorithm, best.shard)), algorithmCapacity(algorithm)))),
|
|
660
945
|
reset: best.evaluated.reset,
|
|
661
946
|
pending: Promise.resolve()
|
|
662
947
|
};
|
|
663
948
|
}
|
|
664
|
-
const
|
|
665
|
-
const reset = now +
|
|
666
|
-
if (consume && this.blockCache && count > 0) this.blockCache.blockUntil(`${this.prefix}:${identifier}`, reset);
|
|
949
|
+
const retryAfter = (candidates.filter((candidate) => candidate.retryAfter !== void 0).sort((a, b) => (a.retryAfter ?? Number.MAX_SAFE_INTEGER) - (b.retryAfter ?? Number.MAX_SAFE_INTEGER))[0] ?? candidates[0]).retryAfter ?? 1;
|
|
950
|
+
const reset = Math.min(now + retryAfter, blockedUntil);
|
|
667
951
|
if (consume && this.enableProtection) recordRatelimitFailure({
|
|
668
952
|
prefix: this.prefix,
|
|
669
953
|
identifier,
|
|
@@ -673,38 +957,48 @@ var Ratelimit = class Ratelimit {
|
|
|
673
957
|
return {
|
|
674
958
|
success: false,
|
|
675
959
|
ok: false,
|
|
676
|
-
limit:
|
|
960
|
+
limit: algorithmBudget(algorithm),
|
|
677
961
|
remaining: 0,
|
|
678
962
|
reset,
|
|
679
963
|
pending: Promise.resolve()
|
|
680
964
|
};
|
|
681
965
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
966
|
+
blockKey(identifier) {
|
|
967
|
+
return `${this.prefix}:${identifier}`;
|
|
968
|
+
}
|
|
969
|
+
async readShards(identifier, algorithm, shards) {
|
|
970
|
+
const samples = await Promise.all(shards.map(async (shard) => ({
|
|
971
|
+
perShard: shardAlgorithm(algorithm, shard),
|
|
972
|
+
shard,
|
|
973
|
+
state: normalizeState(await this.store.getState(this.prefix, identifier, shard))
|
|
974
|
+
})));
|
|
975
|
+
const now = Date.now();
|
|
976
|
+
return samples.map(({ perShard, shard, state }) => ({
|
|
977
|
+
shard,
|
|
978
|
+
perShard,
|
|
979
|
+
evaluated: calculateRatelimit(state, perShard, now, 0)
|
|
980
|
+
}));
|
|
981
|
+
}
|
|
982
|
+
async evaluateCandidates(identifier, algorithm, shards, now, count, reserveRequested) {
|
|
983
|
+
return Promise.all(shards.map(async (shard) => {
|
|
984
|
+
const perShard = shardAlgorithm(algorithm, shard);
|
|
686
985
|
const state = normalizeState(await this.store.getState(this.prefix, identifier, shard));
|
|
687
|
-
const evaluated = calculateRatelimit(state,
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
result.push({
|
|
986
|
+
const evaluated = calculateRatelimit(state, perShard, now, count);
|
|
987
|
+
const retryAfter = getRequestRetryAfter(evaluated, perShard, now, count, reserveRequested);
|
|
988
|
+
return {
|
|
691
989
|
shard,
|
|
692
990
|
state,
|
|
693
991
|
evaluated,
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
992
|
+
retryAfter,
|
|
993
|
+
success: retryAfter === void 0
|
|
994
|
+
};
|
|
995
|
+
}));
|
|
698
996
|
}
|
|
699
997
|
async resolveAlgorithm() {
|
|
700
998
|
if (!this.dynamicLimits) return this.limiter;
|
|
701
999
|
const dynamicLimit = await this.store.getDynamicLimit(this.prefix);
|
|
702
1000
|
return applyDynamicLimit(this.limiter, dynamicLimit);
|
|
703
1001
|
}
|
|
704
|
-
rawLimit(algorithm) {
|
|
705
|
-
if (algorithm.kind === "tokenBucket") return algorithm.maxTokens;
|
|
706
|
-
return algorithm.limit;
|
|
707
|
-
}
|
|
708
1002
|
async runWithTimeout(operation) {
|
|
709
1003
|
if (this.timeout <= 0) return operation();
|
|
710
1004
|
const startedAt = Date.now();
|
|
@@ -729,6 +1023,39 @@ var Ratelimit = class Ratelimit {
|
|
|
729
1023
|
};
|
|
730
1024
|
}
|
|
731
1025
|
};
|
|
1026
|
+
function maximumRequestSize(algorithm, reserveRequested) {
|
|
1027
|
+
let maximum = 0;
|
|
1028
|
+
for (let shard = 0; shard < algorithm.shards; shard += 1) {
|
|
1029
|
+
const perShard = shardAlgorithm(algorithm, shard);
|
|
1030
|
+
maximum = Math.max(maximum, algorithmCapacity(perShard) + reservationHeadroom(perShard, reserveRequested));
|
|
1031
|
+
}
|
|
1032
|
+
return maximum;
|
|
1033
|
+
}
|
|
1034
|
+
function reservationHeadroom(algorithm, reserveRequested) {
|
|
1035
|
+
if (!reserveRequested || algorithm.kind === "slidingWindow") return 0;
|
|
1036
|
+
return algorithm.maxReserved ?? Number.POSITIVE_INFINITY;
|
|
1037
|
+
}
|
|
1038
|
+
function getRequestRetryAfter(evaluated, algorithm, now, count, reserveRequested) {
|
|
1039
|
+
const reservation = reservationHeadroom(algorithm, reserveRequested);
|
|
1040
|
+
if (count > algorithmCapacity(algorithm) + reservation) return Number.POSITIVE_INFINITY;
|
|
1041
|
+
if (!reserveRequested || evaluated.retryAfter === void 0 || algorithm.kind === "slidingWindow") return evaluated.retryAfter;
|
|
1042
|
+
if (algorithm.maxReserved === void 0) return;
|
|
1043
|
+
const excessDebt = -evaluated.remainingRaw - Math.max(0, algorithm.maxReserved);
|
|
1044
|
+
if (excessDebt <= 0) return;
|
|
1045
|
+
if (algorithm.kind === "tokenBucket") return Math.ceil(excessDebt / (algorithm.refillRate / algorithm.interval));
|
|
1046
|
+
return Math.max(1, evaluated.state.ts + algorithm.window * Math.ceil(excessDebt / algorithm.limit) - now);
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Project the tokens left on the sampled shards onto the full budget.
|
|
1050
|
+
*
|
|
1051
|
+
* Sampling every shard adds up to the real global balance, so it is returned
|
|
1052
|
+
* untouched. A partial sample is scaled by the share of the budget it covers,
|
|
1053
|
+
* which assumes the unread shards are drained like the ones that were read.
|
|
1054
|
+
*/
|
|
1055
|
+
function scaleToGlobal(remaining, sampledBudget, totalBudget) {
|
|
1056
|
+
if (sampledBudget <= 0 || sampledBudget >= totalBudget) return remaining;
|
|
1057
|
+
return remaining * (totalBudget / sampledBudget);
|
|
1058
|
+
}
|
|
732
1059
|
function normalizeCount(request) {
|
|
733
1060
|
if (!request) return 1;
|
|
734
1061
|
const value = request.rate ?? request.count ?? 1;
|
|
@@ -853,4 +1180,4 @@ const DAY = 24 * HOUR;
|
|
|
853
1180
|
const WEEK = 7 * DAY;
|
|
854
1181
|
|
|
855
1182
|
//#endregion
|
|
856
|
-
export { DAY, HOUR, MINUTE, RATE_LIMIT_DYNAMIC_TABLE, RATE_LIMIT_HIT_TABLE, RATE_LIMIT_STATE_TABLE, Ratelimit, RatelimitPlugin, SECOND, WEEK, applyDynamicLimit, calculateRatelimit, fixedWindow, slidingWindow, toMs, tokenBucket };
|
|
1183
|
+
export { DAY, HOUR, MINUTE, RATE_LIMIT_DYNAMIC_TABLE, RATE_LIMIT_HIT_TABLE, RATE_LIMIT_STATE_TABLE, Ratelimit, RatelimitPlugin, SECOND, WEEK, applyDynamicLimit, calculateRatelimit, fixedWindow, slidingWindow, snapshotToState, toMs, tokenBucket };
|