@commandkit/ratelimit 0.0.0-dev.20260317060555
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 +801 -0
- package/dist/api.d.ts +79 -0
- package/dist/api.js +266 -0
- package/dist/augmentation.d.ts +11 -0
- package/dist/augmentation.js +8 -0
- package/dist/configure.d.ts +28 -0
- package/dist/configure.js +85 -0
- package/dist/constants.d.ts +17 -0
- package/dist/constants.js +21 -0
- package/dist/directive/use-ratelimit-directive.d.ts +22 -0
- package/dist/directive/use-ratelimit-directive.js +38 -0
- package/dist/directive/use-ratelimit.d.ts +14 -0
- package/dist/directive/use-ratelimit.js +169 -0
- package/dist/engine/RateLimitEngine.d.ts +48 -0
- package/dist/engine/RateLimitEngine.js +137 -0
- package/dist/engine/algorithms/fixed-window.d.ts +44 -0
- package/dist/engine/algorithms/fixed-window.js +198 -0
- package/dist/engine/algorithms/leaky-bucket.d.ts +48 -0
- package/dist/engine/algorithms/leaky-bucket.js +119 -0
- package/dist/engine/algorithms/sliding-window.d.ts +45 -0
- package/dist/engine/algorithms/sliding-window.js +127 -0
- package/dist/engine/algorithms/token-bucket.d.ts +47 -0
- package/dist/engine/algorithms/token-bucket.js +118 -0
- package/dist/engine/violations.d.ts +55 -0
- package/dist/engine/violations.js +106 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +28 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +53 -0
- package/dist/plugin.d.ts +140 -0
- package/dist/plugin.js +796 -0
- package/dist/providers/fallback.d.ts +7 -0
- package/dist/providers/fallback.js +11 -0
- package/dist/providers/memory.d.ts +6 -0
- package/dist/providers/memory.js +11 -0
- package/dist/providers/redis.d.ts +7 -0
- package/dist/providers/redis.js +11 -0
- package/dist/runtime.d.ts +45 -0
- package/dist/runtime.js +67 -0
- package/dist/storage/fallback.d.ts +180 -0
- package/dist/storage/fallback.js +261 -0
- package/dist/storage/memory.d.ts +146 -0
- package/dist/storage/memory.js +304 -0
- package/dist/storage/redis.d.ts +130 -0
- package/dist/storage/redis.js +243 -0
- package/dist/types.d.ts +296 -0
- package/dist/types.js +40 -0
- package/dist/utils/config.d.ts +34 -0
- package/dist/utils/config.js +105 -0
- package/dist/utils/keys.d.ts +102 -0
- package/dist/utils/keys.js +304 -0
- package/dist/utils/locking.d.ts +17 -0
- package/dist/utils/locking.js +60 -0
- package/dist/utils/time.d.ts +23 -0
- package/dist/utils/time.js +72 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
# @commandkit/ratelimit
|
|
2
|
+
|
|
3
|
+
`@commandkit/ratelimit` is the official CommandKit plugin for advanced rate limiting. It provides multi-window policies, role overrides, queueing, exemptions, and multiple algorithms while keeping command handlers lean.
|
|
4
|
+
|
|
5
|
+
The `ratelimit()` factory returns two plugins in order: the compiler plugin for the "use ratelimit" directive and the runtime plugin that enforces limits. Runtime options must be configured before the runtime plugin activates.
|
|
6
|
+
|
|
7
|
+
## Table of contents
|
|
8
|
+
|
|
9
|
+
1. [Installation](#installation)
|
|
10
|
+
2. [Setup](#setup)
|
|
11
|
+
3. [Runtime configuration lifecycle](#runtime-configuration-lifecycle)
|
|
12
|
+
4. [Basic usage](#basic-usage)
|
|
13
|
+
5. [Configuration reference](#configuration-reference)
|
|
14
|
+
6. [Limiter resolution and role strategy](#limiter-resolution-and-role-strategy)
|
|
15
|
+
7. [Scopes and keying](#scopes-and-keying)
|
|
16
|
+
8. [Algorithms](#algorithms)
|
|
17
|
+
9. [Storage](#storage)
|
|
18
|
+
10. [Queue mode](#queue-mode)
|
|
19
|
+
11. [Violations and escalation](#violations-and-escalation)
|
|
20
|
+
12. [Bypass and exemptions](#bypass-and-exemptions)
|
|
21
|
+
13. [Responses, hooks, and events](#responses-hooks-and-events)
|
|
22
|
+
14. [Resets and HMR](#resets-and-hmr)
|
|
23
|
+
15. [Directive: `use ratelimit`](#directive-use-ratelimit)
|
|
24
|
+
16. [Defaults and edge cases](#defaults-and-edge-cases)
|
|
25
|
+
17. [Duration parsing](#duration-parsing)
|
|
26
|
+
18. [Exports](#exports)
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
Install the ratelimit plugin to get started:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @commandkit/ratelimit
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Setup
|
|
37
|
+
|
|
38
|
+
Add the ratelimit plugin to your CommandKit configuration and define a runtime config file.
|
|
39
|
+
|
|
40
|
+
### Quick start
|
|
41
|
+
|
|
42
|
+
Create an auto-loaded runtime config file (for example `ratelimit.ts`) and configure the default limiter:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// ratelimit.ts
|
|
46
|
+
import { configureRatelimit } from '@commandkit/ratelimit';
|
|
47
|
+
|
|
48
|
+
configureRatelimit({
|
|
49
|
+
defaultLimiter: {
|
|
50
|
+
maxRequests: 5,
|
|
51
|
+
interval: '1m',
|
|
52
|
+
scope: 'user',
|
|
53
|
+
algorithm: 'fixed-window',
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Register the plugin in your config:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// commandkit.config.ts
|
|
62
|
+
import { defineConfig } from 'commandkit';
|
|
63
|
+
import { ratelimit } from '@commandkit/ratelimit';
|
|
64
|
+
|
|
65
|
+
export default defineConfig({
|
|
66
|
+
plugins: [ratelimit()],
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The runtime plugin auto-loads `ratelimit.ts` or `ratelimit.js` on startup before commands execute.
|
|
71
|
+
|
|
72
|
+
## Runtime configuration lifecycle
|
|
73
|
+
|
|
74
|
+
### Runtime lifecycle diagram
|
|
75
|
+
|
|
76
|
+
```mermaid
|
|
77
|
+
graph TD
|
|
78
|
+
A[App startup] --> B[Auto-load ratelimit.ts/js]
|
|
79
|
+
B --> C["configureRatelimit()"]
|
|
80
|
+
C --> D["Runtime plugin activate()"]
|
|
81
|
+
D --> E[Resolve storage]
|
|
82
|
+
E --> F[Resolve limiter config]
|
|
83
|
+
F --> G[Consume algorithm]
|
|
84
|
+
G --> H[Aggregate result]
|
|
85
|
+
H --> I[Default response / hooks / events]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `configureRatelimit` is required
|
|
89
|
+
|
|
90
|
+
`RateLimitPlugin.activate()` throws if `configureRatelimit()` was not called. This is enforced to avoid silently running without your intended defaults.
|
|
91
|
+
|
|
92
|
+
### How configuration is stored
|
|
93
|
+
|
|
94
|
+
`configureRatelimit()` merges your config into an in-memory object and sets the configured flag. `getRateLimitConfig()` returns the current object, and `isRateLimitConfigured()` returns whether initialization has happened. If a runtime context is already active, `configureRatelimit()` updates it immediately.
|
|
95
|
+
|
|
96
|
+
### Runtime storage selection
|
|
97
|
+
|
|
98
|
+
Storage is resolved in this order:
|
|
99
|
+
|
|
100
|
+
| Order | Source | Notes |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| 1 | Limiter `storage` override | `RateLimitLimiterConfig.storage` for the command being executed. |
|
|
103
|
+
| 2 | Plugin `storage` option | `RateLimitPluginOptions.storage`. |
|
|
104
|
+
| 3 | Process default | Set via `setRateLimitStorage()` or `setDriver()`. |
|
|
105
|
+
| 4 | Default memory storage | Used unless `initializeDefaultStorage` or `initializeDefaultDriver` is `false`. |
|
|
106
|
+
|
|
107
|
+
If no storage is resolved and defaults are disabled, the plugin logs once and stores an empty result without limiting.
|
|
108
|
+
|
|
109
|
+
### Runtime helpers
|
|
110
|
+
|
|
111
|
+
These helpers are process-wide:
|
|
112
|
+
|
|
113
|
+
| Helper | Purpose |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| `configureRatelimit` | Set runtime options and update active runtime state. |
|
|
116
|
+
| `getRateLimitConfig` | Read the merged in-memory runtime config. |
|
|
117
|
+
| `isRateLimitConfigured` | Check whether `configureRatelimit()` was called. |
|
|
118
|
+
| `setRateLimitStorage` | Set the default storage for the process. |
|
|
119
|
+
| `getRateLimitStorage` | Get the process default storage (or `null`). |
|
|
120
|
+
| `setDriver` / `getDriver` | Aliases for `setRateLimitStorage` / `getRateLimitStorage`. |
|
|
121
|
+
| `setRateLimitRuntime` | Set the active runtime context for APIs and directives. |
|
|
122
|
+
| `getRateLimitRuntime` | Get the active runtime context (or `null`). |
|
|
123
|
+
|
|
124
|
+
## Basic usage
|
|
125
|
+
|
|
126
|
+
Use command metadata or the `use ratelimit` directive to enable rate limiting.
|
|
127
|
+
This section focuses on command metadata; see the directive section for
|
|
128
|
+
function-level usage.
|
|
129
|
+
|
|
130
|
+
### Command metadata and enablement
|
|
131
|
+
|
|
132
|
+
Enable rate limiting by setting `metadata.ratelimit`:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
export const metadata = {
|
|
136
|
+
ratelimit: {
|
|
137
|
+
maxRequests: 3,
|
|
138
|
+
interval: '10s',
|
|
139
|
+
scope: 'user',
|
|
140
|
+
algorithm: 'sliding-window',
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`metadata.ratelimit` can be one of:
|
|
146
|
+
|
|
147
|
+
| Value | Meaning |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| `false` or `undefined` | Plugin does nothing for this command. |
|
|
150
|
+
| `true` | Enable rate limiting using resolved defaults. |
|
|
151
|
+
| `RateLimitCommandConfig` | Enable rate limiting with command-level overrides. |
|
|
152
|
+
|
|
153
|
+
If `env.context` is missing in the execution environment, the plugin skips rate limiting.
|
|
154
|
+
|
|
155
|
+
### Named limiter example
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
configureRatelimit({
|
|
159
|
+
limiters: {
|
|
160
|
+
heavy: { maxRequests: 1, interval: '10s', algorithm: 'fixed-window' },
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
export const metadata = {
|
|
167
|
+
ratelimit: {
|
|
168
|
+
limiter: 'heavy',
|
|
169
|
+
scope: 'user',
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Configuration reference
|
|
175
|
+
|
|
176
|
+
### RateLimitPluginOptions
|
|
177
|
+
|
|
178
|
+
| Field | Type | Default or resolution | Notes |
|
|
179
|
+
| --- | --- | --- | --- |
|
|
180
|
+
| `defaultLimiter` | `RateLimitLimiterConfig` | `DEFAULT_LIMITER` when unset | Base limiter for all commands and directives. |
|
|
181
|
+
| `limiters` | `Record<string, RateLimitLimiterConfig>` | `undefined` | Named limiter presets. |
|
|
182
|
+
| `storage` | `RateLimitStorageConfig` | `undefined` | Resolved before default storage. |
|
|
183
|
+
| `keyPrefix` | `string` | `undefined` | Prepended before `rl:`. |
|
|
184
|
+
| `keyResolver` | `RateLimitKeyResolver` | `undefined` | Used for `custom` scope when the limiter does not override it. |
|
|
185
|
+
| `bypass` | `RateLimitBypassOptions` | `undefined` | Permanent allowlists and optional check. |
|
|
186
|
+
| `hooks` | `RateLimitHooks` | `undefined` | Lifecycle callbacks. |
|
|
187
|
+
| `onRateLimited` | `RateLimitResponseHandler` | `undefined` | Overrides default reply. |
|
|
188
|
+
| `queue` | `RateLimitQueueOptions` | `undefined` | If any queue config exists, `enabled` defaults to `true`. |
|
|
189
|
+
| `roleLimits` | `Record<string, RateLimitLimiterConfig>` | `undefined` | Base role limits. |
|
|
190
|
+
| `roleLimitStrategy` | `RateLimitRoleLimitStrategy` | `highest` when resolving | Used when multiple roles match. |
|
|
191
|
+
| `initializeDefaultStorage` | `boolean` | `true` | Disable to prevent memory fallback. |
|
|
192
|
+
| `initializeDefaultDriver` | `boolean` | `true` | Alias for `initializeDefaultStorage`. |
|
|
193
|
+
|
|
194
|
+
### RateLimitLimiterConfig
|
|
195
|
+
|
|
196
|
+
| Field | Type | Default or resolution | Notes |
|
|
197
|
+
| --- | --- | --- | --- |
|
|
198
|
+
| `maxRequests` | `number` | `10` when missing or `<= 0` | Used by fixed and sliding windows. |
|
|
199
|
+
| `interval` | `DurationLike` | `60s` when missing or invalid | Parsed and clamped to `>= 1ms`. |
|
|
200
|
+
| `scope` | `RateLimitScope` or `RateLimitScope[]` | `user` | Arrays are deduplicated. |
|
|
201
|
+
| `algorithm` | `RateLimitAlgorithmType` | `fixed-window` | Unknown values fall back to fixed-window. |
|
|
202
|
+
| `burst` | `number` | `maxRequests` when missing or `<= 0` | Capacity for token or leaky buckets. |
|
|
203
|
+
| `refillRate` | `number` | `maxRequests / intervalSeconds` | Must be `> 0` for token bucket. |
|
|
204
|
+
| `leakRate` | `number` | `maxRequests / intervalSeconds` | Must be `> 0` for leaky bucket. |
|
|
205
|
+
| `keyResolver` | `RateLimitKeyResolver` | `undefined` | Used only for `custom` scope. |
|
|
206
|
+
| `keyPrefix` | `string` | `undefined` | Overrides plugin prefix for this limiter. |
|
|
207
|
+
| `storage` | `RateLimitStorageConfig` | `undefined` | Overrides storage for this limiter. |
|
|
208
|
+
| `violations` | `ViolationOptions` | `undefined` | Enables escalation unless `escalate` is `false`. |
|
|
209
|
+
| `queue` | `RateLimitQueueOptions` | `undefined` | Overrides queue settings at this layer. |
|
|
210
|
+
| `windows` | `RateLimitWindowConfig[]` | `undefined` | Enables multi-window behavior. |
|
|
211
|
+
| `roleLimits` | `Record<string, RateLimitLimiterConfig>` | `undefined` | Role overrides at this layer. |
|
|
212
|
+
| `roleLimitStrategy` | `RateLimitRoleLimitStrategy` | `highest` when resolving | Used when role limits match. |
|
|
213
|
+
|
|
214
|
+
### RateLimitWindowConfig
|
|
215
|
+
|
|
216
|
+
| Field | Type | Default or resolution | Notes |
|
|
217
|
+
| --- | --- | --- | --- |
|
|
218
|
+
| `id` | `string` | `w1`, `w2`, ... | Auto-generated if empty or missing. |
|
|
219
|
+
| `maxRequests` | `number` | Inherits from base limiter | Applies only to this window. |
|
|
220
|
+
| `interval` | `DurationLike` | Inherits from base limiter | Parsed like the base limiter. |
|
|
221
|
+
| `algorithm` | `RateLimitAlgorithmType` | Inherits from base limiter | Usually keep consistent across windows. |
|
|
222
|
+
| `burst` | `number` | Inherits from base limiter | Used for token or leaky buckets. |
|
|
223
|
+
| `refillRate` | `number` | Inherits from base limiter | Must be `> 0` for token bucket. |
|
|
224
|
+
| `leakRate` | `number` | Inherits from base limiter | Must be `> 0` for leaky bucket. |
|
|
225
|
+
| `violations` | `ViolationOptions` | Inherits from base limiter | Overrides escalation for this window. |
|
|
226
|
+
|
|
227
|
+
### RateLimitQueueOptions
|
|
228
|
+
|
|
229
|
+
| Field | Type | Default or resolution | Notes |
|
|
230
|
+
| --- | --- | --- | --- |
|
|
231
|
+
| `enabled` | `boolean` | `true` when any queue config exists | Otherwise `false`. |
|
|
232
|
+
| `maxSize` | `number` | `3` and clamped to `>= 1` | Queue size is pending plus running. |
|
|
233
|
+
| `timeout` | `DurationLike` | `30s` and clamped to `>= 1ms` | Per queued task. |
|
|
234
|
+
| `deferInteraction` | `boolean` | `true` unless explicitly `false` | Only used for interactions. |
|
|
235
|
+
| `ephemeral` | `boolean` | `true` unless explicitly `false` | Applies to deferred replies. |
|
|
236
|
+
| `concurrency` | `number` | `1` and clamped to `>= 1` | Per queue key. |
|
|
237
|
+
|
|
238
|
+
### ViolationOptions
|
|
239
|
+
|
|
240
|
+
| Field | Type | Default or resolution | Notes |
|
|
241
|
+
| --- | --- | --- | --- |
|
|
242
|
+
| `escalate` | `boolean` | `true` when `violations` is set | Set `false` to disable escalation. |
|
|
243
|
+
| `maxViolations` | `number` | `5` | Maximum escalation steps. |
|
|
244
|
+
| `escalationMultiplier` | `number` | `2` | Multiplies cooldown per repeated violation. |
|
|
245
|
+
| `resetAfter` | `DurationLike` | `1h` | TTL for violation state. |
|
|
246
|
+
|
|
247
|
+
### RateLimitCommandConfig
|
|
248
|
+
|
|
249
|
+
`RateLimitCommandConfig` extends `RateLimitLimiterConfig` and adds:
|
|
250
|
+
|
|
251
|
+
| Field | Type | Default or resolution | Notes |
|
|
252
|
+
| --- | --- | --- | --- |
|
|
253
|
+
| `limiter` | `string` | `undefined` | References a named limiter in `limiters`. |
|
|
254
|
+
|
|
255
|
+
### Result shapes
|
|
256
|
+
|
|
257
|
+
RateLimitStoreValue:
|
|
258
|
+
|
|
259
|
+
| Field | Type | Meaning |
|
|
260
|
+
| --- | --- | --- |
|
|
261
|
+
| `limited` | `boolean` | `true` if any scope or window was limited. |
|
|
262
|
+
| `remaining` | `number` | Minimum remaining across all results. |
|
|
263
|
+
| `resetAt` | `number` | Latest reset timestamp across all results. |
|
|
264
|
+
| `retryAfter` | `number` | Max retry delay across limited results. |
|
|
265
|
+
| `results` | `RateLimitResult[]` | Individual results per scope and window. |
|
|
266
|
+
|
|
267
|
+
RateLimitResult:
|
|
268
|
+
|
|
269
|
+
| Field | Type | Meaning |
|
|
270
|
+
| --- | --- | --- |
|
|
271
|
+
| `key` | `string` | Storage key used for the limiter. |
|
|
272
|
+
| `scope` | `RateLimitScope` | Scope applied for the limiter. |
|
|
273
|
+
| `algorithm` | `RateLimitAlgorithmType` | Algorithm used for the limiter. |
|
|
274
|
+
| `windowId` | `string` | Present for multi-window limits. |
|
|
275
|
+
| `limited` | `boolean` | Whether this limiter hit its limit. |
|
|
276
|
+
| `remaining` | `number` | Remaining requests or capacity. |
|
|
277
|
+
| `resetAt` | `number` | Absolute reset timestamp in ms. |
|
|
278
|
+
| `retryAfter` | `number` | Delay until retry is allowed, in ms. |
|
|
279
|
+
| `limit` | `number` | `maxRequests` for fixed and sliding, `burst` for token and leaky buckets. |
|
|
280
|
+
|
|
281
|
+
## Limiter resolution and role strategy
|
|
282
|
+
|
|
283
|
+
Limiter configuration is layered in this exact order, with later layers overriding earlier ones:
|
|
284
|
+
|
|
285
|
+
| Order | Source | Notes |
|
|
286
|
+
| --- | --- | --- |
|
|
287
|
+
| 1 | `DEFAULT_LIMITER` | Base defaults. |
|
|
288
|
+
| 2 | `defaultLimiter` | Runtime defaults. |
|
|
289
|
+
| 3 | Named limiter | When `metadata.ratelimit.limiter` is set. |
|
|
290
|
+
| 4 | Command overrides | `metadata.ratelimit` config. |
|
|
291
|
+
| 5 | Role override | Selected by role strategy. |
|
|
292
|
+
|
|
293
|
+
### Limiter resolution diagram
|
|
294
|
+
|
|
295
|
+
```mermaid
|
|
296
|
+
graph TD
|
|
297
|
+
A[DEFAULT_LIMITER] --> B[defaultLimiter]
|
|
298
|
+
B --> C[Named limiter]
|
|
299
|
+
C --> D[Command overrides]
|
|
300
|
+
D --> E["Role override (strategy)"]
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Role limits are merged in this order, with later maps overriding earlier ones for the same role id:
|
|
304
|
+
|
|
305
|
+
| Order | Source |
|
|
306
|
+
| --- | --- |
|
|
307
|
+
| 1 | Plugin `roleLimits` |
|
|
308
|
+
| 2 | `defaultLimiter.roleLimits` |
|
|
309
|
+
| 3 | Named limiter `roleLimits` |
|
|
310
|
+
| 4 | Command `roleLimits` |
|
|
311
|
+
|
|
312
|
+
Role strategies:
|
|
313
|
+
|
|
314
|
+
| Strategy | Selection rule |
|
|
315
|
+
| --- | --- |
|
|
316
|
+
| `highest` | Picks the role with the highest request rate (`maxRequests / intervalMs`). |
|
|
317
|
+
| `lowest` | Picks the role with the lowest request rate. |
|
|
318
|
+
| `first` | Uses insertion order of the merged role limits object. |
|
|
319
|
+
|
|
320
|
+
For multi-window limiters, the score uses the minimum rate across windows.
|
|
321
|
+
|
|
322
|
+
## Scopes and keying
|
|
323
|
+
|
|
324
|
+
Supported scopes:
|
|
325
|
+
|
|
326
|
+
| Scope | Required IDs | Key format (without `keyPrefix`) | Skip behavior |
|
|
327
|
+
| --- | --- | --- | --- |
|
|
328
|
+
| `user` | `userId` | `rl:user:{userId}:{commandName}` | Skips if `userId` is missing. |
|
|
329
|
+
| `guild` | `guildId` | `rl:guild:{guildId}:{commandName}` | Skips if `guildId` is missing. |
|
|
330
|
+
| `channel` | `channelId` | `rl:channel:{channelId}:{commandName}` | Skips if `channelId` is missing. |
|
|
331
|
+
| `global` | none | `rl:global:{commandName}` | Never skipped. |
|
|
332
|
+
| `user-guild` | `userId`, `guildId` | `rl:user:{userId}:guild:{guildId}:{commandName}` | Skips if either id is missing. |
|
|
333
|
+
| `custom` | `keyResolver` | `keyResolver(ctx, command, source)` | Skips if resolver is missing or returns falsy. |
|
|
334
|
+
|
|
335
|
+
Keying notes:
|
|
336
|
+
|
|
337
|
+
- `DEFAULT_KEY_PREFIX` is always included in the base format.
|
|
338
|
+
- `keyPrefix` is concatenated before `rl:` as-is, so include a trailing separator if you want one.
|
|
339
|
+
- Multi-window limits append `:w:{windowId}`.
|
|
340
|
+
|
|
341
|
+
### Exemption keys
|
|
342
|
+
|
|
343
|
+
Temporary exemptions are stored under `rl:exempt:{scope}:{id}` (plus optional `keyPrefix`).
|
|
344
|
+
|
|
345
|
+
| Exemption scope | Key format | Notes |
|
|
346
|
+
| --- | --- | --- |
|
|
347
|
+
| `user` | `rl:exempt:user:{userId}` | Resolved from the source user id. |
|
|
348
|
+
| `guild` | `rl:exempt:guild:{guildId}` | Resolved from the guild id. |
|
|
349
|
+
| `role` | `rl:exempt:role:{roleId}` | Resolved from all member roles. |
|
|
350
|
+
| `channel` | `rl:exempt:channel:{channelId}` | Resolved from the channel id. |
|
|
351
|
+
| `category` | `rl:exempt:category:{categoryId}` | Resolved from the parent category id. |
|
|
352
|
+
|
|
353
|
+
## Algorithms
|
|
354
|
+
|
|
355
|
+
### Algorithm matrix
|
|
356
|
+
|
|
357
|
+
| Algorithm | Required config | Storage requirements | `limit` value | Notes |
|
|
358
|
+
| --- | --- | --- | --- | --- |
|
|
359
|
+
| `fixed-window` | `maxRequests`, `interval` | `consumeFixedWindow` or `incr` or `get` and `set` | `maxRequests` | Fallback uses per-process lock and optimistic versioning. |
|
|
360
|
+
| `sliding-window` | `maxRequests`, `interval` | `consumeSlidingWindowLog` or `zRemRangeByScore` + `zCard` + `zAdd` | `maxRequests` | Throws if sorted-set support is missing. |
|
|
361
|
+
| `token-bucket` | `burst`, `refillRate` | `get` and `set` | `burst` | Throws if `refillRate <= 0`. |
|
|
362
|
+
| `leaky-bucket` | `burst`, `leakRate` | `get` and `set` | `burst` | Throws if `leakRate <= 0`. |
|
|
363
|
+
|
|
364
|
+
### Fixed window
|
|
365
|
+
|
|
366
|
+
Execution path:
|
|
367
|
+
|
|
368
|
+
1. If `consumeFixedWindow` exists, it is used.
|
|
369
|
+
2. Else if `incr` exists, it is used.
|
|
370
|
+
3. Else a fallback uses `get` and `set` with a per-process lock.
|
|
371
|
+
|
|
372
|
+
The limiter is considered limited when `count > maxRequests`. The fallback path retries up to five times with optimistic versioning and is serialized only within the current process.
|
|
373
|
+
|
|
374
|
+
#### Fixed window fallback diagram
|
|
375
|
+
|
|
376
|
+
```mermaid
|
|
377
|
+
graph TD
|
|
378
|
+
A[Consume fixed-window] --> B{consumeFixedWindow?}
|
|
379
|
+
B -- Yes --> C[Use consumeFixedWindow]
|
|
380
|
+
B -- No --> D{incr?}
|
|
381
|
+
D -- Yes --> E[Use incr]
|
|
382
|
+
D -- No --> F["get + set fallback (per-process lock)"]
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Sliding window log
|
|
386
|
+
|
|
387
|
+
Execution path:
|
|
388
|
+
|
|
389
|
+
1. If `consumeSlidingWindowLog` exists, it is used (atomic).
|
|
390
|
+
2. Else a sorted-set fallback uses `zRemRangeByScore`, `zCard`, and `zAdd`.
|
|
391
|
+
|
|
392
|
+
If sorted-set methods are missing, the algorithm throws. If `zRangeByScore` is available, it is used to compute an accurate oldest timestamp for `resetAt`; otherwise `resetAt` defaults to `now + window`. The fallback is serialized per process but is not atomic across processes.
|
|
393
|
+
|
|
394
|
+
#### Sliding window fallback diagram
|
|
395
|
+
|
|
396
|
+
```mermaid
|
|
397
|
+
graph TD
|
|
398
|
+
A[Consume sliding-window] --> B{consumeSlidingWindowLog?}
|
|
399
|
+
B -- Yes --> C[Use consumeSlidingWindowLog]
|
|
400
|
+
B -- No --> D{zset methods?}
|
|
401
|
+
D -- No --> E[Throw error]
|
|
402
|
+
D -- Yes --> F[zRemRangeByScore + zCard + zAdd fallback]
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### Token bucket
|
|
406
|
+
|
|
407
|
+
Token bucket uses a stored `tokens` and `lastRefill` state. On each consume, tokens refill based on elapsed time and `refillRate`. If the bucket has fewer than one token, the request is limited and `retryAfter` is computed from the time required to refill one token.
|
|
408
|
+
|
|
409
|
+
### Leaky bucket
|
|
410
|
+
|
|
411
|
+
Leaky bucket uses a stored `level` and `lastLeak` state. Each request adds one token, and the bucket drains at `leakRate`. If adding would exceed `capacity`, the request is limited and `retryAfter` is computed from the time required to drain the overflow.
|
|
412
|
+
|
|
413
|
+
### Multi-window limits
|
|
414
|
+
|
|
415
|
+
Use `windows` to enforce multiple windows simultaneously:
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
configureRatelimit({
|
|
419
|
+
defaultLimiter: {
|
|
420
|
+
scope: 'user',
|
|
421
|
+
algorithm: 'sliding-window',
|
|
422
|
+
windows: [
|
|
423
|
+
{ id: 'short', maxRequests: 10, interval: '1m' },
|
|
424
|
+
{ id: 'long', maxRequests: 1000, interval: '1d' },
|
|
425
|
+
],
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
If a window `id` is omitted, the plugin generates `w1`, `w2`, and so on. Window ids are part of the storage key and appear in results.
|
|
431
|
+
|
|
432
|
+
## Storage
|
|
433
|
+
|
|
434
|
+
### Storage interface
|
|
435
|
+
|
|
436
|
+
Required methods:
|
|
437
|
+
|
|
438
|
+
| Method | Used by | Notes |
|
|
439
|
+
| --- | --- | --- |
|
|
440
|
+
| `get` | All algorithms | Returns stored value or `null`. |
|
|
441
|
+
| `set` | All algorithms | Optional `ttlMs` controls expiry. |
|
|
442
|
+
| `delete` | Resets and algorithm resets | Removes stored state. |
|
|
443
|
+
|
|
444
|
+
Optional methods and features:
|
|
445
|
+
|
|
446
|
+
| Method | Feature | Notes |
|
|
447
|
+
| --- | --- | --- |
|
|
448
|
+
| `consumeFixedWindow` | Fixed-window atomic consume | Used before `incr` and fallback. |
|
|
449
|
+
| `incr` | Fixed-window efficiency | Returns count and TTL. |
|
|
450
|
+
| `consumeSlidingWindowLog` | Sliding-window atomic consume | Preferred over sorted-set fallback. |
|
|
451
|
+
| `zAdd` / `zRemRangeByScore` / `zCard` | Sliding-window fallback | Required when `consumeSlidingWindowLog` is absent. |
|
|
452
|
+
| `zRangeByScore` | Sliding-window reset accuracy | Improves `resetAt` computation. |
|
|
453
|
+
| `ttl` | Exemption listing | Used for `expiresInMs`. |
|
|
454
|
+
| `expire` | Sliding-window fallback | Keeps sorted-set keys from growing indefinitely. |
|
|
455
|
+
| `deleteByPrefix` / `deleteByPattern` | Resets | Required by `resetAllRateLimits` and HMR. |
|
|
456
|
+
| `keysByPrefix` | Exemption listing | Required for listing without a specific id. |
|
|
457
|
+
|
|
458
|
+
### Capability matrix
|
|
459
|
+
|
|
460
|
+
| Feature | Requires | Memory | Redis | Fallback |
|
|
461
|
+
| --- | --- | --- | --- | --- |
|
|
462
|
+
| Fixed-window atomic consume | `consumeFixedWindow` | Yes | Yes | Conditional (both storages) |
|
|
463
|
+
| Fixed-window `incr` | `incr` | Yes | Yes | Conditional (both storages) |
|
|
464
|
+
| Sliding-window atomic consume | `consumeSlidingWindowLog` | Yes | Yes | Conditional (both storages) |
|
|
465
|
+
| Sliding-window fallback | `zAdd` + `zRemRangeByScore` + `zCard` | Yes | Yes | Conditional (both storages) |
|
|
466
|
+
| TTL visibility | `ttl` | Yes | Yes | Conditional (both storages) |
|
|
467
|
+
| Prefix or pattern deletes | `deleteByPrefix` or `deleteByPattern` | Yes | Yes | Conditional (both storages) |
|
|
468
|
+
| Exemption listing | `keysByPrefix` | Yes | Yes | Conditional (both storages) |
|
|
469
|
+
|
|
470
|
+
### Capability overview diagram
|
|
471
|
+
|
|
472
|
+
```mermaid
|
|
473
|
+
graph TD
|
|
474
|
+
A[Storage API] --> B[Required: get / set / delete]
|
|
475
|
+
A --> C[Optional methods]
|
|
476
|
+
C --> D[Fixed window atomic: consumeFixedWindow / incr]
|
|
477
|
+
C --> E[Sliding window atomic: consumeSlidingWindowLog]
|
|
478
|
+
C --> F[Sliding window fallback: zAdd + zRemRangeByScore + zCard]
|
|
479
|
+
C --> G[Listing & TTL: keysByPrefix / ttl]
|
|
480
|
+
C --> H[Bulk reset: deleteByPrefix / deleteByPattern]
|
|
481
|
+
I[Fallback storage] --> J[Uses primary + secondary]
|
|
482
|
+
J --> K[Each optional method must exist on both]
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Memory storage
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
import { MemoryRateLimitStorage, setRateLimitStorage } from '@commandkit/ratelimit';
|
|
489
|
+
|
|
490
|
+
setRateLimitStorage(new MemoryRateLimitStorage());
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Notes:
|
|
494
|
+
|
|
495
|
+
- In-memory only; not safe for multi-process deployments.
|
|
496
|
+
- Implements TTL and sorted-set helpers.
|
|
497
|
+
- `deleteByPattern` supports a simple `*` wildcard, not full glob syntax.
|
|
498
|
+
|
|
499
|
+
### Redis storage
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
import { RedisRateLimitStorage } from '@commandkit/ratelimit/redis';
|
|
503
|
+
import { setRateLimitStorage } from '@commandkit/ratelimit';
|
|
504
|
+
|
|
505
|
+
setRateLimitStorage(
|
|
506
|
+
new RedisRateLimitStorage({ host: 'localhost', port: 6379 }),
|
|
507
|
+
);
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Notes:
|
|
511
|
+
|
|
512
|
+
- Stores values as JSON.
|
|
513
|
+
- Uses Lua scripts for atomic fixed and sliding windows.
|
|
514
|
+
- Uses `SCAN` for prefix and pattern deletes and listing.
|
|
515
|
+
|
|
516
|
+
### Fallback storage
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
import { FallbackRateLimitStorage } from '@commandkit/ratelimit/fallback';
|
|
520
|
+
import { MemoryRateLimitStorage } from '@commandkit/ratelimit/memory';
|
|
521
|
+
import { RedisRateLimitStorage } from '@commandkit/ratelimit/redis';
|
|
522
|
+
import { setRateLimitStorage } from '@commandkit/ratelimit';
|
|
523
|
+
|
|
524
|
+
const primary = new RedisRateLimitStorage({ host: 'localhost', port: 6379 });
|
|
525
|
+
const secondary = new MemoryRateLimitStorage();
|
|
526
|
+
|
|
527
|
+
setRateLimitStorage(new FallbackRateLimitStorage(primary, secondary));
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
Notes:
|
|
531
|
+
|
|
532
|
+
- Every optional method must exist on both storages or the fallback wrapper throws.
|
|
533
|
+
- Primary errors are logged at most once per `cooldownMs` window (default 30s).
|
|
534
|
+
|
|
535
|
+
## Queue mode
|
|
536
|
+
|
|
537
|
+
Queue mode retries commands instead of rejecting immediately.
|
|
538
|
+
|
|
539
|
+
### Queue defaults and clamps
|
|
540
|
+
|
|
541
|
+
| Field | Default | Clamp | Notes |
|
|
542
|
+
| --- | --- | --- | --- |
|
|
543
|
+
| `enabled` | `true` if any queue config exists | n/a | Otherwise `false`. |
|
|
544
|
+
| `maxSize` | `3` | `>= 1` | Queue size is pending plus running. |
|
|
545
|
+
| `timeout` | `30s` | `>= 1ms` | Per queued task. |
|
|
546
|
+
| `deferInteraction` | `true` | n/a | Only applies to interactions. |
|
|
547
|
+
| `ephemeral` | `true` | n/a | Applies to deferred replies. |
|
|
548
|
+
| `concurrency` | `1` | `>= 1` | Per queue key. |
|
|
549
|
+
|
|
550
|
+
### Queue flow
|
|
551
|
+
|
|
552
|
+
1. Rate limit is evaluated and an aggregate result is computed.
|
|
553
|
+
2. If limited and queueing is enabled, the plugin tries to enqueue.
|
|
554
|
+
3. If the queue is full, it falls back to immediate rate-limit handling.
|
|
555
|
+
4. When queued, the interaction is deferred if it is repliable and not already replied or deferred.
|
|
556
|
+
5. The queued task waits `retryAfter`, then re-checks the limiter; if still limited it waits at least 250ms and retries until timeout.
|
|
557
|
+
|
|
558
|
+
### Queue flow diagram
|
|
559
|
+
|
|
560
|
+
```mermaid
|
|
561
|
+
graph TD
|
|
562
|
+
A[Evaluate limiter] --> B{Limited?}
|
|
563
|
+
B -- No --> C[Allow command]
|
|
564
|
+
B -- Yes --> D{Queue enabled?}
|
|
565
|
+
D -- No --> E[Rate-limit response]
|
|
566
|
+
D -- Yes --> F{Queue has capacity?}
|
|
567
|
+
F -- No --> E
|
|
568
|
+
F -- Yes --> G[Enqueue + defer if repliable]
|
|
569
|
+
G --> H[Wait retryAfter]
|
|
570
|
+
H --> I{Still limited?}
|
|
571
|
+
I -- No --> C
|
|
572
|
+
I -- Yes --> J[Wait >= 250ms]
|
|
573
|
+
J --> K{Timed out?}
|
|
574
|
+
K -- No --> H
|
|
575
|
+
K -- Yes --> E
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
## Violations and escalation
|
|
579
|
+
|
|
580
|
+
Violation escalation is stored under `violation:{key}` and uses these defaults:
|
|
581
|
+
|
|
582
|
+
| Option | Default | Meaning |
|
|
583
|
+
| --- | --- | --- |
|
|
584
|
+
| `maxViolations` | `5` | Maximum escalation steps. |
|
|
585
|
+
| `escalationMultiplier` | `2` | Multiplier per repeated violation. |
|
|
586
|
+
| `resetAfter` | `1h` | TTL for violation state. |
|
|
587
|
+
| `escalate` | `true` when `violations` is set | Set `false` to disable escalation. |
|
|
588
|
+
|
|
589
|
+
Formula:
|
|
590
|
+
|
|
591
|
+
`cooldown = baseRetryAfter * multiplier^(count - 1)`
|
|
592
|
+
|
|
593
|
+
If escalation produces a later `resetAt` than the algorithm returned, the result is updated so `resetAt` and `retryAfter` stay accurate.
|
|
594
|
+
|
|
595
|
+
## Bypass and exemptions
|
|
596
|
+
|
|
597
|
+
Bypass order is always:
|
|
598
|
+
|
|
599
|
+
1. `bypass.userIds`, `bypass.guildIds`, and `bypass.roleIds`.
|
|
600
|
+
2. Temporary exemptions stored in storage.
|
|
601
|
+
3. `bypass.check(source)`.
|
|
602
|
+
|
|
603
|
+
Bypass example:
|
|
604
|
+
|
|
605
|
+
```ts
|
|
606
|
+
configureRatelimit({
|
|
607
|
+
bypass: {
|
|
608
|
+
userIds: ['USER_ID'],
|
|
609
|
+
guildIds: ['GUILD_ID'],
|
|
610
|
+
roleIds: ['ROLE_ID'],
|
|
611
|
+
check: (source) => source.channelId === 'ALLOWLIST_CHANNEL',
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
Temporary exemptions:
|
|
617
|
+
|
|
618
|
+
```ts
|
|
619
|
+
import { grantRateLimitExemption } from '@commandkit/ratelimit';
|
|
620
|
+
|
|
621
|
+
await grantRateLimitExemption({
|
|
622
|
+
scope: 'user',
|
|
623
|
+
id: 'USER_ID',
|
|
624
|
+
duration: '1h',
|
|
625
|
+
});
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
Listing behavior:
|
|
629
|
+
|
|
630
|
+
- `listRateLimitExemptions({ scope, id })` reads a single key directly.
|
|
631
|
+
- `listRateLimitExemptions({ scope })` scans by prefix and requires `keysByPrefix`.
|
|
632
|
+
- `expiresInMs` is `null` when `ttl` is not supported.
|
|
633
|
+
|
|
634
|
+
## Responses, hooks, and events
|
|
635
|
+
|
|
636
|
+
### Default response behavior
|
|
637
|
+
|
|
638
|
+
| Source | Conditions | Action |
|
|
639
|
+
| --- | --- | --- |
|
|
640
|
+
| Message | Channel is sendable | `reply()` with cooldown embed. |
|
|
641
|
+
| Interaction | Repliable and not replied/deferred | `reply()` with ephemeral cooldown embed. |
|
|
642
|
+
| Interaction | Repliable and already replied/deferred | `followUp()` with ephemeral cooldown embed. |
|
|
643
|
+
| Interaction | Not repliable | No response. |
|
|
644
|
+
|
|
645
|
+
The default embed title is `:hourglass_flowing_sand: You are on cooldown` and the description uses a relative timestamp based on `resetAt`.
|
|
646
|
+
|
|
647
|
+
### Hooks
|
|
648
|
+
|
|
649
|
+
| Hook | Called when | Notes |
|
|
650
|
+
| --- | --- | --- |
|
|
651
|
+
| `onAllowed` | Command is allowed | Receives the first result. |
|
|
652
|
+
| `onRateLimited` | Command is limited | Receives the first limited result. |
|
|
653
|
+
| `onViolation` | A violation is recorded | Receives key and violation count. |
|
|
654
|
+
| `onReset` | `resetRateLimit` succeeds | Not called by `resetAllRateLimits`. |
|
|
655
|
+
| `onStorageError` | Storage operation fails | `fallbackUsed` is `false` in runtime plugin paths. |
|
|
656
|
+
|
|
657
|
+
### Analytics events
|
|
658
|
+
|
|
659
|
+
The runtime plugin calls `ctx.commandkit.analytics.track(...)` with:
|
|
660
|
+
|
|
661
|
+
| Event name | When |
|
|
662
|
+
| --- | --- |
|
|
663
|
+
| `ratelimit_allowed` | After an allowed consume. |
|
|
664
|
+
| `ratelimit_hit` | After a limited consume. |
|
|
665
|
+
| `ratelimit_violation` | When escalation records a violation. |
|
|
666
|
+
|
|
667
|
+
### Event bus
|
|
668
|
+
|
|
669
|
+
A `ratelimited` event is emitted on the `ratelimits` channel:
|
|
670
|
+
|
|
671
|
+
```ts
|
|
672
|
+
commandkit.events
|
|
673
|
+
.to('ratelimits')
|
|
674
|
+
.on('ratelimited', ({ key, result, source, aggregate, commandName, queued }) => {
|
|
675
|
+
console.log(key, commandName, queued, aggregate.retryAfter);
|
|
676
|
+
});
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
Payload fields include `key`, `result`, `source`, `aggregate`, `commandName`, and `queued`.
|
|
680
|
+
|
|
681
|
+
## Resets and HMR
|
|
682
|
+
|
|
683
|
+
### `resetRateLimit`
|
|
684
|
+
|
|
685
|
+
`resetRateLimit` clears the base key, its `violation:` key, and any window variants. It accepts either a raw `key` or a scope-derived key.
|
|
686
|
+
|
|
687
|
+
| Mode | Required params | Notes |
|
|
688
|
+
| --- | --- | --- |
|
|
689
|
+
| Direct | `key` | Resets `key`, `violation:key`, and window variants. |
|
|
690
|
+
| Scoped | `scope` + `commandName` + required ids | Throws if identifiers are missing. |
|
|
691
|
+
|
|
692
|
+
### `resetAllRateLimits`
|
|
693
|
+
|
|
694
|
+
`resetAllRateLimits` supports several modes and requires storage delete helpers:
|
|
695
|
+
|
|
696
|
+
| Mode | Required params | Storage requirement |
|
|
697
|
+
| --- | --- | --- |
|
|
698
|
+
| Pattern | `pattern` | `deleteByPattern` |
|
|
699
|
+
| Prefix | `prefix` | `deleteByPrefix` |
|
|
700
|
+
| Command name | `commandName` | `deleteByPattern` |
|
|
701
|
+
| Scope | `scope` + required ids | `deleteByPrefix` |
|
|
702
|
+
|
|
703
|
+
### HMR reset behavior
|
|
704
|
+
|
|
705
|
+
When a command file is hot-reloaded, the plugin deletes keys that match:
|
|
706
|
+
|
|
707
|
+
- `*:{commandName}`
|
|
708
|
+
- `violation:*:{commandName}`
|
|
709
|
+
- `*:{commandName}:w:*`
|
|
710
|
+
- `violation:*:{commandName}:w:*`
|
|
711
|
+
|
|
712
|
+
HMR reset requires `deleteByPattern`. If the storage does not support pattern deletes, nothing is cleared.
|
|
713
|
+
|
|
714
|
+
## Directive: `use ratelimit`
|
|
715
|
+
|
|
716
|
+
The compiler plugin (`UseRateLimitDirectivePlugin`) uses `CommonDirectiveTransformer` with `directive = "use ratelimit"` and `importName = "$ckitirl"`. It transforms async functions only.
|
|
717
|
+
|
|
718
|
+
The runtime wrapper:
|
|
719
|
+
|
|
720
|
+
- Uses the runtime default limiter (merged with `DEFAULT_LIMITER`).
|
|
721
|
+
- Generates a per-function key `rl:fn:{uuid}` and applies `keyPrefix` if present.
|
|
722
|
+
- Aggregates results across windows and throws `RateLimitError` when limited.
|
|
723
|
+
- Caches the wrapper per function and exposes it as `globalThis.$ckitirl`.
|
|
724
|
+
|
|
725
|
+
Example:
|
|
726
|
+
|
|
727
|
+
```ts
|
|
728
|
+
import { RateLimitError } from '@commandkit/ratelimit';
|
|
729
|
+
|
|
730
|
+
const heavy = async () => {
|
|
731
|
+
'use ratelimit';
|
|
732
|
+
return 'ok';
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
try {
|
|
736
|
+
await heavy();
|
|
737
|
+
} catch (error) {
|
|
738
|
+
if (error instanceof RateLimitError) {
|
|
739
|
+
console.log(error.result.retryAfter);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
## Defaults and edge cases
|
|
745
|
+
|
|
746
|
+
### Defaults
|
|
747
|
+
|
|
748
|
+
| Setting | Default |
|
|
749
|
+
| --- | --- |
|
|
750
|
+
| `maxRequests` | `10` |
|
|
751
|
+
| `interval` | `60s` |
|
|
752
|
+
| `algorithm` | `fixed-window` |
|
|
753
|
+
| `scope` | `user` |
|
|
754
|
+
| `DEFAULT_KEY_PREFIX` | `rl:` |
|
|
755
|
+
| `RATELIMIT_STORE_KEY` | `ratelimit` |
|
|
756
|
+
| `roleLimitStrategy` | `highest` |
|
|
757
|
+
| `queue.maxSize` | `3` |
|
|
758
|
+
| `queue.timeout` | `30s` |
|
|
759
|
+
| `queue.deferInteraction` | `true` |
|
|
760
|
+
| `queue.ephemeral` | `true` |
|
|
761
|
+
| `queue.concurrency` | `1` |
|
|
762
|
+
| `initializeDefaultStorage` | `true` |
|
|
763
|
+
|
|
764
|
+
### Edge cases
|
|
765
|
+
|
|
766
|
+
1. If no storage is configured and default storage is disabled, the plugin logs once and stores an empty result without limiting.
|
|
767
|
+
2. If no scope key can be resolved, the plugin stores an empty result and skips limiting.
|
|
768
|
+
3. If storage errors occur during consume, `onStorageError` is invoked and the plugin skips limiting for that execution.
|
|
769
|
+
4. For token and leaky buckets, `limit` equals `burst`. For fixed and sliding windows, `limit` equals `maxRequests`.
|
|
770
|
+
|
|
771
|
+
## Duration parsing
|
|
772
|
+
|
|
773
|
+
`DurationLike` accepts numbers (milliseconds) or strings parsed by `ms`, plus custom units for weeks and months.
|
|
774
|
+
|
|
775
|
+
| Unit | Meaning |
|
|
776
|
+
| --- | --- |
|
|
777
|
+
| `ms`, `s`, `m`, `h`, `d` | Standard `ms` units. |
|
|
778
|
+
| `w`, `week`, `weeks` | 7 days. |
|
|
779
|
+
| `mo`, `month`, `months` | 30 days. |
|
|
780
|
+
|
|
781
|
+
## Exports
|
|
782
|
+
|
|
783
|
+
| Export | Description |
|
|
784
|
+
| --- | --- |
|
|
785
|
+
| `ratelimit` | Plugin factory returning compiler + runtime plugins. |
|
|
786
|
+
| `RateLimitPlugin` | Runtime plugin class. |
|
|
787
|
+
| `UseRateLimitDirectivePlugin` | Compiler plugin for `use ratelimit`. |
|
|
788
|
+
| `RateLimitEngine` | Algorithm coordinator with escalation handling. |
|
|
789
|
+
| Algorithm classes | Fixed, sliding, token bucket, and leaky bucket implementations. |
|
|
790
|
+
| Storage classes | Memory, Redis, and fallback storage. |
|
|
791
|
+
| Runtime helpers | `configureRatelimit`, `setRateLimitStorage`, `getRateLimitRuntime`, and more. |
|
|
792
|
+
| API helpers | `getRateLimitInfo`, resets, and exemption helpers. |
|
|
793
|
+
| `RateLimitError` | Error thrown by the directive wrapper. |
|
|
794
|
+
|
|
795
|
+
Subpath exports:
|
|
796
|
+
|
|
797
|
+
- `@commandkit/ratelimit/redis`
|
|
798
|
+
- `@commandkit/ratelimit/memory`
|
|
799
|
+
- `@commandkit/ratelimit/fallback`
|
|
800
|
+
|
|
801
|
+
|