ratefall 0.1.0 → 0.2.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/README.md +75 -2
- package/dist/index.cjs +149 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +116 -2
- package/dist/index.d.ts +116 -2
- package/dist/index.js +147 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ Every app needs rate limiting, and every app ends up with the same boilerplate:
|
|
|
28
28
|
- **Edge and serverless ready.** The built-in Upstash store uses `fetch`, so it runs on Node 18+, Vercel Edge, and Cloudflare Workers.
|
|
29
29
|
- **Bring your own store.** Pass an `@upstash/redis` or `ioredis` client, or a fully custom store.
|
|
30
30
|
- **Two algorithms.** Fixed window (cheap) and sliding window (smooth).
|
|
31
|
+
- **One round trip.** Every Redis check is a single atomic Lua call, not a read plus a write.
|
|
31
32
|
- **Typed end to end.** Written in TypeScript, ships ESM and CJS.
|
|
32
33
|
|
|
33
34
|
## Install
|
|
@@ -80,6 +81,66 @@ const limiter = rateLimit({
|
|
|
80
81
|
})
|
|
81
82
|
```
|
|
82
83
|
|
|
84
|
+
## One atomic round trip
|
|
85
|
+
|
|
86
|
+
A rate limiter written the obvious way costs two commands on the first request
|
|
87
|
+
of every window: `INCR`, then `PEXPIRE` if the counter came back as 1.
|
|
88
|
+
|
|
89
|
+
That second command is not just overhead, it is a correctness problem. If the
|
|
90
|
+
process dies between the two, the key has no expiry and stays in Redis forever.
|
|
91
|
+
You can watch it happen:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
redis> INCR mykey
|
|
95
|
+
(integer) 1
|
|
96
|
+
redis> TTL mykey
|
|
97
|
+
(integer) -1 # no expiry, this key is now immortal
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
One orphan per identifier per window, and nothing ever collects them.
|
|
101
|
+
|
|
102
|
+
`ratefall` runs each check as a single `EVAL`. One round trip, and a TTL that
|
|
103
|
+
cannot be orphaned:
|
|
104
|
+
|
|
105
|
+
```lua
|
|
106
|
+
local count = redis.call('INCR', KEYS[1])
|
|
107
|
+
if count == 1 then
|
|
108
|
+
redis.call('PEXPIRE', KEYS[1], ARGV[1])
|
|
109
|
+
end
|
|
110
|
+
return count
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This is automatic. `ratefall` detects whether your client wants `ioredis` style
|
|
114
|
+
arguments (`eval(script, numKeys, ...keys, ...args)`) or `@upstash/redis` style
|
|
115
|
+
(`eval(script, keys[], args[])`) and calls it correctly. Override the detection
|
|
116
|
+
with `evalStyle` if you need to, including `'none'` to stay on the portable
|
|
117
|
+
path:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
rateLimit({ limit: 100, window: '1m', redis: client, evalStyle: 'none' })
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
If the server rejects scripting (a managed Redis with `EVAL` disabled, a proxy
|
|
124
|
+
that does not implement it), the store notices the first failure and falls back
|
|
125
|
+
to `INCR` + `PEXPIRE` permanently. Nothing throws, and it does not retry the
|
|
126
|
+
script on every subsequent request.
|
|
127
|
+
|
|
128
|
+
### What the Lua path does not change
|
|
129
|
+
|
|
130
|
+
It does not make the limiter more accurate under concurrency. It is tempting to
|
|
131
|
+
assume the two-command sliding window races, but it does not overshoot: the
|
|
132
|
+
previous window's counter is a completed window that nothing increments any
|
|
133
|
+
more, so concurrent callers all read the same stable value, and `INCR` on the
|
|
134
|
+
current counter is atomic, so each caller gets a distinct count. Measured over
|
|
135
|
+
50 concurrent callers against a real Redis, both paths allow exactly the same
|
|
136
|
+
number through. The wins here are the orphaned TTL and the round trip, not
|
|
137
|
+
accuracy.
|
|
138
|
+
|
|
139
|
+
Custom stores are unaffected: implement the three required methods and you get
|
|
140
|
+
the portable path, or add the optional `fixedWindowStep` / `slidingWindowStep`
|
|
141
|
+
and the algorithms will use them. See
|
|
142
|
+
[`examples/custom-store.ts`](./examples/custom-store.ts).
|
|
143
|
+
|
|
83
144
|
## Algorithms
|
|
84
145
|
|
|
85
146
|
```ts
|
|
@@ -116,6 +177,16 @@ export async function POST(req: Request) {
|
|
|
116
177
|
}
|
|
117
178
|
```
|
|
118
179
|
|
|
180
|
+
## Examples
|
|
181
|
+
|
|
182
|
+
Runnable snippets live in [`examples/`](./examples): a
|
|
183
|
+
[Next.js route handler](./examples/next-route-handler.ts),
|
|
184
|
+
[Express middleware](./examples/express-middleware.ts),
|
|
185
|
+
[Hono on the edge](./examples/hono-edge.ts),
|
|
186
|
+
[your own ioredis client](./examples/ioredis-client.ts),
|
|
187
|
+
[a custom store](./examples/custom-store.ts), and
|
|
188
|
+
[tiered limits sharing one store](./examples/tiered-limits.ts).
|
|
189
|
+
|
|
119
190
|
## API
|
|
120
191
|
|
|
121
192
|
### `rateLimit(options)`
|
|
@@ -128,6 +199,7 @@ export async function POST(req: Request) {
|
|
|
128
199
|
| `prefix` | `string` | `'ratefall'` | Key namespace, so multiple limiters can share one store. |
|
|
129
200
|
| `store` | `Store` | auto | An explicit store. Overrides everything else. |
|
|
130
201
|
| `redis` | `RedisLike` | auto | A Redis client. Overrides env auto-detection. |
|
|
202
|
+
| `evalStyle` | `'auto' \| 'ioredis' \| 'upstash' \| 'none'` | `'auto'` | How to call the Redis client's `eval`. `'none'` disables the Lua path. |
|
|
131
203
|
| `silent` | `boolean` | `false` | Suppress the production in-memory warning. |
|
|
132
204
|
|
|
133
205
|
Returns a `RateLimiter` with one method:
|
|
@@ -146,12 +218,13 @@ Returns a `RateLimiter` with one method:
|
|
|
146
218
|
|
|
147
219
|
## How it compares
|
|
148
220
|
|
|
149
|
-
If you are all-in on Upstash and want every algorithm, analytics, and multi-region support, use [`@upstash/ratelimit`](https://github.com/upstash/ratelimit). `ratefall` is the smaller, zero-dependency option focused on one thing: the same limiter working from local dev to production without you wiring the store. It falls back to memory with no Redis at all, which `@upstash/ratelimit` does not.
|
|
221
|
+
If you are all-in on Upstash and want every algorithm, analytics, and multi-region support, use [`@upstash/ratelimit`](https://github.com/upstash/ratelimit). `ratefall` is the smaller, zero-dependency option focused on one thing: the same limiter working from local dev to production without you wiring the store. It falls back to memory with no Redis at all, which `@upstash/ratelimit` does not, and it works with `ioredis` and any duck-typed client rather than only Upstash.
|
|
150
222
|
|
|
151
223
|
## Notes
|
|
152
224
|
|
|
153
225
|
- The in-memory store is per-process. It is meant for local development and single-instance deploys, not shared production traffic.
|
|
154
|
-
- Redis operations
|
|
226
|
+
- Redis operations run as a single atomic `EVAL`. The `INCR` + `PEXPIRE` path is kept as an automatic fallback for servers without scripting.
|
|
227
|
+
- The Lua scripts are exercised against a real Redis in CI, not just against fakes. To run those locally: `docker run -p 6379:6379 redis:7-alpine` then `REDIS_URL=redis://localhost:6379 npm test`.
|
|
155
228
|
|
|
156
229
|
## License
|
|
157
230
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
// src/algorithms/fixed-window.ts
|
|
4
|
+
async function step(store, key, ttlMs) {
|
|
5
|
+
if (store.fixedWindowStep) return store.fixedWindowStep(key, ttlMs);
|
|
6
|
+
const count = await store.incr(key);
|
|
7
|
+
if (count === 1) await store.pexpire(key, ttlMs);
|
|
8
|
+
return count;
|
|
9
|
+
}
|
|
4
10
|
var fixedWindow = async (store, ctx, identifier, now) => {
|
|
5
11
|
const { limit, windowMs, prefix } = ctx;
|
|
6
12
|
const windowStart = Math.floor(now / windowMs) * windowMs;
|
|
7
13
|
const key = `${prefix}:${identifier}:${windowStart}`;
|
|
8
|
-
const count = await store
|
|
9
|
-
if (count === 1) await store.pexpire(key, windowMs);
|
|
14
|
+
const count = await step(store, key, windowMs);
|
|
10
15
|
const reset = windowStart + windowMs;
|
|
11
16
|
const success = count <= limit;
|
|
12
17
|
return {
|
|
@@ -19,6 +24,13 @@ var fixedWindow = async (store, ctx, identifier, now) => {
|
|
|
19
24
|
};
|
|
20
25
|
|
|
21
26
|
// src/algorithms/sliding-window.ts
|
|
27
|
+
async function step2(store, currentKey, previousKey, ttlMs) {
|
|
28
|
+
if (store.slidingWindowStep) return store.slidingWindowStep(currentKey, previousKey, ttlMs);
|
|
29
|
+
const previous = await store.get(previousKey);
|
|
30
|
+
const current = await store.incr(currentKey);
|
|
31
|
+
if (current === 1) await store.pexpire(currentKey, ttlMs);
|
|
32
|
+
return [previous, current];
|
|
33
|
+
}
|
|
22
34
|
var slidingWindow = async (store, ctx, identifier, now) => {
|
|
23
35
|
const { limit, windowMs, prefix } = ctx;
|
|
24
36
|
const currentStart = Math.floor(now / windowMs) * windowMs;
|
|
@@ -26,9 +38,7 @@ var slidingWindow = async (store, ctx, identifier, now) => {
|
|
|
26
38
|
const fraction = (now - currentStart) / windowMs;
|
|
27
39
|
const currentKey = `${prefix}:${identifier}:${currentStart}`;
|
|
28
40
|
const previousKey = `${prefix}:${identifier}:${previousStart}`;
|
|
29
|
-
const previousCount = await store
|
|
30
|
-
const currentCount = await store.incr(currentKey);
|
|
31
|
-
if (currentCount === 1) await store.pexpire(currentKey, windowMs * 2);
|
|
41
|
+
const [previousCount, currentCount] = await step2(store, currentKey, previousKey, windowMs * 2);
|
|
32
42
|
const weighted = previousCount * (1 - fraction) + currentCount;
|
|
33
43
|
const reset = currentStart + windowMs;
|
|
34
44
|
const success = weighted <= limit;
|
|
@@ -77,6 +87,25 @@ var MemoryStore = class {
|
|
|
77
87
|
async get(key) {
|
|
78
88
|
return this.live(key)?.count ?? 0;
|
|
79
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Atomic fixed-window step. JavaScript is single threaded and none of this
|
|
92
|
+
* awaits, so the increment and the TTL land together with no interleaving.
|
|
93
|
+
*/
|
|
94
|
+
async fixedWindowStep(key, ttlMs) {
|
|
95
|
+
const entry = this.live(key);
|
|
96
|
+
if (!entry) {
|
|
97
|
+
this.map.set(key, { count: 1, expiresAt: Date.now() + ttlMs });
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
entry.count += 1;
|
|
101
|
+
return entry.count;
|
|
102
|
+
}
|
|
103
|
+
/** Atomic sliding-window step. Same reasoning as {@link fixedWindowStep}. */
|
|
104
|
+
async slidingWindowStep(currentKey, previousKey, ttlMs) {
|
|
105
|
+
const previous = this.live(previousKey)?.count ?? 0;
|
|
106
|
+
const current = await this.fixedWindowStep(currentKey, ttlMs);
|
|
107
|
+
return [previous, current];
|
|
108
|
+
}
|
|
80
109
|
sweep() {
|
|
81
110
|
const now = Date.now();
|
|
82
111
|
for (const [key, entry] of this.map) {
|
|
@@ -89,12 +118,75 @@ var MemoryStore = class {
|
|
|
89
118
|
}
|
|
90
119
|
};
|
|
91
120
|
|
|
121
|
+
// src/lua.ts
|
|
122
|
+
var FIXED_WINDOW_SCRIPT = `
|
|
123
|
+
local count = redis.call('INCR', KEYS[1])
|
|
124
|
+
if count == 1 then
|
|
125
|
+
redis.call('PEXPIRE', KEYS[1], ARGV[1])
|
|
126
|
+
end
|
|
127
|
+
return count
|
|
128
|
+
`.trim();
|
|
129
|
+
var SLIDING_WINDOW_SCRIPT = `
|
|
130
|
+
local previous = redis.call('GET', KEYS[2])
|
|
131
|
+
local previousCount = 0
|
|
132
|
+
if previous then
|
|
133
|
+
previousCount = math.floor(tonumber(previous) or 0)
|
|
134
|
+
end
|
|
135
|
+
local current = redis.call('INCR', KEYS[1])
|
|
136
|
+
if current == 1 then
|
|
137
|
+
redis.call('PEXPIRE', KEYS[1], ARGV[1])
|
|
138
|
+
end
|
|
139
|
+
return { previousCount, current }
|
|
140
|
+
`.trim();
|
|
141
|
+
|
|
92
142
|
// src/stores/redis.ts
|
|
143
|
+
function toCount(value) {
|
|
144
|
+
if (value == null || value === false) return 0;
|
|
145
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
146
|
+
return Number.isFinite(n) ? n : 0;
|
|
147
|
+
}
|
|
148
|
+
function detectEvalStyle(client) {
|
|
149
|
+
if (typeof client.eval !== "function") return "none";
|
|
150
|
+
if (typeof client.defineCommand === "function") return "ioredis";
|
|
151
|
+
return "upstash";
|
|
152
|
+
}
|
|
93
153
|
var RedisStore = class {
|
|
94
|
-
|
|
154
|
+
client;
|
|
155
|
+
style;
|
|
156
|
+
constructor(client, options = {}) {
|
|
95
157
|
this.client = client;
|
|
158
|
+
const requested = options.evalStyle ?? "auto";
|
|
159
|
+
this.style = requested === "auto" ? detectEvalStyle(client) : requested;
|
|
160
|
+
if (this.style !== "none" && typeof client.eval !== "function") {
|
|
161
|
+
this.style = "none";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** True when this store will take the atomic Lua path. */
|
|
165
|
+
get atomic() {
|
|
166
|
+
return this.style !== "none";
|
|
167
|
+
}
|
|
168
|
+
async runScript(script, keys, args) {
|
|
169
|
+
const evalFn = this.client.eval;
|
|
170
|
+
if (this.style === "ioredis") {
|
|
171
|
+
return evalFn.call(this.client, script, keys.length, ...keys, ...args);
|
|
172
|
+
}
|
|
173
|
+
return evalFn.call(this.client, script, keys, args);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Run `script`, falling back to the portable path forever if the server or
|
|
177
|
+
* client cannot handle scripting. A single failure is enough to decide: a
|
|
178
|
+
* server either supports EVAL or it does not, so retrying every request would
|
|
179
|
+
* just double the cost of every check on that deployment.
|
|
180
|
+
*/
|
|
181
|
+
async tryScript(script, keys, args, parse) {
|
|
182
|
+
if (this.style === "none") return void 0;
|
|
183
|
+
try {
|
|
184
|
+
return parse(await this.runScript(script, keys, args));
|
|
185
|
+
} catch {
|
|
186
|
+
this.style = "none";
|
|
187
|
+
return void 0;
|
|
188
|
+
}
|
|
96
189
|
}
|
|
97
|
-
client;
|
|
98
190
|
async incr(key) {
|
|
99
191
|
return this.client.incr(key);
|
|
100
192
|
}
|
|
@@ -102,14 +194,39 @@ var RedisStore = class {
|
|
|
102
194
|
await this.client.pexpire(key, ms);
|
|
103
195
|
}
|
|
104
196
|
async get(key) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
197
|
+
return toCount(await this.client.get(key));
|
|
198
|
+
}
|
|
199
|
+
async fixedWindowStep(key, ttlMs) {
|
|
200
|
+
const viaScript = await this.tryScript(FIXED_WINDOW_SCRIPT, [key], [ttlMs], toCount);
|
|
201
|
+
if (viaScript !== void 0) return viaScript;
|
|
202
|
+
const count = await this.incr(key);
|
|
203
|
+
if (count === 1) await this.pexpire(key, ttlMs);
|
|
204
|
+
return count;
|
|
205
|
+
}
|
|
206
|
+
async slidingWindowStep(currentKey, previousKey, ttlMs) {
|
|
207
|
+
const viaScript = await this.tryScript(
|
|
208
|
+
SLIDING_WINDOW_SCRIPT,
|
|
209
|
+
[currentKey, previousKey],
|
|
210
|
+
[ttlMs],
|
|
211
|
+
(raw) => {
|
|
212
|
+
const pair = raw;
|
|
213
|
+
return [toCount(pair?.[0]), toCount(pair?.[1])];
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
if (viaScript !== void 0) return viaScript;
|
|
217
|
+
const previous = await this.get(previousKey);
|
|
218
|
+
const current = await this.incr(currentKey);
|
|
219
|
+
if (current === 1) await this.pexpire(currentKey, ttlMs);
|
|
220
|
+
return [previous, current];
|
|
109
221
|
}
|
|
110
222
|
};
|
|
111
223
|
|
|
112
224
|
// src/stores/upstash-rest.ts
|
|
225
|
+
function toCount2(value) {
|
|
226
|
+
if (value == null || value === false) return 0;
|
|
227
|
+
const n = Number(value);
|
|
228
|
+
return Number.isFinite(n) ? n : 0;
|
|
229
|
+
}
|
|
113
230
|
var UpstashRestStore = class {
|
|
114
231
|
url;
|
|
115
232
|
token;
|
|
@@ -149,10 +266,18 @@ var UpstashRestStore = class {
|
|
|
149
266
|
await this.command(["PEXPIRE", key, ms]);
|
|
150
267
|
}
|
|
151
268
|
async get(key) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
269
|
+
return toCount2(await this.command(["GET", key]));
|
|
270
|
+
}
|
|
271
|
+
/** Run a Lua script through the REST API. Keys come first, then args. */
|
|
272
|
+
async evalScript(script, keys, args) {
|
|
273
|
+
return this.command(["EVAL", script, keys.length, ...keys, ...args]);
|
|
274
|
+
}
|
|
275
|
+
async fixedWindowStep(key, ttlMs) {
|
|
276
|
+
return toCount2(await this.evalScript(FIXED_WINDOW_SCRIPT, [key], [ttlMs]));
|
|
277
|
+
}
|
|
278
|
+
async slidingWindowStep(currentKey, previousKey, ttlMs) {
|
|
279
|
+
const raw = await this.evalScript(SLIDING_WINDOW_SCRIPT, [currentKey, previousKey], [ttlMs]);
|
|
280
|
+
return [toCount2(raw?.[0]), toCount2(raw?.[1])];
|
|
156
281
|
}
|
|
157
282
|
};
|
|
158
283
|
|
|
@@ -166,7 +291,12 @@ function resolveStore(options) {
|
|
|
166
291
|
return { store: options.store, distributed: true, source: "custom store" };
|
|
167
292
|
}
|
|
168
293
|
if (options.redis) {
|
|
169
|
-
|
|
294
|
+
const store = new RedisStore(options.redis, { evalStyle: options.evalStyle });
|
|
295
|
+
return {
|
|
296
|
+
store,
|
|
297
|
+
distributed: true,
|
|
298
|
+
source: store.atomic ? "redis client (atomic)" : "redis client"
|
|
299
|
+
};
|
|
170
300
|
}
|
|
171
301
|
const url = readEnv("UPSTASH_REDIS_REST_URL");
|
|
172
302
|
const token = readEnv("UPSTASH_REDIS_REST_TOKEN");
|
|
@@ -240,9 +370,12 @@ function rateLimit(options) {
|
|
|
240
370
|
};
|
|
241
371
|
}
|
|
242
372
|
|
|
373
|
+
exports.FIXED_WINDOW_SCRIPT = FIXED_WINDOW_SCRIPT;
|
|
243
374
|
exports.MemoryStore = MemoryStore;
|
|
244
375
|
exports.RedisStore = RedisStore;
|
|
376
|
+
exports.SLIDING_WINDOW_SCRIPT = SLIDING_WINDOW_SCRIPT;
|
|
245
377
|
exports.UpstashRestStore = UpstashRestStore;
|
|
378
|
+
exports.detectEvalStyle = detectEvalStyle;
|
|
246
379
|
exports.parseWindow = parseWindow;
|
|
247
380
|
exports.rateLimit = rateLimit;
|
|
248
381
|
exports.resolveStore = resolveStore;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/algorithms/fixed-window.ts","../src/algorithms/sliding-window.ts","../src/stores/memory.ts","../src/stores/redis.ts","../src/stores/upstash-rest.ts","../src/resolve-store.ts","../src/window.ts","../src/rate-limit.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { AlgorithmFn } from './index.js'\n\n/**\n * Fixed-window counter. Divides time into contiguous windows of `windowMs` and\n * counts requests per window. Simple and cheap (one counter per window), at the\n * cost of allowing up to 2x `limit` across a window boundary in the worst case.\n */\nexport const fixedWindow: AlgorithmFn = async (store, ctx, identifier, now) => {\n const { limit, windowMs, prefix } = ctx\n const windowStart = Math.floor(now / windowMs) * windowMs\n const key = `${prefix}:${identifier}:${windowStart}`\n\n const count = await store.incr(key)\n if (count === 1) await store.pexpire(key, windowMs)\n\n const reset = windowStart + windowMs\n const success = count <= limit\n return {\n success,\n limit,\n remaining: Math.max(0, limit - count),\n reset,\n retryAfter: success ? 0 : reset - now,\n }\n}\n","import type { AlgorithmFn } from './index.js'\n\n/**\n * Sliding-window counter. Weights the previous window's count by how far the\n * current window has elapsed and adds the current count, smoothing out the\n * boundary bursts that fixed-window allows. Uses two counters per identifier.\n */\nexport const slidingWindow: AlgorithmFn = async (store, ctx, identifier, now) => {\n const { limit, windowMs, prefix } = ctx\n const currentStart = Math.floor(now / windowMs) * windowMs\n const previousStart = currentStart - windowMs\n const fraction = (now - currentStart) / windowMs // 0..1 through the current window\n\n const currentKey = `${prefix}:${identifier}:${currentStart}`\n const previousKey = `${prefix}:${identifier}:${previousStart}`\n\n const previousCount = await store.get(previousKey)\n const currentCount = await store.incr(currentKey)\n // Keep the counter alive long enough to still be read as \"previous\" next window.\n if (currentCount === 1) await store.pexpire(currentKey, windowMs * 2)\n\n const weighted = previousCount * (1 - fraction) + currentCount\n const reset = currentStart + windowMs\n const success = weighted <= limit\n return {\n success,\n limit,\n remaining: Math.max(0, Math.floor(limit - weighted)),\n reset,\n retryAfter: success ? 0 : reset - now,\n }\n}\n","import type { Store } from '../types.js'\n\ninterface Entry {\n count: number\n expiresAt: number\n}\n\n/**\n * In-process counter store. Used as the automatic fallback when no Redis is\n * configured. State is per-instance and not shared across processes, so it is\n * meant for local development and single-instance deployments, not distributed\n * production traffic.\n */\nexport class MemoryStore implements Store {\n private readonly map = new Map<string, Entry>()\n private readonly sweeper: ReturnType<typeof setInterval> | undefined\n\n /** @param sweepMs How often to purge expired keys. Set to 0 to disable the timer (lazy expiry still applies). */\n constructor(sweepMs = 30_000) {\n if (sweepMs > 0 && typeof setInterval === 'function') {\n this.sweeper = setInterval(() => this.sweep(), sweepMs)\n // Never keep the event loop alive for cleanup alone.\n this.sweeper.unref?.()\n }\n }\n\n private live(key: string): Entry | undefined {\n const entry = this.map.get(key)\n if (!entry) return undefined\n if (entry.expiresAt <= Date.now()) {\n this.map.delete(key)\n return undefined\n }\n return entry\n }\n\n async incr(key: string): Promise<number> {\n const entry = this.live(key)\n if (!entry) {\n // Default to no expiry; pexpire is expected to follow on the first hit.\n this.map.set(key, { count: 1, expiresAt: Number.POSITIVE_INFINITY })\n return 1\n }\n entry.count += 1\n return entry.count\n }\n\n async pexpire(key: string, ms: number): Promise<void> {\n const entry = this.live(key)\n if (entry) entry.expiresAt = Date.now() + ms\n }\n\n async get(key: string): Promise<number> {\n return this.live(key)?.count ?? 0\n }\n\n private sweep(): void {\n const now = Date.now()\n for (const [key, entry] of this.map) {\n if (entry.expiresAt <= now) this.map.delete(key)\n }\n }\n\n /** Stop the background sweeper. Optional; the timer is unref'd. */\n dispose(): void {\n if (this.sweeper) clearInterval(this.sweeper)\n }\n}\n","import type { RedisLike, Store } from '../types.js'\n\n/**\n * Adapts any Redis client with `incr` / `pexpire` / `get` (both `@upstash/redis`\n * and `ioredis` qualify) to the {@link Store} interface.\n */\nexport class RedisStore implements Store {\n constructor(private readonly client: RedisLike) {}\n\n async incr(key: string): Promise<number> {\n return this.client.incr(key)\n }\n\n async pexpire(key: string, ms: number): Promise<void> {\n await this.client.pexpire(key, ms)\n }\n\n async get(key: string): Promise<number> {\n const value = await this.client.get(key)\n if (value == null) return 0\n const n = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(n) ? n : 0\n }\n}\n","import type { Store } from '../types.js'\n\nexport interface UpstashRestOptions {\n url: string\n token: string\n /** Custom fetch implementation. Defaults to the global `fetch`. */\n fetch?: typeof fetch\n}\n\n/**\n * Zero-dependency store backed by the Upstash Redis REST API. Uses `fetch`, so\n * it runs anywhere the runtime provides one (Node 18+, edge, workers) without\n * pulling in a Redis client. This is what env auto-detection constructs.\n */\nexport class UpstashRestStore implements Store {\n private readonly url: string\n private readonly token: string\n private readonly fetchImpl: typeof fetch\n\n constructor(options: UpstashRestOptions) {\n this.url = options.url.replace(/\\/$/, '')\n this.token = options.token\n const f = options.fetch ?? globalThis.fetch\n if (typeof f !== 'function') {\n throw new Error('ratefall: no global fetch available; pass a `fetch` implementation to UpstashRestStore.')\n }\n this.fetchImpl = f\n }\n\n private async command(args: (string | number)[]): Promise<unknown> {\n const res = await this.fetchImpl(this.url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(args.map(String)),\n })\n if (!res.ok) {\n const detail = await res.text().catch(() => '')\n throw new Error(`ratefall: Upstash REST error ${res.status} ${res.statusText}${detail ? ` - ${detail}` : ''}`)\n }\n const body = (await res.json()) as { result?: unknown; error?: string }\n if (body.error) {\n throw new Error(`ratefall: Upstash command failed - ${body.error}`)\n }\n return body.result\n }\n\n async incr(key: string): Promise<number> {\n return Number(await this.command(['INCR', key]))\n }\n\n async pexpire(key: string, ms: number): Promise<void> {\n await this.command(['PEXPIRE', key, ms])\n }\n\n async get(key: string): Promise<number> {\n const result = await this.command(['GET', key])\n if (result == null) return 0\n const n = Number(result)\n return Number.isFinite(n) ? n : 0\n }\n}\n","import type { RateLimitOptions, Store } from './types.js'\nimport { MemoryStore } from './stores/memory.js'\nimport { RedisStore } from './stores/redis.js'\nimport { UpstashRestStore } from './stores/upstash-rest.js'\n\nexport interface ResolvedStore {\n store: Store\n /** Whether the store is shared across processes (Redis) vs per-instance (memory). */\n distributed: boolean\n /** Human-readable source, for the dev warning. */\n source: string\n}\n\nfunction readEnv(key: string): string | undefined {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env\n return env?.[key]\n}\n\n/**\n * Decide which store to use, in priority order:\n * 1. an explicit `store`\n * 2. an explicit `redis` client\n * 3. `UPSTASH_REDIS_REST_URL` + `UPSTASH_REDIS_REST_TOKEN` in the environment\n * 4. in-memory fallback\n */\nexport function resolveStore(options: RateLimitOptions): ResolvedStore {\n if (options.store) {\n return { store: options.store, distributed: true, source: 'custom store' }\n }\n\n if (options.redis) {\n return { store: new RedisStore(options.redis), distributed: true, source: 'redis client' }\n }\n\n const url = readEnv('UPSTASH_REDIS_REST_URL')\n const token = readEnv('UPSTASH_REDIS_REST_TOKEN')\n if (url && token) {\n return { store: new UpstashRestStore({ url, token }), distributed: true, source: 'Upstash REST (env)' }\n }\n\n return { store: new MemoryStore(), distributed: false, source: 'in-memory' }\n}\n\n/** True when the runtime looks like a production deployment. */\nexport function isProduction(): boolean {\n return readEnv('NODE_ENV') === 'production'\n}\n","const UNITS: Record<string, number> = {\n ms: 1,\n s: 1000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n}\n\nconst PATTERN = /^(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)$/\n\n/**\n * Normalize a window into milliseconds. Accepts a raw millisecond number or a\n * duration string like `'500ms'`, `'10s'`, `'1m'`, `'2h'`, `'1d'`.\n */\nexport function parseWindow(window: string | number): number {\n if (typeof window === 'number') {\n if (!Number.isFinite(window) || window <= 0) {\n throw new RangeError(`ratefall: window must be a positive number of milliseconds, got ${window}`)\n }\n return Math.floor(window)\n }\n\n const match = PATTERN.exec(window.trim())\n if (!match) {\n throw new TypeError(`ratefall: invalid window \"${window}\". Use a number of ms or a string like \"10s\", \"1m\", \"2h\".`)\n }\n\n const value = Number(match[1])\n const unit = UNITS[match[2] as string] as number\n const ms = Math.floor(value * unit)\n if (ms <= 0) {\n throw new RangeError(`ratefall: window must be greater than 0, got \"${window}\".`)\n }\n return ms\n}\n","import type { AlgorithmContext, AlgorithmFn } from './algorithms/index.js'\nimport { fixedWindow } from './algorithms/fixed-window.js'\nimport { slidingWindow } from './algorithms/sliding-window.js'\nimport { isProduction, resolveStore } from './resolve-store.js'\nimport type { RateLimiter, RateLimitOptions } from './types.js'\nimport { parseWindow } from './window.js'\n\nconst ALGORITHMS: Record<string, AlgorithmFn> = {\n 'fixed-window': fixedWindow,\n 'sliding-window': slidingWindow,\n}\n\n/**\n * Create a rate limiter. With no store configured it uses an in-memory counter\n * (great for local dev); set `UPSTASH_REDIS_REST_URL` + `UPSTASH_REDIS_REST_TOKEN`,\n * or pass a `redis` client, and the exact same code becomes distributed.\n *\n * @example\n * const limiter = rateLimit({ limit: 10, window: '10s' })\n * const { success } = await limiter.limit(userId)\n * if (!success) return new Response('slow down', { status: 429 })\n */\nexport function rateLimit(options: RateLimitOptions): RateLimiter {\n const { limit, algorithm = 'fixed-window', prefix = 'ratefall', silent = false } = options\n\n if (!Number.isInteger(limit) || limit <= 0) {\n throw new RangeError(`ratefall: limit must be a positive integer, got ${limit}`)\n }\n\n const run = ALGORITHMS[algorithm]\n if (!run) {\n throw new TypeError(`ratefall: unknown algorithm \"${algorithm}\". Use \"fixed-window\" or \"sliding-window\".`)\n }\n\n const windowMs = parseWindow(options.window)\n const { store, distributed, source } = resolveStore(options)\n\n if (!distributed && !silent && isProduction()) {\n // Not an error: single-instance prod is legitimate. But un-shared limits in a\n // multi-instance deploy are a footgun, so make it loud exactly once.\n console.warn(\n `ratefall: running with an in-memory store in production. Limits are per-instance and NOT shared. ` +\n `Set UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN or pass a \\`redis\\` client. Pass \\`silent: true\\` to hide this.`,\n )\n }\n\n const ctx: AlgorithmContext = { limit, windowMs, prefix }\n\n return {\n async limit(identifier: string) {\n if (!identifier) {\n throw new TypeError('ratefall: identifier must be a non-empty string.')\n }\n return run(store, ctx, identifier, Date.now())\n },\n }\n}\n\n/** The resolved store source, exposed for debugging/tests. */\nexport { resolveStore } from './resolve-store.js'\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/algorithms/fixed-window.ts","../src/algorithms/sliding-window.ts","../src/stores/memory.ts","../src/lua.ts","../src/stores/redis.ts","../src/stores/upstash-rest.ts","../src/resolve-store.ts","../src/window.ts","../src/rate-limit.ts"],"names":["step","toCount"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { AlgorithmFn } from './index.js'\nimport type { Store } from '../types.js'\n\n/**\n * Increment the window counter, preferring the store's atomic single-call path\n * and falling back to `incr` + a conditional `pexpire` for stores that only\n * implement the three required methods.\n */\nasync function step(store: Store, key: string, ttlMs: number): Promise<number> {\n if (store.fixedWindowStep) return store.fixedWindowStep(key, ttlMs)\n\n const count = await store.incr(key)\n if (count === 1) await store.pexpire(key, ttlMs)\n return count\n}\n\n/**\n * Fixed-window counter. Divides time into contiguous windows of `windowMs` and\n * counts requests per window. Simple and cheap (one counter per window), at the\n * cost of allowing up to 2x `limit` across a window boundary in the worst case.\n */\nexport const fixedWindow: AlgorithmFn = async (store, ctx, identifier, now) => {\n const { limit, windowMs, prefix } = ctx\n const windowStart = Math.floor(now / windowMs) * windowMs\n const key = `${prefix}:${identifier}:${windowStart}`\n\n const count = await step(store, key, windowMs)\n\n const reset = windowStart + windowMs\n const success = count <= limit\n return {\n success,\n limit,\n remaining: Math.max(0, limit - count),\n reset,\n retryAfter: success ? 0 : reset - now,\n }\n}\n","import type { AlgorithmFn } from './index.js'\nimport type { Store } from '../types.js'\n\n/**\n * Read the previous window's counter and increment the current one, preferring\n * the store's atomic single-call path.\n *\n * The fallback runs the read and the increment as two separate commands. That\n * costs an extra round trip, and it means the two observations are taken at\n * slightly different moments, so a request landing exactly on a window boundary\n * can weight a previous count that moved between the `GET` and the `INCR`. It\n * does not overshoot the limit under ordinary concurrency: the previous window\n * is complete and therefore stable, and `INCR` is atomic.\n */\nasync function step(\n store: Store,\n currentKey: string,\n previousKey: string,\n ttlMs: number,\n): Promise<[number, number]> {\n if (store.slidingWindowStep) return store.slidingWindowStep(currentKey, previousKey, ttlMs)\n\n const previous = await store.get(previousKey)\n const current = await store.incr(currentKey)\n if (current === 1) await store.pexpire(currentKey, ttlMs)\n return [previous, current]\n}\n\n/**\n * Sliding-window counter. Weights the previous window's count by how far the\n * current window has elapsed and adds the current count, smoothing out the\n * boundary bursts that fixed-window allows. Uses two counters per identifier.\n */\nexport const slidingWindow: AlgorithmFn = async (store, ctx, identifier, now) => {\n const { limit, windowMs, prefix } = ctx\n const currentStart = Math.floor(now / windowMs) * windowMs\n const previousStart = currentStart - windowMs\n const fraction = (now - currentStart) / windowMs // 0..1 through the current window\n\n const currentKey = `${prefix}:${identifier}:${currentStart}`\n const previousKey = `${prefix}:${identifier}:${previousStart}`\n\n // Keep the current counter alive long enough to still be read as \"previous\"\n // during the next window.\n const [previousCount, currentCount] = await step(store, currentKey, previousKey, windowMs * 2)\n\n const weighted = previousCount * (1 - fraction) + currentCount\n const reset = currentStart + windowMs\n const success = weighted <= limit\n return {\n success,\n limit,\n remaining: Math.max(0, Math.floor(limit - weighted)),\n reset,\n retryAfter: success ? 0 : reset - now,\n }\n}\n","import type { Store } from '../types.js'\n\ninterface Entry {\n count: number\n expiresAt: number\n}\n\n/**\n * In-process counter store. Used as the automatic fallback when no Redis is\n * configured. State is per-instance and not shared across processes, so it is\n * meant for local development and single-instance deployments, not distributed\n * production traffic.\n */\nexport class MemoryStore implements Store {\n private readonly map = new Map<string, Entry>()\n private readonly sweeper: ReturnType<typeof setInterval> | undefined\n\n /** @param sweepMs How often to purge expired keys. Set to 0 to disable the timer (lazy expiry still applies). */\n constructor(sweepMs = 30_000) {\n if (sweepMs > 0 && typeof setInterval === 'function') {\n this.sweeper = setInterval(() => this.sweep(), sweepMs)\n // Never keep the event loop alive for cleanup alone.\n this.sweeper.unref?.()\n }\n }\n\n private live(key: string): Entry | undefined {\n const entry = this.map.get(key)\n if (!entry) return undefined\n if (entry.expiresAt <= Date.now()) {\n this.map.delete(key)\n return undefined\n }\n return entry\n }\n\n async incr(key: string): Promise<number> {\n const entry = this.live(key)\n if (!entry) {\n // Default to no expiry; pexpire is expected to follow on the first hit.\n this.map.set(key, { count: 1, expiresAt: Number.POSITIVE_INFINITY })\n return 1\n }\n entry.count += 1\n return entry.count\n }\n\n async pexpire(key: string, ms: number): Promise<void> {\n const entry = this.live(key)\n if (entry) entry.expiresAt = Date.now() + ms\n }\n\n async get(key: string): Promise<number> {\n return this.live(key)?.count ?? 0\n }\n\n /**\n * Atomic fixed-window step. JavaScript is single threaded and none of this\n * awaits, so the increment and the TTL land together with no interleaving.\n */\n async fixedWindowStep(key: string, ttlMs: number): Promise<number> {\n const entry = this.live(key)\n if (!entry) {\n this.map.set(key, { count: 1, expiresAt: Date.now() + ttlMs })\n return 1\n }\n entry.count += 1\n return entry.count\n }\n\n /** Atomic sliding-window step. Same reasoning as {@link fixedWindowStep}. */\n async slidingWindowStep(\n currentKey: string,\n previousKey: string,\n ttlMs: number,\n ): Promise<[number, number]> {\n const previous = this.live(previousKey)?.count ?? 0\n const current = await this.fixedWindowStep(currentKey, ttlMs)\n return [previous, current]\n }\n\n private sweep(): void {\n const now = Date.now()\n for (const [key, entry] of this.map) {\n if (entry.expiresAt <= now) this.map.delete(key)\n }\n }\n\n /** Stop the background sweeper. Optional; the timer is unref'd. */\n dispose(): void {\n if (this.sweeper) clearInterval(this.sweeper)\n }\n}\n","/**\n * Lua scripts for the single-round-trip Redis path.\n *\n * The fallback path (`INCR` then a conditional `PEXPIRE`) costs two round trips\n * on the first hit of every window, and is not atomic: a process that dies\n * between the two commands leaves a key with no TTL, which then leaks in Redis\n * forever. Verified against Redis 7.4: `INCR` on a fresh key reports `TTL -1`\n * until the `PEXPIRE` lands.\n *\n * Running the same work inside `EVAL` fixes both: one round trip, and a TTL\n * that cannot be orphaned.\n *\n * Note on what this does NOT fix. It is tempting to claim the fallback races\n * under concurrency. It does not, at least not in a way that overshoots the\n * limit: the previous window's counter is a completed window that nothing\n * increments any more, so every concurrent caller reads the same stable value,\n * and `INCR` on the current key is atomic, so each caller gets a distinct\n * count. Measured over 50 concurrent callers against a real Redis, both paths\n * allow exactly the same number through.\n */\n\n/**\n * Fixed window. Increments the counter and sets the TTL on first touch.\n *\n * KEYS[1] window counter\n * ARGV[1] window length in milliseconds\n * returns the new count\n */\nexport const FIXED_WINDOW_SCRIPT = `\nlocal count = redis.call('INCR', KEYS[1])\nif count == 1 then\n redis.call('PEXPIRE', KEYS[1], ARGV[1])\nend\nreturn count\n`.trim()\n\n/**\n * Sliding window. Reads the previous window's counter and increments the\n * current one as a single atomic unit.\n *\n * KEYS[1] current window counter\n * KEYS[2] previous window counter\n * ARGV[1] retention in milliseconds (two windows, so the current counter is\n * still readable as \"previous\" during the next window)\n * returns { previousCount, currentCount }\n */\nexport const SLIDING_WINDOW_SCRIPT = `\nlocal previous = redis.call('GET', KEYS[2])\nlocal previousCount = 0\nif previous then\n previousCount = math.floor(tonumber(previous) or 0)\nend\nlocal current = redis.call('INCR', KEYS[1])\nif current == 1 then\n redis.call('PEXPIRE', KEYS[1], ARGV[1])\nend\nreturn { previousCount, current }\n`.trim()\n","import { FIXED_WINDOW_SCRIPT, SLIDING_WINDOW_SCRIPT } from '../lua.js'\nimport type { EvalStyle, RedisLike, Store } from '../types.js'\n\nexport interface RedisStoreOptions {\n /** How to call the client's `eval`. Defaults to `'auto'`. */\n evalStyle?: EvalStyle\n}\n\n/** Coerce whatever a client returns for a counter into a non-negative number. */\nfunction toCount(value: unknown): number {\n if (value == null || value === false) return 0\n const n = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(n) ? n : 0\n}\n\n/**\n * Decide which `eval` calling convention a client uses.\n *\n * `ioredis` is the odd one out: it takes a key count followed by flat keys and\n * args, and it is the only one of the two that exposes `defineCommand`. Every\n * other client we support (`@upstash/redis`) takes two arrays.\n */\nexport function detectEvalStyle(client: RedisLike): Exclude<EvalStyle, 'auto'> {\n if (typeof client.eval !== 'function') return 'none'\n if (typeof (client as { defineCommand?: unknown }).defineCommand === 'function') return 'ioredis'\n return 'upstash'\n}\n\n/**\n * Adapts any Redis client with `incr` / `pexpire` / `get` (both `@upstash/redis`\n * and `ioredis` qualify) to the {@link Store} interface.\n *\n * When the client also exposes `eval`, checks run as a single atomic Lua call\n * instead of two or three separate commands. If a server rejects scripting the\n * store degrades to the portable path once and stays there.\n */\nexport class RedisStore implements Store {\n private readonly client: RedisLike\n private style: Exclude<EvalStyle, 'auto'>\n\n constructor(client: RedisLike, options: RedisStoreOptions = {}) {\n this.client = client\n const requested = options.evalStyle ?? 'auto'\n this.style = requested === 'auto' ? detectEvalStyle(client) : requested\n if (this.style !== 'none' && typeof client.eval !== 'function') {\n // An explicit style cannot conjure a method that is not there.\n this.style = 'none'\n }\n }\n\n /** True when this store will take the atomic Lua path. */\n get atomic(): boolean {\n return this.style !== 'none'\n }\n\n private async runScript(script: string, keys: string[], args: (string | number)[]): Promise<unknown> {\n const evalFn = this.client.eval as (...a: unknown[]) => Promise<unknown>\n if (this.style === 'ioredis') {\n return evalFn.call(this.client, script, keys.length, ...keys, ...args)\n }\n return evalFn.call(this.client, script, keys, args)\n }\n\n /**\n * Run `script`, falling back to the portable path forever if the server or\n * client cannot handle scripting. A single failure is enough to decide: a\n * server either supports EVAL or it does not, so retrying every request would\n * just double the cost of every check on that deployment.\n */\n private async tryScript<T>(\n script: string,\n keys: string[],\n args: (string | number)[],\n parse: (raw: unknown) => T,\n ): Promise<T | undefined> {\n if (this.style === 'none') return undefined\n try {\n return parse(await this.runScript(script, keys, args))\n } catch {\n this.style = 'none'\n return undefined\n }\n }\n\n async incr(key: string): Promise<number> {\n return this.client.incr(key)\n }\n\n async pexpire(key: string, ms: number): Promise<void> {\n await this.client.pexpire(key, ms)\n }\n\n async get(key: string): Promise<number> {\n return toCount(await this.client.get(key))\n }\n\n async fixedWindowStep(key: string, ttlMs: number): Promise<number> {\n const viaScript = await this.tryScript(FIXED_WINDOW_SCRIPT, [key], [ttlMs], toCount)\n if (viaScript !== undefined) return viaScript\n\n const count = await this.incr(key)\n if (count === 1) await this.pexpire(key, ttlMs)\n return count\n }\n\n async slidingWindowStep(\n currentKey: string,\n previousKey: string,\n ttlMs: number,\n ): Promise<[number, number]> {\n const viaScript = await this.tryScript(\n SLIDING_WINDOW_SCRIPT,\n [currentKey, previousKey],\n [ttlMs],\n (raw): [number, number] => {\n const pair = raw as unknown[]\n return [toCount(pair?.[0]), toCount(pair?.[1])]\n },\n )\n if (viaScript !== undefined) return viaScript\n\n const previous = await this.get(previousKey)\n const current = await this.incr(currentKey)\n if (current === 1) await this.pexpire(currentKey, ttlMs)\n return [previous, current]\n }\n}\n","import { FIXED_WINDOW_SCRIPT, SLIDING_WINDOW_SCRIPT } from '../lua.js'\nimport type { Store } from '../types.js'\n\nexport interface UpstashRestOptions {\n url: string\n token: string\n /** Custom fetch implementation. Defaults to the global `fetch`. */\n fetch?: typeof fetch\n}\n\n/** Coerce a REST result into a non-negative counter value. */\nfunction toCount(value: unknown): number {\n if (value == null || value === false) return 0\n const n = Number(value)\n return Number.isFinite(n) ? n : 0\n}\n\n/**\n * Zero-dependency store backed by the Upstash Redis REST API. Uses `fetch`, so\n * it runs anywhere the runtime provides one (Node 18+, edge, workers) without\n * pulling in a Redis client. This is what env auto-detection constructs.\n */\nexport class UpstashRestStore implements Store {\n private readonly url: string\n private readonly token: string\n private readonly fetchImpl: typeof fetch\n\n constructor(options: UpstashRestOptions) {\n this.url = options.url.replace(/\\/$/, '')\n this.token = options.token\n const f = options.fetch ?? globalThis.fetch\n if (typeof f !== 'function') {\n throw new Error('ratefall: no global fetch available; pass a `fetch` implementation to UpstashRestStore.')\n }\n this.fetchImpl = f\n }\n\n private async command(args: (string | number)[]): Promise<unknown> {\n const res = await this.fetchImpl(this.url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(args.map(String)),\n })\n if (!res.ok) {\n const detail = await res.text().catch(() => '')\n throw new Error(`ratefall: Upstash REST error ${res.status} ${res.statusText}${detail ? ` - ${detail}` : ''}`)\n }\n const body = (await res.json()) as { result?: unknown; error?: string }\n if (body.error) {\n throw new Error(`ratefall: Upstash command failed - ${body.error}`)\n }\n return body.result\n }\n\n async incr(key: string): Promise<number> {\n return Number(await this.command(['INCR', key]))\n }\n\n async pexpire(key: string, ms: number): Promise<void> {\n await this.command(['PEXPIRE', key, ms])\n }\n\n async get(key: string): Promise<number> {\n return toCount(await this.command(['GET', key]))\n }\n\n /** Run a Lua script through the REST API. Keys come first, then args. */\n private async evalScript(\n script: string,\n keys: string[],\n args: (string | number)[],\n ): Promise<unknown> {\n return this.command(['EVAL', script, keys.length, ...keys, ...args])\n }\n\n async fixedWindowStep(key: string, ttlMs: number): Promise<number> {\n return toCount(await this.evalScript(FIXED_WINDOW_SCRIPT, [key], [ttlMs]))\n }\n\n async slidingWindowStep(\n currentKey: string,\n previousKey: string,\n ttlMs: number,\n ): Promise<[number, number]> {\n const raw = (await this.evalScript(SLIDING_WINDOW_SCRIPT, [currentKey, previousKey], [ttlMs])) as unknown[]\n return [toCount(raw?.[0]), toCount(raw?.[1])]\n }\n}\n","import type { RateLimitOptions, Store } from './types.js'\nimport { MemoryStore } from './stores/memory.js'\nimport { RedisStore } from './stores/redis.js'\nimport { UpstashRestStore } from './stores/upstash-rest.js'\n\nexport interface ResolvedStore {\n store: Store\n /** Whether the store is shared across processes (Redis) vs per-instance (memory). */\n distributed: boolean\n /** Human-readable source, for the dev warning. */\n source: string\n}\n\nfunction readEnv(key: string): string | undefined {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env\n return env?.[key]\n}\n\n/**\n * Decide which store to use, in priority order:\n * 1. an explicit `store`\n * 2. an explicit `redis` client\n * 3. `UPSTASH_REDIS_REST_URL` + `UPSTASH_REDIS_REST_TOKEN` in the environment\n * 4. in-memory fallback\n */\nexport function resolveStore(options: RateLimitOptions): ResolvedStore {\n if (options.store) {\n return { store: options.store, distributed: true, source: 'custom store' }\n }\n\n if (options.redis) {\n const store = new RedisStore(options.redis, { evalStyle: options.evalStyle })\n return {\n store,\n distributed: true,\n source: store.atomic ? 'redis client (atomic)' : 'redis client',\n }\n }\n\n const url = readEnv('UPSTASH_REDIS_REST_URL')\n const token = readEnv('UPSTASH_REDIS_REST_TOKEN')\n if (url && token) {\n return { store: new UpstashRestStore({ url, token }), distributed: true, source: 'Upstash REST (env)' }\n }\n\n return { store: new MemoryStore(), distributed: false, source: 'in-memory' }\n}\n\n/** True when the runtime looks like a production deployment. */\nexport function isProduction(): boolean {\n return readEnv('NODE_ENV') === 'production'\n}\n","const UNITS: Record<string, number> = {\n ms: 1,\n s: 1000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n}\n\nconst PATTERN = /^(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)$/\n\n/**\n * Normalize a window into milliseconds. Accepts a raw millisecond number or a\n * duration string like `'500ms'`, `'10s'`, `'1m'`, `'2h'`, `'1d'`.\n */\nexport function parseWindow(window: string | number): number {\n if (typeof window === 'number') {\n if (!Number.isFinite(window) || window <= 0) {\n throw new RangeError(`ratefall: window must be a positive number of milliseconds, got ${window}`)\n }\n return Math.floor(window)\n }\n\n const match = PATTERN.exec(window.trim())\n if (!match) {\n throw new TypeError(`ratefall: invalid window \"${window}\". Use a number of ms or a string like \"10s\", \"1m\", \"2h\".`)\n }\n\n const value = Number(match[1])\n const unit = UNITS[match[2] as string] as number\n const ms = Math.floor(value * unit)\n if (ms <= 0) {\n throw new RangeError(`ratefall: window must be greater than 0, got \"${window}\".`)\n }\n return ms\n}\n","import type { AlgorithmContext, AlgorithmFn } from './algorithms/index.js'\nimport { fixedWindow } from './algorithms/fixed-window.js'\nimport { slidingWindow } from './algorithms/sliding-window.js'\nimport { isProduction, resolveStore } from './resolve-store.js'\nimport type { RateLimiter, RateLimitOptions } from './types.js'\nimport { parseWindow } from './window.js'\n\nconst ALGORITHMS: Record<string, AlgorithmFn> = {\n 'fixed-window': fixedWindow,\n 'sliding-window': slidingWindow,\n}\n\n/**\n * Create a rate limiter. With no store configured it uses an in-memory counter\n * (great for local dev); set `UPSTASH_REDIS_REST_URL` + `UPSTASH_REDIS_REST_TOKEN`,\n * or pass a `redis` client, and the exact same code becomes distributed.\n *\n * @example\n * const limiter = rateLimit({ limit: 10, window: '10s' })\n * const { success } = await limiter.limit(userId)\n * if (!success) return new Response('slow down', { status: 429 })\n */\nexport function rateLimit(options: RateLimitOptions): RateLimiter {\n const { limit, algorithm = 'fixed-window', prefix = 'ratefall', silent = false } = options\n\n if (!Number.isInteger(limit) || limit <= 0) {\n throw new RangeError(`ratefall: limit must be a positive integer, got ${limit}`)\n }\n\n const run = ALGORITHMS[algorithm]\n if (!run) {\n throw new TypeError(`ratefall: unknown algorithm \"${algorithm}\". Use \"fixed-window\" or \"sliding-window\".`)\n }\n\n const windowMs = parseWindow(options.window)\n const { store, distributed, source } = resolveStore(options)\n\n if (!distributed && !silent && isProduction()) {\n // Not an error: single-instance prod is legitimate. But un-shared limits in a\n // multi-instance deploy are a footgun, so make it loud exactly once.\n console.warn(\n `ratefall: running with an in-memory store in production. Limits are per-instance and NOT shared. ` +\n `Set UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN or pass a \\`redis\\` client. Pass \\`silent: true\\` to hide this.`,\n )\n }\n\n const ctx: AlgorithmContext = { limit, windowMs, prefix }\n\n return {\n async limit(identifier: string) {\n if (!identifier) {\n throw new TypeError('ratefall: identifier must be a non-empty string.')\n }\n return run(store, ctx, identifier, Date.now())\n },\n }\n}\n\n/** The resolved store source, exposed for debugging/tests. */\nexport { resolveStore } from './resolve-store.js'\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -15,6 +15,10 @@ interface RateLimitResult {
|
|
|
15
15
|
* Minimal counter backend. Mirrors the handful of Redis commands the
|
|
16
16
|
* algorithms need, so any store (memory, Upstash REST, a duck-typed Redis
|
|
17
17
|
* client) can satisfy it.
|
|
18
|
+
*
|
|
19
|
+
* The three required methods are the portable fallback path. A store may also
|
|
20
|
+
* implement either optional method below to collapse a check into one atomic
|
|
21
|
+
* operation; the algorithms use them automatically when present.
|
|
18
22
|
*/
|
|
19
23
|
interface Store {
|
|
20
24
|
/** Increment the counter at `key`, returning the new value. */
|
|
@@ -23,16 +27,41 @@ interface Store {
|
|
|
23
27
|
pexpire(key: string, ms: number): Promise<void>;
|
|
24
28
|
/** Read the counter at `key`. Returns 0 when the key is absent or expired. */
|
|
25
29
|
get(key: string): Promise<number>;
|
|
30
|
+
/**
|
|
31
|
+
* Optional atomic fixed-window step: increment `key` and set its TTL to
|
|
32
|
+
* `ttlMs` if this was the first hit, in one indivisible operation. Returns
|
|
33
|
+
* the new count.
|
|
34
|
+
*/
|
|
35
|
+
fixedWindowStep?(key: string, ttlMs: number): Promise<number>;
|
|
36
|
+
/**
|
|
37
|
+
* Optional atomic sliding-window step: read `previousKey` and increment
|
|
38
|
+
* `currentKey` (setting its TTL to `ttlMs` on first hit) in one indivisible
|
|
39
|
+
* operation. Returns `[previousCount, currentCount]`.
|
|
40
|
+
*/
|
|
41
|
+
slidingWindowStep?(currentKey: string, previousKey: string, ttlMs: number): Promise<[previous: number, current: number]>;
|
|
26
42
|
}
|
|
27
43
|
/**
|
|
28
44
|
* The subset of a Redis client `ratefall` uses. Both `@upstash/redis` and
|
|
29
45
|
* `ioredis` satisfy this shape, so you can pass either without an adapter.
|
|
46
|
+
*
|
|
47
|
+
* `eval` is optional. When the client exposes it, `ratefall` uses the atomic
|
|
48
|
+
* single-round-trip path; otherwise it falls back to `incr` + `pexpire`.
|
|
30
49
|
*/
|
|
31
50
|
interface RedisLike {
|
|
32
51
|
incr(key: string): Promise<number>;
|
|
33
52
|
pexpire(key: string, ms: number): Promise<unknown>;
|
|
34
53
|
get(key: string): Promise<string | number | null>;
|
|
54
|
+
eval?(...args: never[]): Promise<unknown>;
|
|
35
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* How a Redis client's `eval` wants its arguments.
|
|
58
|
+
*
|
|
59
|
+
* - `'ioredis'`: `eval(script, numKeys, ...keys, ...args)`
|
|
60
|
+
* - `'upstash'`: `eval(script, keys[], args[])`
|
|
61
|
+
* - `'auto'`: sniff the client (default)
|
|
62
|
+
* - `'none'`: never use `eval`, always take the portable fallback path
|
|
63
|
+
*/
|
|
64
|
+
type EvalStyle = 'auto' | 'ioredis' | 'upstash' | 'none';
|
|
36
65
|
type Algorithm = 'fixed-window' | 'sliding-window';
|
|
37
66
|
interface RateLimitOptions {
|
|
38
67
|
/** Maximum number of requests allowed per window. */
|
|
@@ -47,6 +76,11 @@ interface RateLimitOptions {
|
|
|
47
76
|
store?: Store;
|
|
48
77
|
/** A Redis client (`@upstash/redis`, `ioredis`, ...). Overrides env auto-detection. */
|
|
49
78
|
redis?: RedisLike;
|
|
79
|
+
/**
|
|
80
|
+
* How to call the Redis client's `eval`. Defaults to `'auto'`, which detects
|
|
81
|
+
* `ioredis` and `@upstash/redis`. Set `'none'` to disable the Lua path.
|
|
82
|
+
*/
|
|
83
|
+
evalStyle?: EvalStyle;
|
|
50
84
|
/** Suppress the "no distributed store in production" warning. */
|
|
51
85
|
silent?: boolean;
|
|
52
86
|
}
|
|
@@ -99,21 +133,57 @@ declare class MemoryStore implements Store {
|
|
|
99
133
|
incr(key: string): Promise<number>;
|
|
100
134
|
pexpire(key: string, ms: number): Promise<void>;
|
|
101
135
|
get(key: string): Promise<number>;
|
|
136
|
+
/**
|
|
137
|
+
* Atomic fixed-window step. JavaScript is single threaded and none of this
|
|
138
|
+
* awaits, so the increment and the TTL land together with no interleaving.
|
|
139
|
+
*/
|
|
140
|
+
fixedWindowStep(key: string, ttlMs: number): Promise<number>;
|
|
141
|
+
/** Atomic sliding-window step. Same reasoning as {@link fixedWindowStep}. */
|
|
142
|
+
slidingWindowStep(currentKey: string, previousKey: string, ttlMs: number): Promise<[number, number]>;
|
|
102
143
|
private sweep;
|
|
103
144
|
/** Stop the background sweeper. Optional; the timer is unref'd. */
|
|
104
145
|
dispose(): void;
|
|
105
146
|
}
|
|
106
147
|
|
|
148
|
+
interface RedisStoreOptions {
|
|
149
|
+
/** How to call the client's `eval`. Defaults to `'auto'`. */
|
|
150
|
+
evalStyle?: EvalStyle;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Decide which `eval` calling convention a client uses.
|
|
154
|
+
*
|
|
155
|
+
* `ioredis` is the odd one out: it takes a key count followed by flat keys and
|
|
156
|
+
* args, and it is the only one of the two that exposes `defineCommand`. Every
|
|
157
|
+
* other client we support (`@upstash/redis`) takes two arrays.
|
|
158
|
+
*/
|
|
159
|
+
declare function detectEvalStyle(client: RedisLike): Exclude<EvalStyle, 'auto'>;
|
|
107
160
|
/**
|
|
108
161
|
* Adapts any Redis client with `incr` / `pexpire` / `get` (both `@upstash/redis`
|
|
109
162
|
* and `ioredis` qualify) to the {@link Store} interface.
|
|
163
|
+
*
|
|
164
|
+
* When the client also exposes `eval`, checks run as a single atomic Lua call
|
|
165
|
+
* instead of two or three separate commands. If a server rejects scripting the
|
|
166
|
+
* store degrades to the portable path once and stays there.
|
|
110
167
|
*/
|
|
111
168
|
declare class RedisStore implements Store {
|
|
112
169
|
private readonly client;
|
|
113
|
-
|
|
170
|
+
private style;
|
|
171
|
+
constructor(client: RedisLike, options?: RedisStoreOptions);
|
|
172
|
+
/** True when this store will take the atomic Lua path. */
|
|
173
|
+
get atomic(): boolean;
|
|
174
|
+
private runScript;
|
|
175
|
+
/**
|
|
176
|
+
* Run `script`, falling back to the portable path forever if the server or
|
|
177
|
+
* client cannot handle scripting. A single failure is enough to decide: a
|
|
178
|
+
* server either supports EVAL or it does not, so retrying every request would
|
|
179
|
+
* just double the cost of every check on that deployment.
|
|
180
|
+
*/
|
|
181
|
+
private tryScript;
|
|
114
182
|
incr(key: string): Promise<number>;
|
|
115
183
|
pexpire(key: string, ms: number): Promise<void>;
|
|
116
184
|
get(key: string): Promise<number>;
|
|
185
|
+
fixedWindowStep(key: string, ttlMs: number): Promise<number>;
|
|
186
|
+
slidingWindowStep(currentKey: string, previousKey: string, ttlMs: number): Promise<[number, number]>;
|
|
117
187
|
}
|
|
118
188
|
|
|
119
189
|
interface UpstashRestOptions {
|
|
@@ -136,6 +206,10 @@ declare class UpstashRestStore implements Store {
|
|
|
136
206
|
incr(key: string): Promise<number>;
|
|
137
207
|
pexpire(key: string, ms: number): Promise<void>;
|
|
138
208
|
get(key: string): Promise<number>;
|
|
209
|
+
/** Run a Lua script through the REST API. Keys come first, then args. */
|
|
210
|
+
private evalScript;
|
|
211
|
+
fixedWindowStep(key: string, ttlMs: number): Promise<number>;
|
|
212
|
+
slidingWindowStep(currentKey: string, previousKey: string, ttlMs: number): Promise<[number, number]>;
|
|
139
213
|
}
|
|
140
214
|
|
|
141
215
|
/**
|
|
@@ -144,4 +218,44 @@ declare class UpstashRestStore implements Store {
|
|
|
144
218
|
*/
|
|
145
219
|
declare function parseWindow(window: string | number): number;
|
|
146
220
|
|
|
147
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Lua scripts for the single-round-trip Redis path.
|
|
223
|
+
*
|
|
224
|
+
* The fallback path (`INCR` then a conditional `PEXPIRE`) costs two round trips
|
|
225
|
+
* on the first hit of every window, and is not atomic: a process that dies
|
|
226
|
+
* between the two commands leaves a key with no TTL, which then leaks in Redis
|
|
227
|
+
* forever. Verified against Redis 7.4: `INCR` on a fresh key reports `TTL -1`
|
|
228
|
+
* until the `PEXPIRE` lands.
|
|
229
|
+
*
|
|
230
|
+
* Running the same work inside `EVAL` fixes both: one round trip, and a TTL
|
|
231
|
+
* that cannot be orphaned.
|
|
232
|
+
*
|
|
233
|
+
* Note on what this does NOT fix. It is tempting to claim the fallback races
|
|
234
|
+
* under concurrency. It does not, at least not in a way that overshoots the
|
|
235
|
+
* limit: the previous window's counter is a completed window that nothing
|
|
236
|
+
* increments any more, so every concurrent caller reads the same stable value,
|
|
237
|
+
* and `INCR` on the current key is atomic, so each caller gets a distinct
|
|
238
|
+
* count. Measured over 50 concurrent callers against a real Redis, both paths
|
|
239
|
+
* allow exactly the same number through.
|
|
240
|
+
*/
|
|
241
|
+
/**
|
|
242
|
+
* Fixed window. Increments the counter and sets the TTL on first touch.
|
|
243
|
+
*
|
|
244
|
+
* KEYS[1] window counter
|
|
245
|
+
* ARGV[1] window length in milliseconds
|
|
246
|
+
* returns the new count
|
|
247
|
+
*/
|
|
248
|
+
declare const FIXED_WINDOW_SCRIPT: string;
|
|
249
|
+
/**
|
|
250
|
+
* Sliding window. Reads the previous window's counter and increments the
|
|
251
|
+
* current one as a single atomic unit.
|
|
252
|
+
*
|
|
253
|
+
* KEYS[1] current window counter
|
|
254
|
+
* KEYS[2] previous window counter
|
|
255
|
+
* ARGV[1] retention in milliseconds (two windows, so the current counter is
|
|
256
|
+
* still readable as "previous" during the next window)
|
|
257
|
+
* returns { previousCount, currentCount }
|
|
258
|
+
*/
|
|
259
|
+
declare const SLIDING_WINDOW_SCRIPT: string;
|
|
260
|
+
|
|
261
|
+
export { type Algorithm, type EvalStyle, FIXED_WINDOW_SCRIPT, MemoryStore, type RateLimitOptions, type RateLimitResult, type RateLimiter, type RedisLike, RedisStore, type RedisStoreOptions, SLIDING_WINDOW_SCRIPT, type Store, type UpstashRestOptions, UpstashRestStore, detectEvalStyle, parseWindow, rateLimit, resolveStore };
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ interface RateLimitResult {
|
|
|
15
15
|
* Minimal counter backend. Mirrors the handful of Redis commands the
|
|
16
16
|
* algorithms need, so any store (memory, Upstash REST, a duck-typed Redis
|
|
17
17
|
* client) can satisfy it.
|
|
18
|
+
*
|
|
19
|
+
* The three required methods are the portable fallback path. A store may also
|
|
20
|
+
* implement either optional method below to collapse a check into one atomic
|
|
21
|
+
* operation; the algorithms use them automatically when present.
|
|
18
22
|
*/
|
|
19
23
|
interface Store {
|
|
20
24
|
/** Increment the counter at `key`, returning the new value. */
|
|
@@ -23,16 +27,41 @@ interface Store {
|
|
|
23
27
|
pexpire(key: string, ms: number): Promise<void>;
|
|
24
28
|
/** Read the counter at `key`. Returns 0 when the key is absent or expired. */
|
|
25
29
|
get(key: string): Promise<number>;
|
|
30
|
+
/**
|
|
31
|
+
* Optional atomic fixed-window step: increment `key` and set its TTL to
|
|
32
|
+
* `ttlMs` if this was the first hit, in one indivisible operation. Returns
|
|
33
|
+
* the new count.
|
|
34
|
+
*/
|
|
35
|
+
fixedWindowStep?(key: string, ttlMs: number): Promise<number>;
|
|
36
|
+
/**
|
|
37
|
+
* Optional atomic sliding-window step: read `previousKey` and increment
|
|
38
|
+
* `currentKey` (setting its TTL to `ttlMs` on first hit) in one indivisible
|
|
39
|
+
* operation. Returns `[previousCount, currentCount]`.
|
|
40
|
+
*/
|
|
41
|
+
slidingWindowStep?(currentKey: string, previousKey: string, ttlMs: number): Promise<[previous: number, current: number]>;
|
|
26
42
|
}
|
|
27
43
|
/**
|
|
28
44
|
* The subset of a Redis client `ratefall` uses. Both `@upstash/redis` and
|
|
29
45
|
* `ioredis` satisfy this shape, so you can pass either without an adapter.
|
|
46
|
+
*
|
|
47
|
+
* `eval` is optional. When the client exposes it, `ratefall` uses the atomic
|
|
48
|
+
* single-round-trip path; otherwise it falls back to `incr` + `pexpire`.
|
|
30
49
|
*/
|
|
31
50
|
interface RedisLike {
|
|
32
51
|
incr(key: string): Promise<number>;
|
|
33
52
|
pexpire(key: string, ms: number): Promise<unknown>;
|
|
34
53
|
get(key: string): Promise<string | number | null>;
|
|
54
|
+
eval?(...args: never[]): Promise<unknown>;
|
|
35
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* How a Redis client's `eval` wants its arguments.
|
|
58
|
+
*
|
|
59
|
+
* - `'ioredis'`: `eval(script, numKeys, ...keys, ...args)`
|
|
60
|
+
* - `'upstash'`: `eval(script, keys[], args[])`
|
|
61
|
+
* - `'auto'`: sniff the client (default)
|
|
62
|
+
* - `'none'`: never use `eval`, always take the portable fallback path
|
|
63
|
+
*/
|
|
64
|
+
type EvalStyle = 'auto' | 'ioredis' | 'upstash' | 'none';
|
|
36
65
|
type Algorithm = 'fixed-window' | 'sliding-window';
|
|
37
66
|
interface RateLimitOptions {
|
|
38
67
|
/** Maximum number of requests allowed per window. */
|
|
@@ -47,6 +76,11 @@ interface RateLimitOptions {
|
|
|
47
76
|
store?: Store;
|
|
48
77
|
/** A Redis client (`@upstash/redis`, `ioredis`, ...). Overrides env auto-detection. */
|
|
49
78
|
redis?: RedisLike;
|
|
79
|
+
/**
|
|
80
|
+
* How to call the Redis client's `eval`. Defaults to `'auto'`, which detects
|
|
81
|
+
* `ioredis` and `@upstash/redis`. Set `'none'` to disable the Lua path.
|
|
82
|
+
*/
|
|
83
|
+
evalStyle?: EvalStyle;
|
|
50
84
|
/** Suppress the "no distributed store in production" warning. */
|
|
51
85
|
silent?: boolean;
|
|
52
86
|
}
|
|
@@ -99,21 +133,57 @@ declare class MemoryStore implements Store {
|
|
|
99
133
|
incr(key: string): Promise<number>;
|
|
100
134
|
pexpire(key: string, ms: number): Promise<void>;
|
|
101
135
|
get(key: string): Promise<number>;
|
|
136
|
+
/**
|
|
137
|
+
* Atomic fixed-window step. JavaScript is single threaded and none of this
|
|
138
|
+
* awaits, so the increment and the TTL land together with no interleaving.
|
|
139
|
+
*/
|
|
140
|
+
fixedWindowStep(key: string, ttlMs: number): Promise<number>;
|
|
141
|
+
/** Atomic sliding-window step. Same reasoning as {@link fixedWindowStep}. */
|
|
142
|
+
slidingWindowStep(currentKey: string, previousKey: string, ttlMs: number): Promise<[number, number]>;
|
|
102
143
|
private sweep;
|
|
103
144
|
/** Stop the background sweeper. Optional; the timer is unref'd. */
|
|
104
145
|
dispose(): void;
|
|
105
146
|
}
|
|
106
147
|
|
|
148
|
+
interface RedisStoreOptions {
|
|
149
|
+
/** How to call the client's `eval`. Defaults to `'auto'`. */
|
|
150
|
+
evalStyle?: EvalStyle;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Decide which `eval` calling convention a client uses.
|
|
154
|
+
*
|
|
155
|
+
* `ioredis` is the odd one out: it takes a key count followed by flat keys and
|
|
156
|
+
* args, and it is the only one of the two that exposes `defineCommand`. Every
|
|
157
|
+
* other client we support (`@upstash/redis`) takes two arrays.
|
|
158
|
+
*/
|
|
159
|
+
declare function detectEvalStyle(client: RedisLike): Exclude<EvalStyle, 'auto'>;
|
|
107
160
|
/**
|
|
108
161
|
* Adapts any Redis client with `incr` / `pexpire` / `get` (both `@upstash/redis`
|
|
109
162
|
* and `ioredis` qualify) to the {@link Store} interface.
|
|
163
|
+
*
|
|
164
|
+
* When the client also exposes `eval`, checks run as a single atomic Lua call
|
|
165
|
+
* instead of two or three separate commands. If a server rejects scripting the
|
|
166
|
+
* store degrades to the portable path once and stays there.
|
|
110
167
|
*/
|
|
111
168
|
declare class RedisStore implements Store {
|
|
112
169
|
private readonly client;
|
|
113
|
-
|
|
170
|
+
private style;
|
|
171
|
+
constructor(client: RedisLike, options?: RedisStoreOptions);
|
|
172
|
+
/** True when this store will take the atomic Lua path. */
|
|
173
|
+
get atomic(): boolean;
|
|
174
|
+
private runScript;
|
|
175
|
+
/**
|
|
176
|
+
* Run `script`, falling back to the portable path forever if the server or
|
|
177
|
+
* client cannot handle scripting. A single failure is enough to decide: a
|
|
178
|
+
* server either supports EVAL or it does not, so retrying every request would
|
|
179
|
+
* just double the cost of every check on that deployment.
|
|
180
|
+
*/
|
|
181
|
+
private tryScript;
|
|
114
182
|
incr(key: string): Promise<number>;
|
|
115
183
|
pexpire(key: string, ms: number): Promise<void>;
|
|
116
184
|
get(key: string): Promise<number>;
|
|
185
|
+
fixedWindowStep(key: string, ttlMs: number): Promise<number>;
|
|
186
|
+
slidingWindowStep(currentKey: string, previousKey: string, ttlMs: number): Promise<[number, number]>;
|
|
117
187
|
}
|
|
118
188
|
|
|
119
189
|
interface UpstashRestOptions {
|
|
@@ -136,6 +206,10 @@ declare class UpstashRestStore implements Store {
|
|
|
136
206
|
incr(key: string): Promise<number>;
|
|
137
207
|
pexpire(key: string, ms: number): Promise<void>;
|
|
138
208
|
get(key: string): Promise<number>;
|
|
209
|
+
/** Run a Lua script through the REST API. Keys come first, then args. */
|
|
210
|
+
private evalScript;
|
|
211
|
+
fixedWindowStep(key: string, ttlMs: number): Promise<number>;
|
|
212
|
+
slidingWindowStep(currentKey: string, previousKey: string, ttlMs: number): Promise<[number, number]>;
|
|
139
213
|
}
|
|
140
214
|
|
|
141
215
|
/**
|
|
@@ -144,4 +218,44 @@ declare class UpstashRestStore implements Store {
|
|
|
144
218
|
*/
|
|
145
219
|
declare function parseWindow(window: string | number): number;
|
|
146
220
|
|
|
147
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Lua scripts for the single-round-trip Redis path.
|
|
223
|
+
*
|
|
224
|
+
* The fallback path (`INCR` then a conditional `PEXPIRE`) costs two round trips
|
|
225
|
+
* on the first hit of every window, and is not atomic: a process that dies
|
|
226
|
+
* between the two commands leaves a key with no TTL, which then leaks in Redis
|
|
227
|
+
* forever. Verified against Redis 7.4: `INCR` on a fresh key reports `TTL -1`
|
|
228
|
+
* until the `PEXPIRE` lands.
|
|
229
|
+
*
|
|
230
|
+
* Running the same work inside `EVAL` fixes both: one round trip, and a TTL
|
|
231
|
+
* that cannot be orphaned.
|
|
232
|
+
*
|
|
233
|
+
* Note on what this does NOT fix. It is tempting to claim the fallback races
|
|
234
|
+
* under concurrency. It does not, at least not in a way that overshoots the
|
|
235
|
+
* limit: the previous window's counter is a completed window that nothing
|
|
236
|
+
* increments any more, so every concurrent caller reads the same stable value,
|
|
237
|
+
* and `INCR` on the current key is atomic, so each caller gets a distinct
|
|
238
|
+
* count. Measured over 50 concurrent callers against a real Redis, both paths
|
|
239
|
+
* allow exactly the same number through.
|
|
240
|
+
*/
|
|
241
|
+
/**
|
|
242
|
+
* Fixed window. Increments the counter and sets the TTL on first touch.
|
|
243
|
+
*
|
|
244
|
+
* KEYS[1] window counter
|
|
245
|
+
* ARGV[1] window length in milliseconds
|
|
246
|
+
* returns the new count
|
|
247
|
+
*/
|
|
248
|
+
declare const FIXED_WINDOW_SCRIPT: string;
|
|
249
|
+
/**
|
|
250
|
+
* Sliding window. Reads the previous window's counter and increments the
|
|
251
|
+
* current one as a single atomic unit.
|
|
252
|
+
*
|
|
253
|
+
* KEYS[1] current window counter
|
|
254
|
+
* KEYS[2] previous window counter
|
|
255
|
+
* ARGV[1] retention in milliseconds (two windows, so the current counter is
|
|
256
|
+
* still readable as "previous" during the next window)
|
|
257
|
+
* returns { previousCount, currentCount }
|
|
258
|
+
*/
|
|
259
|
+
declare const SLIDING_WINDOW_SCRIPT: string;
|
|
260
|
+
|
|
261
|
+
export { type Algorithm, type EvalStyle, FIXED_WINDOW_SCRIPT, MemoryStore, type RateLimitOptions, type RateLimitResult, type RateLimiter, type RedisLike, RedisStore, type RedisStoreOptions, SLIDING_WINDOW_SCRIPT, type Store, type UpstashRestOptions, UpstashRestStore, detectEvalStyle, parseWindow, rateLimit, resolveStore };
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// src/algorithms/fixed-window.ts
|
|
2
|
+
async function step(store, key, ttlMs) {
|
|
3
|
+
if (store.fixedWindowStep) return store.fixedWindowStep(key, ttlMs);
|
|
4
|
+
const count = await store.incr(key);
|
|
5
|
+
if (count === 1) await store.pexpire(key, ttlMs);
|
|
6
|
+
return count;
|
|
7
|
+
}
|
|
2
8
|
var fixedWindow = async (store, ctx, identifier, now) => {
|
|
3
9
|
const { limit, windowMs, prefix } = ctx;
|
|
4
10
|
const windowStart = Math.floor(now / windowMs) * windowMs;
|
|
5
11
|
const key = `${prefix}:${identifier}:${windowStart}`;
|
|
6
|
-
const count = await store
|
|
7
|
-
if (count === 1) await store.pexpire(key, windowMs);
|
|
12
|
+
const count = await step(store, key, windowMs);
|
|
8
13
|
const reset = windowStart + windowMs;
|
|
9
14
|
const success = count <= limit;
|
|
10
15
|
return {
|
|
@@ -17,6 +22,13 @@ var fixedWindow = async (store, ctx, identifier, now) => {
|
|
|
17
22
|
};
|
|
18
23
|
|
|
19
24
|
// src/algorithms/sliding-window.ts
|
|
25
|
+
async function step2(store, currentKey, previousKey, ttlMs) {
|
|
26
|
+
if (store.slidingWindowStep) return store.slidingWindowStep(currentKey, previousKey, ttlMs);
|
|
27
|
+
const previous = await store.get(previousKey);
|
|
28
|
+
const current = await store.incr(currentKey);
|
|
29
|
+
if (current === 1) await store.pexpire(currentKey, ttlMs);
|
|
30
|
+
return [previous, current];
|
|
31
|
+
}
|
|
20
32
|
var slidingWindow = async (store, ctx, identifier, now) => {
|
|
21
33
|
const { limit, windowMs, prefix } = ctx;
|
|
22
34
|
const currentStart = Math.floor(now / windowMs) * windowMs;
|
|
@@ -24,9 +36,7 @@ var slidingWindow = async (store, ctx, identifier, now) => {
|
|
|
24
36
|
const fraction = (now - currentStart) / windowMs;
|
|
25
37
|
const currentKey = `${prefix}:${identifier}:${currentStart}`;
|
|
26
38
|
const previousKey = `${prefix}:${identifier}:${previousStart}`;
|
|
27
|
-
const previousCount = await store
|
|
28
|
-
const currentCount = await store.incr(currentKey);
|
|
29
|
-
if (currentCount === 1) await store.pexpire(currentKey, windowMs * 2);
|
|
39
|
+
const [previousCount, currentCount] = await step2(store, currentKey, previousKey, windowMs * 2);
|
|
30
40
|
const weighted = previousCount * (1 - fraction) + currentCount;
|
|
31
41
|
const reset = currentStart + windowMs;
|
|
32
42
|
const success = weighted <= limit;
|
|
@@ -75,6 +85,25 @@ var MemoryStore = class {
|
|
|
75
85
|
async get(key) {
|
|
76
86
|
return this.live(key)?.count ?? 0;
|
|
77
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Atomic fixed-window step. JavaScript is single threaded and none of this
|
|
90
|
+
* awaits, so the increment and the TTL land together with no interleaving.
|
|
91
|
+
*/
|
|
92
|
+
async fixedWindowStep(key, ttlMs) {
|
|
93
|
+
const entry = this.live(key);
|
|
94
|
+
if (!entry) {
|
|
95
|
+
this.map.set(key, { count: 1, expiresAt: Date.now() + ttlMs });
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
entry.count += 1;
|
|
99
|
+
return entry.count;
|
|
100
|
+
}
|
|
101
|
+
/** Atomic sliding-window step. Same reasoning as {@link fixedWindowStep}. */
|
|
102
|
+
async slidingWindowStep(currentKey, previousKey, ttlMs) {
|
|
103
|
+
const previous = this.live(previousKey)?.count ?? 0;
|
|
104
|
+
const current = await this.fixedWindowStep(currentKey, ttlMs);
|
|
105
|
+
return [previous, current];
|
|
106
|
+
}
|
|
78
107
|
sweep() {
|
|
79
108
|
const now = Date.now();
|
|
80
109
|
for (const [key, entry] of this.map) {
|
|
@@ -87,12 +116,75 @@ var MemoryStore = class {
|
|
|
87
116
|
}
|
|
88
117
|
};
|
|
89
118
|
|
|
119
|
+
// src/lua.ts
|
|
120
|
+
var FIXED_WINDOW_SCRIPT = `
|
|
121
|
+
local count = redis.call('INCR', KEYS[1])
|
|
122
|
+
if count == 1 then
|
|
123
|
+
redis.call('PEXPIRE', KEYS[1], ARGV[1])
|
|
124
|
+
end
|
|
125
|
+
return count
|
|
126
|
+
`.trim();
|
|
127
|
+
var SLIDING_WINDOW_SCRIPT = `
|
|
128
|
+
local previous = redis.call('GET', KEYS[2])
|
|
129
|
+
local previousCount = 0
|
|
130
|
+
if previous then
|
|
131
|
+
previousCount = math.floor(tonumber(previous) or 0)
|
|
132
|
+
end
|
|
133
|
+
local current = redis.call('INCR', KEYS[1])
|
|
134
|
+
if current == 1 then
|
|
135
|
+
redis.call('PEXPIRE', KEYS[1], ARGV[1])
|
|
136
|
+
end
|
|
137
|
+
return { previousCount, current }
|
|
138
|
+
`.trim();
|
|
139
|
+
|
|
90
140
|
// src/stores/redis.ts
|
|
141
|
+
function toCount(value) {
|
|
142
|
+
if (value == null || value === false) return 0;
|
|
143
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
144
|
+
return Number.isFinite(n) ? n : 0;
|
|
145
|
+
}
|
|
146
|
+
function detectEvalStyle(client) {
|
|
147
|
+
if (typeof client.eval !== "function") return "none";
|
|
148
|
+
if (typeof client.defineCommand === "function") return "ioredis";
|
|
149
|
+
return "upstash";
|
|
150
|
+
}
|
|
91
151
|
var RedisStore = class {
|
|
92
|
-
|
|
152
|
+
client;
|
|
153
|
+
style;
|
|
154
|
+
constructor(client, options = {}) {
|
|
93
155
|
this.client = client;
|
|
156
|
+
const requested = options.evalStyle ?? "auto";
|
|
157
|
+
this.style = requested === "auto" ? detectEvalStyle(client) : requested;
|
|
158
|
+
if (this.style !== "none" && typeof client.eval !== "function") {
|
|
159
|
+
this.style = "none";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** True when this store will take the atomic Lua path. */
|
|
163
|
+
get atomic() {
|
|
164
|
+
return this.style !== "none";
|
|
165
|
+
}
|
|
166
|
+
async runScript(script, keys, args) {
|
|
167
|
+
const evalFn = this.client.eval;
|
|
168
|
+
if (this.style === "ioredis") {
|
|
169
|
+
return evalFn.call(this.client, script, keys.length, ...keys, ...args);
|
|
170
|
+
}
|
|
171
|
+
return evalFn.call(this.client, script, keys, args);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Run `script`, falling back to the portable path forever if the server or
|
|
175
|
+
* client cannot handle scripting. A single failure is enough to decide: a
|
|
176
|
+
* server either supports EVAL or it does not, so retrying every request would
|
|
177
|
+
* just double the cost of every check on that deployment.
|
|
178
|
+
*/
|
|
179
|
+
async tryScript(script, keys, args, parse) {
|
|
180
|
+
if (this.style === "none") return void 0;
|
|
181
|
+
try {
|
|
182
|
+
return parse(await this.runScript(script, keys, args));
|
|
183
|
+
} catch {
|
|
184
|
+
this.style = "none";
|
|
185
|
+
return void 0;
|
|
186
|
+
}
|
|
94
187
|
}
|
|
95
|
-
client;
|
|
96
188
|
async incr(key) {
|
|
97
189
|
return this.client.incr(key);
|
|
98
190
|
}
|
|
@@ -100,14 +192,39 @@ var RedisStore = class {
|
|
|
100
192
|
await this.client.pexpire(key, ms);
|
|
101
193
|
}
|
|
102
194
|
async get(key) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
195
|
+
return toCount(await this.client.get(key));
|
|
196
|
+
}
|
|
197
|
+
async fixedWindowStep(key, ttlMs) {
|
|
198
|
+
const viaScript = await this.tryScript(FIXED_WINDOW_SCRIPT, [key], [ttlMs], toCount);
|
|
199
|
+
if (viaScript !== void 0) return viaScript;
|
|
200
|
+
const count = await this.incr(key);
|
|
201
|
+
if (count === 1) await this.pexpire(key, ttlMs);
|
|
202
|
+
return count;
|
|
203
|
+
}
|
|
204
|
+
async slidingWindowStep(currentKey, previousKey, ttlMs) {
|
|
205
|
+
const viaScript = await this.tryScript(
|
|
206
|
+
SLIDING_WINDOW_SCRIPT,
|
|
207
|
+
[currentKey, previousKey],
|
|
208
|
+
[ttlMs],
|
|
209
|
+
(raw) => {
|
|
210
|
+
const pair = raw;
|
|
211
|
+
return [toCount(pair?.[0]), toCount(pair?.[1])];
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
if (viaScript !== void 0) return viaScript;
|
|
215
|
+
const previous = await this.get(previousKey);
|
|
216
|
+
const current = await this.incr(currentKey);
|
|
217
|
+
if (current === 1) await this.pexpire(currentKey, ttlMs);
|
|
218
|
+
return [previous, current];
|
|
107
219
|
}
|
|
108
220
|
};
|
|
109
221
|
|
|
110
222
|
// src/stores/upstash-rest.ts
|
|
223
|
+
function toCount2(value) {
|
|
224
|
+
if (value == null || value === false) return 0;
|
|
225
|
+
const n = Number(value);
|
|
226
|
+
return Number.isFinite(n) ? n : 0;
|
|
227
|
+
}
|
|
111
228
|
var UpstashRestStore = class {
|
|
112
229
|
url;
|
|
113
230
|
token;
|
|
@@ -147,10 +264,18 @@ var UpstashRestStore = class {
|
|
|
147
264
|
await this.command(["PEXPIRE", key, ms]);
|
|
148
265
|
}
|
|
149
266
|
async get(key) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
267
|
+
return toCount2(await this.command(["GET", key]));
|
|
268
|
+
}
|
|
269
|
+
/** Run a Lua script through the REST API. Keys come first, then args. */
|
|
270
|
+
async evalScript(script, keys, args) {
|
|
271
|
+
return this.command(["EVAL", script, keys.length, ...keys, ...args]);
|
|
272
|
+
}
|
|
273
|
+
async fixedWindowStep(key, ttlMs) {
|
|
274
|
+
return toCount2(await this.evalScript(FIXED_WINDOW_SCRIPT, [key], [ttlMs]));
|
|
275
|
+
}
|
|
276
|
+
async slidingWindowStep(currentKey, previousKey, ttlMs) {
|
|
277
|
+
const raw = await this.evalScript(SLIDING_WINDOW_SCRIPT, [currentKey, previousKey], [ttlMs]);
|
|
278
|
+
return [toCount2(raw?.[0]), toCount2(raw?.[1])];
|
|
154
279
|
}
|
|
155
280
|
};
|
|
156
281
|
|
|
@@ -164,7 +289,12 @@ function resolveStore(options) {
|
|
|
164
289
|
return { store: options.store, distributed: true, source: "custom store" };
|
|
165
290
|
}
|
|
166
291
|
if (options.redis) {
|
|
167
|
-
|
|
292
|
+
const store = new RedisStore(options.redis, { evalStyle: options.evalStyle });
|
|
293
|
+
return {
|
|
294
|
+
store,
|
|
295
|
+
distributed: true,
|
|
296
|
+
source: store.atomic ? "redis client (atomic)" : "redis client"
|
|
297
|
+
};
|
|
168
298
|
}
|
|
169
299
|
const url = readEnv("UPSTASH_REDIS_REST_URL");
|
|
170
300
|
const token = readEnv("UPSTASH_REDIS_REST_TOKEN");
|
|
@@ -238,6 +368,6 @@ function rateLimit(options) {
|
|
|
238
368
|
};
|
|
239
369
|
}
|
|
240
370
|
|
|
241
|
-
export { MemoryStore, RedisStore, UpstashRestStore, parseWindow, rateLimit, resolveStore };
|
|
371
|
+
export { FIXED_WINDOW_SCRIPT, MemoryStore, RedisStore, SLIDING_WINDOW_SCRIPT, UpstashRestStore, detectEvalStyle, parseWindow, rateLimit, resolveStore };
|
|
242
372
|
//# sourceMappingURL=index.js.map
|
|
243
373
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/algorithms/fixed-window.ts","../src/algorithms/sliding-window.ts","../src/stores/memory.ts","../src/stores/redis.ts","../src/stores/upstash-rest.ts","../src/resolve-store.ts","../src/window.ts","../src/rate-limit.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { AlgorithmFn } from './index.js'\n\n/**\n * Fixed-window counter. Divides time into contiguous windows of `windowMs` and\n * counts requests per window. Simple and cheap (one counter per window), at the\n * cost of allowing up to 2x `limit` across a window boundary in the worst case.\n */\nexport const fixedWindow: AlgorithmFn = async (store, ctx, identifier, now) => {\n const { limit, windowMs, prefix } = ctx\n const windowStart = Math.floor(now / windowMs) * windowMs\n const key = `${prefix}:${identifier}:${windowStart}`\n\n const count = await store.incr(key)\n if (count === 1) await store.pexpire(key, windowMs)\n\n const reset = windowStart + windowMs\n const success = count <= limit\n return {\n success,\n limit,\n remaining: Math.max(0, limit - count),\n reset,\n retryAfter: success ? 0 : reset - now,\n }\n}\n","import type { AlgorithmFn } from './index.js'\n\n/**\n * Sliding-window counter. Weights the previous window's count by how far the\n * current window has elapsed and adds the current count, smoothing out the\n * boundary bursts that fixed-window allows. Uses two counters per identifier.\n */\nexport const slidingWindow: AlgorithmFn = async (store, ctx, identifier, now) => {\n const { limit, windowMs, prefix } = ctx\n const currentStart = Math.floor(now / windowMs) * windowMs\n const previousStart = currentStart - windowMs\n const fraction = (now - currentStart) / windowMs // 0..1 through the current window\n\n const currentKey = `${prefix}:${identifier}:${currentStart}`\n const previousKey = `${prefix}:${identifier}:${previousStart}`\n\n const previousCount = await store.get(previousKey)\n const currentCount = await store.incr(currentKey)\n // Keep the counter alive long enough to still be read as \"previous\" next window.\n if (currentCount === 1) await store.pexpire(currentKey, windowMs * 2)\n\n const weighted = previousCount * (1 - fraction) + currentCount\n const reset = currentStart + windowMs\n const success = weighted <= limit\n return {\n success,\n limit,\n remaining: Math.max(0, Math.floor(limit - weighted)),\n reset,\n retryAfter: success ? 0 : reset - now,\n }\n}\n","import type { Store } from '../types.js'\n\ninterface Entry {\n count: number\n expiresAt: number\n}\n\n/**\n * In-process counter store. Used as the automatic fallback when no Redis is\n * configured. State is per-instance and not shared across processes, so it is\n * meant for local development and single-instance deployments, not distributed\n * production traffic.\n */\nexport class MemoryStore implements Store {\n private readonly map = new Map<string, Entry>()\n private readonly sweeper: ReturnType<typeof setInterval> | undefined\n\n /** @param sweepMs How often to purge expired keys. Set to 0 to disable the timer (lazy expiry still applies). */\n constructor(sweepMs = 30_000) {\n if (sweepMs > 0 && typeof setInterval === 'function') {\n this.sweeper = setInterval(() => this.sweep(), sweepMs)\n // Never keep the event loop alive for cleanup alone.\n this.sweeper.unref?.()\n }\n }\n\n private live(key: string): Entry | undefined {\n const entry = this.map.get(key)\n if (!entry) return undefined\n if (entry.expiresAt <= Date.now()) {\n this.map.delete(key)\n return undefined\n }\n return entry\n }\n\n async incr(key: string): Promise<number> {\n const entry = this.live(key)\n if (!entry) {\n // Default to no expiry; pexpire is expected to follow on the first hit.\n this.map.set(key, { count: 1, expiresAt: Number.POSITIVE_INFINITY })\n return 1\n }\n entry.count += 1\n return entry.count\n }\n\n async pexpire(key: string, ms: number): Promise<void> {\n const entry = this.live(key)\n if (entry) entry.expiresAt = Date.now() + ms\n }\n\n async get(key: string): Promise<number> {\n return this.live(key)?.count ?? 0\n }\n\n private sweep(): void {\n const now = Date.now()\n for (const [key, entry] of this.map) {\n if (entry.expiresAt <= now) this.map.delete(key)\n }\n }\n\n /** Stop the background sweeper. Optional; the timer is unref'd. */\n dispose(): void {\n if (this.sweeper) clearInterval(this.sweeper)\n }\n}\n","import type { RedisLike, Store } from '../types.js'\n\n/**\n * Adapts any Redis client with `incr` / `pexpire` / `get` (both `@upstash/redis`\n * and `ioredis` qualify) to the {@link Store} interface.\n */\nexport class RedisStore implements Store {\n constructor(private readonly client: RedisLike) {}\n\n async incr(key: string): Promise<number> {\n return this.client.incr(key)\n }\n\n async pexpire(key: string, ms: number): Promise<void> {\n await this.client.pexpire(key, ms)\n }\n\n async get(key: string): Promise<number> {\n const value = await this.client.get(key)\n if (value == null) return 0\n const n = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(n) ? n : 0\n }\n}\n","import type { Store } from '../types.js'\n\nexport interface UpstashRestOptions {\n url: string\n token: string\n /** Custom fetch implementation. Defaults to the global `fetch`. */\n fetch?: typeof fetch\n}\n\n/**\n * Zero-dependency store backed by the Upstash Redis REST API. Uses `fetch`, so\n * it runs anywhere the runtime provides one (Node 18+, edge, workers) without\n * pulling in a Redis client. This is what env auto-detection constructs.\n */\nexport class UpstashRestStore implements Store {\n private readonly url: string\n private readonly token: string\n private readonly fetchImpl: typeof fetch\n\n constructor(options: UpstashRestOptions) {\n this.url = options.url.replace(/\\/$/, '')\n this.token = options.token\n const f = options.fetch ?? globalThis.fetch\n if (typeof f !== 'function') {\n throw new Error('ratefall: no global fetch available; pass a `fetch` implementation to UpstashRestStore.')\n }\n this.fetchImpl = f\n }\n\n private async command(args: (string | number)[]): Promise<unknown> {\n const res = await this.fetchImpl(this.url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(args.map(String)),\n })\n if (!res.ok) {\n const detail = await res.text().catch(() => '')\n throw new Error(`ratefall: Upstash REST error ${res.status} ${res.statusText}${detail ? ` - ${detail}` : ''}`)\n }\n const body = (await res.json()) as { result?: unknown; error?: string }\n if (body.error) {\n throw new Error(`ratefall: Upstash command failed - ${body.error}`)\n }\n return body.result\n }\n\n async incr(key: string): Promise<number> {\n return Number(await this.command(['INCR', key]))\n }\n\n async pexpire(key: string, ms: number): Promise<void> {\n await this.command(['PEXPIRE', key, ms])\n }\n\n async get(key: string): Promise<number> {\n const result = await this.command(['GET', key])\n if (result == null) return 0\n const n = Number(result)\n return Number.isFinite(n) ? n : 0\n }\n}\n","import type { RateLimitOptions, Store } from './types.js'\nimport { MemoryStore } from './stores/memory.js'\nimport { RedisStore } from './stores/redis.js'\nimport { UpstashRestStore } from './stores/upstash-rest.js'\n\nexport interface ResolvedStore {\n store: Store\n /** Whether the store is shared across processes (Redis) vs per-instance (memory). */\n distributed: boolean\n /** Human-readable source, for the dev warning. */\n source: string\n}\n\nfunction readEnv(key: string): string | undefined {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env\n return env?.[key]\n}\n\n/**\n * Decide which store to use, in priority order:\n * 1. an explicit `store`\n * 2. an explicit `redis` client\n * 3. `UPSTASH_REDIS_REST_URL` + `UPSTASH_REDIS_REST_TOKEN` in the environment\n * 4. in-memory fallback\n */\nexport function resolveStore(options: RateLimitOptions): ResolvedStore {\n if (options.store) {\n return { store: options.store, distributed: true, source: 'custom store' }\n }\n\n if (options.redis) {\n return { store: new RedisStore(options.redis), distributed: true, source: 'redis client' }\n }\n\n const url = readEnv('UPSTASH_REDIS_REST_URL')\n const token = readEnv('UPSTASH_REDIS_REST_TOKEN')\n if (url && token) {\n return { store: new UpstashRestStore({ url, token }), distributed: true, source: 'Upstash REST (env)' }\n }\n\n return { store: new MemoryStore(), distributed: false, source: 'in-memory' }\n}\n\n/** True when the runtime looks like a production deployment. */\nexport function isProduction(): boolean {\n return readEnv('NODE_ENV') === 'production'\n}\n","const UNITS: Record<string, number> = {\n ms: 1,\n s: 1000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n}\n\nconst PATTERN = /^(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)$/\n\n/**\n * Normalize a window into milliseconds. Accepts a raw millisecond number or a\n * duration string like `'500ms'`, `'10s'`, `'1m'`, `'2h'`, `'1d'`.\n */\nexport function parseWindow(window: string | number): number {\n if (typeof window === 'number') {\n if (!Number.isFinite(window) || window <= 0) {\n throw new RangeError(`ratefall: window must be a positive number of milliseconds, got ${window}`)\n }\n return Math.floor(window)\n }\n\n const match = PATTERN.exec(window.trim())\n if (!match) {\n throw new TypeError(`ratefall: invalid window \"${window}\". Use a number of ms or a string like \"10s\", \"1m\", \"2h\".`)\n }\n\n const value = Number(match[1])\n const unit = UNITS[match[2] as string] as number\n const ms = Math.floor(value * unit)\n if (ms <= 0) {\n throw new RangeError(`ratefall: window must be greater than 0, got \"${window}\".`)\n }\n return ms\n}\n","import type { AlgorithmContext, AlgorithmFn } from './algorithms/index.js'\nimport { fixedWindow } from './algorithms/fixed-window.js'\nimport { slidingWindow } from './algorithms/sliding-window.js'\nimport { isProduction, resolveStore } from './resolve-store.js'\nimport type { RateLimiter, RateLimitOptions } from './types.js'\nimport { parseWindow } from './window.js'\n\nconst ALGORITHMS: Record<string, AlgorithmFn> = {\n 'fixed-window': fixedWindow,\n 'sliding-window': slidingWindow,\n}\n\n/**\n * Create a rate limiter. With no store configured it uses an in-memory counter\n * (great for local dev); set `UPSTASH_REDIS_REST_URL` + `UPSTASH_REDIS_REST_TOKEN`,\n * or pass a `redis` client, and the exact same code becomes distributed.\n *\n * @example\n * const limiter = rateLimit({ limit: 10, window: '10s' })\n * const { success } = await limiter.limit(userId)\n * if (!success) return new Response('slow down', { status: 429 })\n */\nexport function rateLimit(options: RateLimitOptions): RateLimiter {\n const { limit, algorithm = 'fixed-window', prefix = 'ratefall', silent = false } = options\n\n if (!Number.isInteger(limit) || limit <= 0) {\n throw new RangeError(`ratefall: limit must be a positive integer, got ${limit}`)\n }\n\n const run = ALGORITHMS[algorithm]\n if (!run) {\n throw new TypeError(`ratefall: unknown algorithm \"${algorithm}\". Use \"fixed-window\" or \"sliding-window\".`)\n }\n\n const windowMs = parseWindow(options.window)\n const { store, distributed, source } = resolveStore(options)\n\n if (!distributed && !silent && isProduction()) {\n // Not an error: single-instance prod is legitimate. But un-shared limits in a\n // multi-instance deploy are a footgun, so make it loud exactly once.\n console.warn(\n `ratefall: running with an in-memory store in production. Limits are per-instance and NOT shared. ` +\n `Set UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN or pass a \\`redis\\` client. Pass \\`silent: true\\` to hide this.`,\n )\n }\n\n const ctx: AlgorithmContext = { limit, windowMs, prefix }\n\n return {\n async limit(identifier: string) {\n if (!identifier) {\n throw new TypeError('ratefall: identifier must be a non-empty string.')\n }\n return run(store, ctx, identifier, Date.now())\n },\n }\n}\n\n/** The resolved store source, exposed for debugging/tests. */\nexport { resolveStore } from './resolve-store.js'\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/algorithms/fixed-window.ts","../src/algorithms/sliding-window.ts","../src/stores/memory.ts","../src/lua.ts","../src/stores/redis.ts","../src/stores/upstash-rest.ts","../src/resolve-store.ts","../src/window.ts","../src/rate-limit.ts"],"names":["step","toCount"],"mappings":"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","file":"index.js","sourcesContent":["import type { AlgorithmFn } from './index.js'\nimport type { Store } from '../types.js'\n\n/**\n * Increment the window counter, preferring the store's atomic single-call path\n * and falling back to `incr` + a conditional `pexpire` for stores that only\n * implement the three required methods.\n */\nasync function step(store: Store, key: string, ttlMs: number): Promise<number> {\n if (store.fixedWindowStep) return store.fixedWindowStep(key, ttlMs)\n\n const count = await store.incr(key)\n if (count === 1) await store.pexpire(key, ttlMs)\n return count\n}\n\n/**\n * Fixed-window counter. Divides time into contiguous windows of `windowMs` and\n * counts requests per window. Simple and cheap (one counter per window), at the\n * cost of allowing up to 2x `limit` across a window boundary in the worst case.\n */\nexport const fixedWindow: AlgorithmFn = async (store, ctx, identifier, now) => {\n const { limit, windowMs, prefix } = ctx\n const windowStart = Math.floor(now / windowMs) * windowMs\n const key = `${prefix}:${identifier}:${windowStart}`\n\n const count = await step(store, key, windowMs)\n\n const reset = windowStart + windowMs\n const success = count <= limit\n return {\n success,\n limit,\n remaining: Math.max(0, limit - count),\n reset,\n retryAfter: success ? 0 : reset - now,\n }\n}\n","import type { AlgorithmFn } from './index.js'\nimport type { Store } from '../types.js'\n\n/**\n * Read the previous window's counter and increment the current one, preferring\n * the store's atomic single-call path.\n *\n * The fallback runs the read and the increment as two separate commands. That\n * costs an extra round trip, and it means the two observations are taken at\n * slightly different moments, so a request landing exactly on a window boundary\n * can weight a previous count that moved between the `GET` and the `INCR`. It\n * does not overshoot the limit under ordinary concurrency: the previous window\n * is complete and therefore stable, and `INCR` is atomic.\n */\nasync function step(\n store: Store,\n currentKey: string,\n previousKey: string,\n ttlMs: number,\n): Promise<[number, number]> {\n if (store.slidingWindowStep) return store.slidingWindowStep(currentKey, previousKey, ttlMs)\n\n const previous = await store.get(previousKey)\n const current = await store.incr(currentKey)\n if (current === 1) await store.pexpire(currentKey, ttlMs)\n return [previous, current]\n}\n\n/**\n * Sliding-window counter. Weights the previous window's count by how far the\n * current window has elapsed and adds the current count, smoothing out the\n * boundary bursts that fixed-window allows. Uses two counters per identifier.\n */\nexport const slidingWindow: AlgorithmFn = async (store, ctx, identifier, now) => {\n const { limit, windowMs, prefix } = ctx\n const currentStart = Math.floor(now / windowMs) * windowMs\n const previousStart = currentStart - windowMs\n const fraction = (now - currentStart) / windowMs // 0..1 through the current window\n\n const currentKey = `${prefix}:${identifier}:${currentStart}`\n const previousKey = `${prefix}:${identifier}:${previousStart}`\n\n // Keep the current counter alive long enough to still be read as \"previous\"\n // during the next window.\n const [previousCount, currentCount] = await step(store, currentKey, previousKey, windowMs * 2)\n\n const weighted = previousCount * (1 - fraction) + currentCount\n const reset = currentStart + windowMs\n const success = weighted <= limit\n return {\n success,\n limit,\n remaining: Math.max(0, Math.floor(limit - weighted)),\n reset,\n retryAfter: success ? 0 : reset - now,\n }\n}\n","import type { Store } from '../types.js'\n\ninterface Entry {\n count: number\n expiresAt: number\n}\n\n/**\n * In-process counter store. Used as the automatic fallback when no Redis is\n * configured. State is per-instance and not shared across processes, so it is\n * meant for local development and single-instance deployments, not distributed\n * production traffic.\n */\nexport class MemoryStore implements Store {\n private readonly map = new Map<string, Entry>()\n private readonly sweeper: ReturnType<typeof setInterval> | undefined\n\n /** @param sweepMs How often to purge expired keys. Set to 0 to disable the timer (lazy expiry still applies). */\n constructor(sweepMs = 30_000) {\n if (sweepMs > 0 && typeof setInterval === 'function') {\n this.sweeper = setInterval(() => this.sweep(), sweepMs)\n // Never keep the event loop alive for cleanup alone.\n this.sweeper.unref?.()\n }\n }\n\n private live(key: string): Entry | undefined {\n const entry = this.map.get(key)\n if (!entry) return undefined\n if (entry.expiresAt <= Date.now()) {\n this.map.delete(key)\n return undefined\n }\n return entry\n }\n\n async incr(key: string): Promise<number> {\n const entry = this.live(key)\n if (!entry) {\n // Default to no expiry; pexpire is expected to follow on the first hit.\n this.map.set(key, { count: 1, expiresAt: Number.POSITIVE_INFINITY })\n return 1\n }\n entry.count += 1\n return entry.count\n }\n\n async pexpire(key: string, ms: number): Promise<void> {\n const entry = this.live(key)\n if (entry) entry.expiresAt = Date.now() + ms\n }\n\n async get(key: string): Promise<number> {\n return this.live(key)?.count ?? 0\n }\n\n /**\n * Atomic fixed-window step. JavaScript is single threaded and none of this\n * awaits, so the increment and the TTL land together with no interleaving.\n */\n async fixedWindowStep(key: string, ttlMs: number): Promise<number> {\n const entry = this.live(key)\n if (!entry) {\n this.map.set(key, { count: 1, expiresAt: Date.now() + ttlMs })\n return 1\n }\n entry.count += 1\n return entry.count\n }\n\n /** Atomic sliding-window step. Same reasoning as {@link fixedWindowStep}. */\n async slidingWindowStep(\n currentKey: string,\n previousKey: string,\n ttlMs: number,\n ): Promise<[number, number]> {\n const previous = this.live(previousKey)?.count ?? 0\n const current = await this.fixedWindowStep(currentKey, ttlMs)\n return [previous, current]\n }\n\n private sweep(): void {\n const now = Date.now()\n for (const [key, entry] of this.map) {\n if (entry.expiresAt <= now) this.map.delete(key)\n }\n }\n\n /** Stop the background sweeper. Optional; the timer is unref'd. */\n dispose(): void {\n if (this.sweeper) clearInterval(this.sweeper)\n }\n}\n","/**\n * Lua scripts for the single-round-trip Redis path.\n *\n * The fallback path (`INCR` then a conditional `PEXPIRE`) costs two round trips\n * on the first hit of every window, and is not atomic: a process that dies\n * between the two commands leaves a key with no TTL, which then leaks in Redis\n * forever. Verified against Redis 7.4: `INCR` on a fresh key reports `TTL -1`\n * until the `PEXPIRE` lands.\n *\n * Running the same work inside `EVAL` fixes both: one round trip, and a TTL\n * that cannot be orphaned.\n *\n * Note on what this does NOT fix. It is tempting to claim the fallback races\n * under concurrency. It does not, at least not in a way that overshoots the\n * limit: the previous window's counter is a completed window that nothing\n * increments any more, so every concurrent caller reads the same stable value,\n * and `INCR` on the current key is atomic, so each caller gets a distinct\n * count. Measured over 50 concurrent callers against a real Redis, both paths\n * allow exactly the same number through.\n */\n\n/**\n * Fixed window. Increments the counter and sets the TTL on first touch.\n *\n * KEYS[1] window counter\n * ARGV[1] window length in milliseconds\n * returns the new count\n */\nexport const FIXED_WINDOW_SCRIPT = `\nlocal count = redis.call('INCR', KEYS[1])\nif count == 1 then\n redis.call('PEXPIRE', KEYS[1], ARGV[1])\nend\nreturn count\n`.trim()\n\n/**\n * Sliding window. Reads the previous window's counter and increments the\n * current one as a single atomic unit.\n *\n * KEYS[1] current window counter\n * KEYS[2] previous window counter\n * ARGV[1] retention in milliseconds (two windows, so the current counter is\n * still readable as \"previous\" during the next window)\n * returns { previousCount, currentCount }\n */\nexport const SLIDING_WINDOW_SCRIPT = `\nlocal previous = redis.call('GET', KEYS[2])\nlocal previousCount = 0\nif previous then\n previousCount = math.floor(tonumber(previous) or 0)\nend\nlocal current = redis.call('INCR', KEYS[1])\nif current == 1 then\n redis.call('PEXPIRE', KEYS[1], ARGV[1])\nend\nreturn { previousCount, current }\n`.trim()\n","import { FIXED_WINDOW_SCRIPT, SLIDING_WINDOW_SCRIPT } from '../lua.js'\nimport type { EvalStyle, RedisLike, Store } from '../types.js'\n\nexport interface RedisStoreOptions {\n /** How to call the client's `eval`. Defaults to `'auto'`. */\n evalStyle?: EvalStyle\n}\n\n/** Coerce whatever a client returns for a counter into a non-negative number. */\nfunction toCount(value: unknown): number {\n if (value == null || value === false) return 0\n const n = typeof value === 'number' ? value : Number(value)\n return Number.isFinite(n) ? n : 0\n}\n\n/**\n * Decide which `eval` calling convention a client uses.\n *\n * `ioredis` is the odd one out: it takes a key count followed by flat keys and\n * args, and it is the only one of the two that exposes `defineCommand`. Every\n * other client we support (`@upstash/redis`) takes two arrays.\n */\nexport function detectEvalStyle(client: RedisLike): Exclude<EvalStyle, 'auto'> {\n if (typeof client.eval !== 'function') return 'none'\n if (typeof (client as { defineCommand?: unknown }).defineCommand === 'function') return 'ioredis'\n return 'upstash'\n}\n\n/**\n * Adapts any Redis client with `incr` / `pexpire` / `get` (both `@upstash/redis`\n * and `ioredis` qualify) to the {@link Store} interface.\n *\n * When the client also exposes `eval`, checks run as a single atomic Lua call\n * instead of two or three separate commands. If a server rejects scripting the\n * store degrades to the portable path once and stays there.\n */\nexport class RedisStore implements Store {\n private readonly client: RedisLike\n private style: Exclude<EvalStyle, 'auto'>\n\n constructor(client: RedisLike, options: RedisStoreOptions = {}) {\n this.client = client\n const requested = options.evalStyle ?? 'auto'\n this.style = requested === 'auto' ? detectEvalStyle(client) : requested\n if (this.style !== 'none' && typeof client.eval !== 'function') {\n // An explicit style cannot conjure a method that is not there.\n this.style = 'none'\n }\n }\n\n /** True when this store will take the atomic Lua path. */\n get atomic(): boolean {\n return this.style !== 'none'\n }\n\n private async runScript(script: string, keys: string[], args: (string | number)[]): Promise<unknown> {\n const evalFn = this.client.eval as (...a: unknown[]) => Promise<unknown>\n if (this.style === 'ioredis') {\n return evalFn.call(this.client, script, keys.length, ...keys, ...args)\n }\n return evalFn.call(this.client, script, keys, args)\n }\n\n /**\n * Run `script`, falling back to the portable path forever if the server or\n * client cannot handle scripting. A single failure is enough to decide: a\n * server either supports EVAL or it does not, so retrying every request would\n * just double the cost of every check on that deployment.\n */\n private async tryScript<T>(\n script: string,\n keys: string[],\n args: (string | number)[],\n parse: (raw: unknown) => T,\n ): Promise<T | undefined> {\n if (this.style === 'none') return undefined\n try {\n return parse(await this.runScript(script, keys, args))\n } catch {\n this.style = 'none'\n return undefined\n }\n }\n\n async incr(key: string): Promise<number> {\n return this.client.incr(key)\n }\n\n async pexpire(key: string, ms: number): Promise<void> {\n await this.client.pexpire(key, ms)\n }\n\n async get(key: string): Promise<number> {\n return toCount(await this.client.get(key))\n }\n\n async fixedWindowStep(key: string, ttlMs: number): Promise<number> {\n const viaScript = await this.tryScript(FIXED_WINDOW_SCRIPT, [key], [ttlMs], toCount)\n if (viaScript !== undefined) return viaScript\n\n const count = await this.incr(key)\n if (count === 1) await this.pexpire(key, ttlMs)\n return count\n }\n\n async slidingWindowStep(\n currentKey: string,\n previousKey: string,\n ttlMs: number,\n ): Promise<[number, number]> {\n const viaScript = await this.tryScript(\n SLIDING_WINDOW_SCRIPT,\n [currentKey, previousKey],\n [ttlMs],\n (raw): [number, number] => {\n const pair = raw as unknown[]\n return [toCount(pair?.[0]), toCount(pair?.[1])]\n },\n )\n if (viaScript !== undefined) return viaScript\n\n const previous = await this.get(previousKey)\n const current = await this.incr(currentKey)\n if (current === 1) await this.pexpire(currentKey, ttlMs)\n return [previous, current]\n }\n}\n","import { FIXED_WINDOW_SCRIPT, SLIDING_WINDOW_SCRIPT } from '../lua.js'\nimport type { Store } from '../types.js'\n\nexport interface UpstashRestOptions {\n url: string\n token: string\n /** Custom fetch implementation. Defaults to the global `fetch`. */\n fetch?: typeof fetch\n}\n\n/** Coerce a REST result into a non-negative counter value. */\nfunction toCount(value: unknown): number {\n if (value == null || value === false) return 0\n const n = Number(value)\n return Number.isFinite(n) ? n : 0\n}\n\n/**\n * Zero-dependency store backed by the Upstash Redis REST API. Uses `fetch`, so\n * it runs anywhere the runtime provides one (Node 18+, edge, workers) without\n * pulling in a Redis client. This is what env auto-detection constructs.\n */\nexport class UpstashRestStore implements Store {\n private readonly url: string\n private readonly token: string\n private readonly fetchImpl: typeof fetch\n\n constructor(options: UpstashRestOptions) {\n this.url = options.url.replace(/\\/$/, '')\n this.token = options.token\n const f = options.fetch ?? globalThis.fetch\n if (typeof f !== 'function') {\n throw new Error('ratefall: no global fetch available; pass a `fetch` implementation to UpstashRestStore.')\n }\n this.fetchImpl = f\n }\n\n private async command(args: (string | number)[]): Promise<unknown> {\n const res = await this.fetchImpl(this.url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(args.map(String)),\n })\n if (!res.ok) {\n const detail = await res.text().catch(() => '')\n throw new Error(`ratefall: Upstash REST error ${res.status} ${res.statusText}${detail ? ` - ${detail}` : ''}`)\n }\n const body = (await res.json()) as { result?: unknown; error?: string }\n if (body.error) {\n throw new Error(`ratefall: Upstash command failed - ${body.error}`)\n }\n return body.result\n }\n\n async incr(key: string): Promise<number> {\n return Number(await this.command(['INCR', key]))\n }\n\n async pexpire(key: string, ms: number): Promise<void> {\n await this.command(['PEXPIRE', key, ms])\n }\n\n async get(key: string): Promise<number> {\n return toCount(await this.command(['GET', key]))\n }\n\n /** Run a Lua script through the REST API. Keys come first, then args. */\n private async evalScript(\n script: string,\n keys: string[],\n args: (string | number)[],\n ): Promise<unknown> {\n return this.command(['EVAL', script, keys.length, ...keys, ...args])\n }\n\n async fixedWindowStep(key: string, ttlMs: number): Promise<number> {\n return toCount(await this.evalScript(FIXED_WINDOW_SCRIPT, [key], [ttlMs]))\n }\n\n async slidingWindowStep(\n currentKey: string,\n previousKey: string,\n ttlMs: number,\n ): Promise<[number, number]> {\n const raw = (await this.evalScript(SLIDING_WINDOW_SCRIPT, [currentKey, previousKey], [ttlMs])) as unknown[]\n return [toCount(raw?.[0]), toCount(raw?.[1])]\n }\n}\n","import type { RateLimitOptions, Store } from './types.js'\nimport { MemoryStore } from './stores/memory.js'\nimport { RedisStore } from './stores/redis.js'\nimport { UpstashRestStore } from './stores/upstash-rest.js'\n\nexport interface ResolvedStore {\n store: Store\n /** Whether the store is shared across processes (Redis) vs per-instance (memory). */\n distributed: boolean\n /** Human-readable source, for the dev warning. */\n source: string\n}\n\nfunction readEnv(key: string): string | undefined {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env\n return env?.[key]\n}\n\n/**\n * Decide which store to use, in priority order:\n * 1. an explicit `store`\n * 2. an explicit `redis` client\n * 3. `UPSTASH_REDIS_REST_URL` + `UPSTASH_REDIS_REST_TOKEN` in the environment\n * 4. in-memory fallback\n */\nexport function resolveStore(options: RateLimitOptions): ResolvedStore {\n if (options.store) {\n return { store: options.store, distributed: true, source: 'custom store' }\n }\n\n if (options.redis) {\n const store = new RedisStore(options.redis, { evalStyle: options.evalStyle })\n return {\n store,\n distributed: true,\n source: store.atomic ? 'redis client (atomic)' : 'redis client',\n }\n }\n\n const url = readEnv('UPSTASH_REDIS_REST_URL')\n const token = readEnv('UPSTASH_REDIS_REST_TOKEN')\n if (url && token) {\n return { store: new UpstashRestStore({ url, token }), distributed: true, source: 'Upstash REST (env)' }\n }\n\n return { store: new MemoryStore(), distributed: false, source: 'in-memory' }\n}\n\n/** True when the runtime looks like a production deployment. */\nexport function isProduction(): boolean {\n return readEnv('NODE_ENV') === 'production'\n}\n","const UNITS: Record<string, number> = {\n ms: 1,\n s: 1000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n}\n\nconst PATTERN = /^(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)$/\n\n/**\n * Normalize a window into milliseconds. Accepts a raw millisecond number or a\n * duration string like `'500ms'`, `'10s'`, `'1m'`, `'2h'`, `'1d'`.\n */\nexport function parseWindow(window: string | number): number {\n if (typeof window === 'number') {\n if (!Number.isFinite(window) || window <= 0) {\n throw new RangeError(`ratefall: window must be a positive number of milliseconds, got ${window}`)\n }\n return Math.floor(window)\n }\n\n const match = PATTERN.exec(window.trim())\n if (!match) {\n throw new TypeError(`ratefall: invalid window \"${window}\". Use a number of ms or a string like \"10s\", \"1m\", \"2h\".`)\n }\n\n const value = Number(match[1])\n const unit = UNITS[match[2] as string] as number\n const ms = Math.floor(value * unit)\n if (ms <= 0) {\n throw new RangeError(`ratefall: window must be greater than 0, got \"${window}\".`)\n }\n return ms\n}\n","import type { AlgorithmContext, AlgorithmFn } from './algorithms/index.js'\nimport { fixedWindow } from './algorithms/fixed-window.js'\nimport { slidingWindow } from './algorithms/sliding-window.js'\nimport { isProduction, resolveStore } from './resolve-store.js'\nimport type { RateLimiter, RateLimitOptions } from './types.js'\nimport { parseWindow } from './window.js'\n\nconst ALGORITHMS: Record<string, AlgorithmFn> = {\n 'fixed-window': fixedWindow,\n 'sliding-window': slidingWindow,\n}\n\n/**\n * Create a rate limiter. With no store configured it uses an in-memory counter\n * (great for local dev); set `UPSTASH_REDIS_REST_URL` + `UPSTASH_REDIS_REST_TOKEN`,\n * or pass a `redis` client, and the exact same code becomes distributed.\n *\n * @example\n * const limiter = rateLimit({ limit: 10, window: '10s' })\n * const { success } = await limiter.limit(userId)\n * if (!success) return new Response('slow down', { status: 429 })\n */\nexport function rateLimit(options: RateLimitOptions): RateLimiter {\n const { limit, algorithm = 'fixed-window', prefix = 'ratefall', silent = false } = options\n\n if (!Number.isInteger(limit) || limit <= 0) {\n throw new RangeError(`ratefall: limit must be a positive integer, got ${limit}`)\n }\n\n const run = ALGORITHMS[algorithm]\n if (!run) {\n throw new TypeError(`ratefall: unknown algorithm \"${algorithm}\". Use \"fixed-window\" or \"sliding-window\".`)\n }\n\n const windowMs = parseWindow(options.window)\n const { store, distributed, source } = resolveStore(options)\n\n if (!distributed && !silent && isProduction()) {\n // Not an error: single-instance prod is legitimate. But un-shared limits in a\n // multi-instance deploy are a footgun, so make it loud exactly once.\n console.warn(\n `ratefall: running with an in-memory store in production. Limits are per-instance and NOT shared. ` +\n `Set UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN or pass a \\`redis\\` client. Pass \\`silent: true\\` to hide this.`,\n )\n }\n\n const ctx: AlgorithmContext = { limit, windowMs, prefix }\n\n return {\n async limit(identifier: string) {\n if (!identifier) {\n throw new TypeError('ratefall: identifier must be a non-empty string.')\n }\n return run(store, ctx, identifier, Date.now())\n },\n }\n}\n\n/** The resolved store source, exposed for debugging/tests. */\nexport { resolveStore } from './resolve-store.js'\n"]}
|
package/package.json
CHANGED