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 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 use `incr` plus a conditional `pexpire`. A Lua-based single-round-trip path is on the roadmap.
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.incr(key);
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.get(previousKey);
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
- constructor(client) {
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
- const value = await this.client.get(key);
106
- if (value == null) return 0;
107
- const n = typeof value === "number" ? value : Number(value);
108
- return Number.isFinite(n) ? n : 0;
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
- const result = await this.command(["GET", key]);
153
- if (result == null) return 0;
154
- const n = Number(result);
155
- return Number.isFinite(n) ? n : 0;
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
- return { store: new RedisStore(options.redis), distributed: true, source: "redis client" };
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;
@@ -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
- constructor(client: RedisLike);
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
- export { type Algorithm, MemoryStore, type RateLimitOptions, type RateLimitResult, type RateLimiter, type RedisLike, RedisStore, type Store, type UpstashRestOptions, UpstashRestStore, parseWindow, rateLimit, resolveStore };
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
- constructor(client: RedisLike);
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
- export { type Algorithm, MemoryStore, type RateLimitOptions, type RateLimitResult, type RateLimiter, type RedisLike, RedisStore, type Store, type UpstashRestOptions, UpstashRestStore, parseWindow, rateLimit, resolveStore };
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.incr(key);
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.get(previousKey);
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
- constructor(client) {
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
- const value = await this.client.get(key);
104
- if (value == null) return 0;
105
- const n = typeof value === "number" ? value : Number(value);
106
- return Number.isFinite(n) ? n : 0;
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
- const result = await this.command(["GET", key]);
151
- if (result == null) return 0;
152
- const n = Number(result);
153
- return Number.isFinite(n) ? n : 0;
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
- return { store: new RedisStore(options.redis), distributed: true, source: "redis client" };
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":";AAQA,eAAe,IAAA,CAAK,KAAA,EAAc,GAAA,EAAa,KAAA,EAAgC;AAC7E,EAAA,IAAI,MAAM,eAAA,EAAiB,OAAO,KAAA,CAAM,eAAA,CAAgB,KAAK,KAAK,CAAA;AAElE,EAAA,MAAM,KAAA,GAAQ,MAAM,KAAA,CAAM,IAAA,CAAK,GAAG,CAAA;AAClC,EAAA,IAAI,UAAU,CAAA,EAAG,MAAM,KAAA,CAAM,OAAA,CAAQ,KAAK,KAAK,CAAA;AAC/C,EAAA,OAAO,KAAA;AACT;AAOO,IAAM,WAAA,GAA2B,OAAO,KAAA,EAAO,GAAA,EAAK,YAAY,GAAA,KAAQ;AAC7E,EAAA,MAAM,EAAE,KAAA,EAAO,QAAA,EAAU,MAAA,EAAO,GAAI,GAAA;AACpC,EAAA,MAAM,WAAA,GAAc,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,QAAQ,CAAA,GAAI,QAAA;AACjD,EAAA,MAAM,MAAM,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,UAAU,IAAI,WAAW,CAAA,CAAA;AAElD,EAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,KAAA,EAAO,KAAK,QAAQ,CAAA;AAE7C,EAAA,MAAM,QAAQ,WAAA,GAAc,QAAA;AAC5B,EAAA,MAAM,UAAU,KAAA,IAAS,KAAA;AACzB,EAAA,OAAO;AAAA,IACL,OAAA;AAAA,IACA,KAAA;AAAA,IACA,SAAA,EAAW,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,QAAQ,KAAK,CAAA;AAAA,IACpC,KAAA;AAAA,IACA,UAAA,EAAY,OAAA,GAAU,CAAA,GAAI,KAAA,GAAQ;AAAA,GACpC;AACF,CAAA;;;ACvBA,eAAeA,KAAAA,CACb,KAAA,EACA,UAAA,EACA,WAAA,EACA,KAAA,EAC2B;AAC3B,EAAA,IAAI,MAAM,iBAAA,EAAmB,OAAO,MAAM,iBAAA,CAAkB,UAAA,EAAY,aAAa,KAAK,CAAA;AAE1F,EAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,CAAI,WAAW,CAAA;AAC5C,EAAA,MAAM,OAAA,GAAU,MAAM,KAAA,CAAM,IAAA,CAAK,UAAU,CAAA;AAC3C,EAAA,IAAI,YAAY,CAAA,EAAG,MAAM,KAAA,CAAM,OAAA,CAAQ,YAAY,KAAK,CAAA;AACxD,EAAA,OAAO,CAAC,UAAU,OAAO,CAAA;AAC3B;AAOO,IAAM,aAAA,GAA6B,OAAO,KAAA,EAAO,GAAA,EAAK,YAAY,GAAA,KAAQ;AAC/E,EAAA,MAAM,EAAE,KAAA,EAAO,QAAA,EAAU,MAAA,EAAO,GAAI,GAAA;AACpC,EAAA,MAAM,YAAA,GAAe,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,QAAQ,CAAA,GAAI,QAAA;AAClD,EAAA,MAAM,gBAAgB,YAAA,GAAe,QAAA;AACrC,EAAA,MAAM,QAAA,GAAA,CAAY,MAAM,YAAA,IAAgB,QAAA;AAExC,EAAA,MAAM,aAAa,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,UAAU,IAAI,YAAY,CAAA,CAAA;AAC1D,EAAA,MAAM,cAAc,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,UAAU,IAAI,aAAa,CAAA,CAAA;AAI5D,EAAA,MAAM,CAAC,aAAA,EAAe,YAAY,CAAA,GAAI,MAAMA,MAAK,KAAA,EAAO,UAAA,EAAY,WAAA,EAAa,QAAA,GAAW,CAAC,CAAA;AAE7F,EAAA,MAAM,QAAA,GAAW,aAAA,IAAiB,CAAA,GAAI,QAAA,CAAA,GAAY,YAAA;AAClD,EAAA,MAAM,QAAQ,YAAA,GAAe,QAAA;AAC7B,EAAA,MAAM,UAAU,QAAA,IAAY,KAAA;AAC5B,EAAA,OAAO;AAAA,IACL,OAAA;AAAA,IACA,KAAA;AAAA,IACA,SAAA,EAAW,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,KAAA,CAAM,KAAA,GAAQ,QAAQ,CAAC,CAAA;AAAA,IACnD,KAAA;AAAA,IACA,UAAA,EAAY,OAAA,GAAU,CAAA,GAAI,KAAA,GAAQ;AAAA,GACpC;AACF,CAAA;;;AC3CO,IAAM,cAAN,MAAmC;AAAA,EACvB,GAAA,uBAAU,GAAA,EAAmB;AAAA,EAC7B,OAAA;AAAA;AAAA,EAGjB,WAAA,CAAY,UAAU,GAAA,EAAQ;AAC5B,IAAA,IAAI,OAAA,GAAU,CAAA,IAAK,OAAO,WAAA,KAAgB,UAAA,EAAY;AACpD,MAAA,IAAA,CAAK,UAAU,WAAA,CAAY,MAAM,IAAA,CAAK,KAAA,IAAS,OAAO,CAAA;AAEtD,MAAA,IAAA,CAAK,QAAQ,KAAA,IAAQ;AAAA,IACvB;AAAA,EACF;AAAA,EAEQ,KAAK,GAAA,EAAgC;AAC3C,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA;AAC9B,IAAA,IAAI,CAAC,OAAO,OAAO,MAAA;AACnB,IAAA,IAAI,KAAA,CAAM,SAAA,IAAa,IAAA,CAAK,GAAA,EAAI,EAAG;AACjC,MAAA,IAAA,CAAK,GAAA,CAAI,OAAO,GAAG,CAAA;AACnB,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,OAAO,KAAA;AAAA,EACT;AAAA,EAEA,MAAM,KAAK,GAAA,EAA8B;AACvC,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,IAAA,CAAK,GAAG,CAAA;AAC3B,IAAA,IAAI,CAAC,KAAA,EAAO;AAEV,MAAA,IAAA,CAAK,GAAA,CAAI,IAAI,GAAA,EAAK,EAAE,OAAO,CAAA,EAAG,SAAA,EAAW,MAAA,CAAO,iBAAA,EAAmB,CAAA;AACnE,MAAA,OAAO,CAAA;AAAA,IACT;AACA,IAAA,KAAA,CAAM,KAAA,IAAS,CAAA;AACf,IAAA,OAAO,KAAA,CAAM,KAAA;AAAA,EACf;AAAA,EAEA,MAAM,OAAA,CAAQ,GAAA,EAAa,EAAA,EAA2B;AACpD,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,IAAA,CAAK,GAAG,CAAA;AAC3B,IAAA,IAAI,KAAA,EAAO,KAAA,CAAM,SAAA,GAAY,IAAA,CAAK,KAAI,GAAI,EAAA;AAAA,EAC5C;AAAA,EAEA,MAAM,IAAI,GAAA,EAA8B;AACtC,IAAA,OAAO,IAAA,CAAK,IAAA,CAAK,GAAG,CAAA,EAAG,KAAA,IAAS,CAAA;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,eAAA,CAAgB,GAAA,EAAa,KAAA,EAAgC;AACjE,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,IAAA,CAAK,GAAG,CAAA;AAC3B,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,GAAA,EAAK,EAAE,KAAA,EAAO,CAAA,EAAG,SAAA,EAAW,IAAA,CAAK,GAAA,EAAI,GAAI,KAAA,EAAO,CAAA;AAC7D,MAAA,OAAO,CAAA;AAAA,IACT;AACA,IAAA,KAAA,CAAM,KAAA,IAAS,CAAA;AACf,IAAA,OAAO,KAAA,CAAM,KAAA;AAAA,EACf;AAAA;AAAA,EAGA,MAAM,iBAAA,CACJ,UAAA,EACA,WAAA,EACA,KAAA,EAC2B;AAC3B,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,IAAA,CAAK,WAAW,GAAG,KAAA,IAAS,CAAA;AAClD,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,eAAA,CAAgB,YAAY,KAAK,CAAA;AAC5D,IAAA,OAAO,CAAC,UAAU,OAAO,CAAA;AAAA,EAC3B;AAAA,EAEQ,KAAA,GAAc;AACpB,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,IAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,CAAA,IAAK,KAAK,GAAA,EAAK;AACnC,MAAA,IAAI,MAAM,SAAA,IAAa,GAAA,EAAK,IAAA,CAAK,GAAA,CAAI,OAAO,GAAG,CAAA;AAAA,IACjD;AAAA,EACF;AAAA;AAAA,EAGA,OAAA,GAAgB;AACd,IAAA,IAAI,IAAA,CAAK,OAAA,EAAS,aAAA,CAAc,IAAA,CAAK,OAAO,CAAA;AAAA,EAC9C;AACF;;;AChEO,IAAM,mBAAA,GAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA,CAMjC,IAAA;AAYK,IAAM,qBAAA,GAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA,CAWnC,IAAA;;;AChDF,SAAS,QAAQ,KAAA,EAAwB;AACvC,EAAA,IAAI,KAAA,IAAS,IAAA,IAAQ,KAAA,KAAU,KAAA,EAAO,OAAO,CAAA;AAC7C,EAAA,MAAM,IAAI,OAAO,KAAA,KAAU,QAAA,GAAW,KAAA,GAAQ,OAAO,KAAK,CAAA;AAC1D,EAAA,OAAO,MAAA,CAAO,QAAA,CAAS,CAAC,CAAA,GAAI,CAAA,GAAI,CAAA;AAClC;AASO,SAAS,gBAAgB,MAAA,EAA+C;AAC7E,EAAA,IAAI,OAAO,MAAA,CAAO,IAAA,KAAS,UAAA,EAAY,OAAO,MAAA;AAC9C,EAAA,IAAI,OAAQ,MAAA,CAAuC,aAAA,KAAkB,UAAA,EAAY,OAAO,SAAA;AACxF,EAAA,OAAO,SAAA;AACT;AAUO,IAAM,aAAN,MAAkC;AAAA,EACtB,MAAA;AAAA,EACT,KAAA;AAAA,EAER,WAAA,CAAY,MAAA,EAAmB,OAAA,GAA6B,EAAC,EAAG;AAC9D,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,MAAA;AACvC,IAAA,IAAA,CAAK,KAAA,GAAQ,SAAA,KAAc,MAAA,GAAS,eAAA,CAAgB,MAAM,CAAA,GAAI,SAAA;AAC9D,IAAA,IAAI,KAAK,KAAA,KAAU,MAAA,IAAU,OAAO,MAAA,CAAO,SAAS,UAAA,EAAY;AAE9D,MAAA,IAAA,CAAK,KAAA,GAAQ,MAAA;AAAA,IACf;AAAA,EACF;AAAA;AAAA,EAGA,IAAI,MAAA,GAAkB;AACpB,IAAA,OAAO,KAAK,KAAA,KAAU,MAAA;AAAA,EACxB;AAAA,EAEA,MAAc,SAAA,CAAU,MAAA,EAAgB,IAAA,EAAgB,IAAA,EAA6C;AACnG,IAAA,MAAM,MAAA,GAAS,KAAK,MAAA,CAAO,IAAA;AAC3B,IAAA,IAAI,IAAA,CAAK,UAAU,SAAA,EAAW;AAC5B,MAAA,OAAO,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,MAAA,EAAQ,MAAA,EAAQ,KAAK,MAAA,EAAQ,GAAG,IAAA,EAAM,GAAG,IAAI,CAAA;AAAA,IACvE;AACA,IAAA,OAAO,OAAO,IAAA,CAAK,IAAA,CAAK,MAAA,EAAQ,MAAA,EAAQ,MAAM,IAAI,CAAA;AAAA,EACpD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,SAAA,CACZ,MAAA,EACA,IAAA,EACA,MACA,KAAA,EACwB;AACxB,IAAA,IAAI,IAAA,CAAK,KAAA,KAAU,MAAA,EAAQ,OAAO,MAAA;AAClC,IAAA,IAAI;AACF,MAAA,OAAO,MAAM,MAAM,IAAA,CAAK,UAAU,MAAA,EAAQ,IAAA,EAAM,IAAI,CAAC,CAAA;AAAA,IACvD,CAAA,CAAA,MAAQ;AACN,MAAA,IAAA,CAAK,KAAA,GAAQ,MAAA;AACb,MAAA,OAAO,MAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,GAAA,EAA8B;AACvC,IAAA,OAAO,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,GAAG,CAAA;AAAA,EAC7B;AAAA,EAEA,MAAM,OAAA,CAAQ,GAAA,EAAa,EAAA,EAA2B;AACpD,IAAA,MAAM,IAAA,CAAK,MAAA,CAAO,OAAA,CAAQ,GAAA,EAAK,EAAE,CAAA;AAAA,EACnC;AAAA,EAEA,MAAM,IAAI,GAAA,EAA8B;AACtC,IAAA,OAAO,QAAQ,MAAM,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,GAAG,CAAC,CAAA;AAAA,EAC3C;AAAA,EAEA,MAAM,eAAA,CAAgB,GAAA,EAAa,KAAA,EAAgC;AACjE,IAAA,MAAM,SAAA,GAAY,MAAM,IAAA,CAAK,SAAA,CAAU,mBAAA,EAAqB,CAAC,GAAG,CAAA,EAAG,CAAC,KAAK,CAAA,EAAG,OAAO,CAAA;AACnF,IAAA,IAAI,SAAA,KAAc,QAAW,OAAO,SAAA;AAEpC,IAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,IAAA,CAAK,GAAG,CAAA;AACjC,IAAA,IAAI,UAAU,CAAA,EAAG,MAAM,IAAA,CAAK,OAAA,CAAQ,KAAK,KAAK,CAAA;AAC9C,IAAA,OAAO,KAAA;AAAA,EACT;AAAA,EAEA,MAAM,iBAAA,CACJ,UAAA,EACA,WAAA,EACA,KAAA,EAC2B;AAC3B,IAAA,MAAM,SAAA,GAAY,MAAM,IAAA,CAAK,SAAA;AAAA,MAC3B,qBAAA;AAAA,MACA,CAAC,YAAY,WAAW,CAAA;AAAA,MACxB,CAAC,KAAK,CAAA;AAAA,MACN,CAAC,GAAA,KAA0B;AACzB,QAAA,MAAM,IAAA,GAAO,GAAA;AACb,QAAA,OAAO,CAAC,OAAA,CAAQ,IAAA,GAAO,CAAC,CAAC,GAAG,OAAA,CAAQ,IAAA,GAAO,CAAC,CAAC,CAAC,CAAA;AAAA,MAChD;AAAA,KACF;AACA,IAAA,IAAI,SAAA,KAAc,QAAW,OAAO,SAAA;AAEpC,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,CAAK,GAAA,CAAI,WAAW,CAAA;AAC3C,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,IAAA,CAAK,UAAU,CAAA;AAC1C,IAAA,IAAI,YAAY,CAAA,EAAG,MAAM,IAAA,CAAK,OAAA,CAAQ,YAAY,KAAK,CAAA;AACvD,IAAA,OAAO,CAAC,UAAU,OAAO,CAAA;AAAA,EAC3B;AACF;;;ACnHA,SAASC,SAAQ,KAAA,EAAwB;AACvC,EAAA,IAAI,KAAA,IAAS,IAAA,IAAQ,KAAA,KAAU,KAAA,EAAO,OAAO,CAAA;AAC7C,EAAA,MAAM,CAAA,GAAI,OAAO,KAAK,CAAA;AACtB,EAAA,OAAO,MAAA,CAAO,QAAA,CAAS,CAAC,CAAA,GAAI,CAAA,GAAI,CAAA;AAClC;AAOO,IAAM,mBAAN,MAAwC;AAAA,EAC5B,GAAA;AAAA,EACA,KAAA;AAAA,EACA,SAAA;AAAA,EAEjB,YAAY,OAAA,EAA6B;AACvC,IAAA,IAAA,CAAK,GAAA,GAAM,OAAA,CAAQ,GAAA,CAAI,OAAA,CAAQ,OAAO,EAAE,CAAA;AACxC,IAAA,IAAA,CAAK,QAAQ,OAAA,CAAQ,KAAA;AACrB,IAAA,MAAM,CAAA,GAAI,OAAA,CAAQ,KAAA,IAAS,UAAA,CAAW,KAAA;AACtC,IAAA,IAAI,OAAO,MAAM,UAAA,EAAY;AAC3B,MAAA,MAAM,IAAI,MAAM,yFAAyF,CAAA;AAAA,IAC3G;AACA,IAAA,IAAA,CAAK,SAAA,GAAY,CAAA;AAAA,EACnB;AAAA,EAEA,MAAc,QAAQ,IAAA,EAA6C;AACjE,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,SAAA,CAAU,KAAK,GAAA,EAAK;AAAA,MACzC,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,aAAA,EAAe,CAAA,OAAA,EAAU,IAAA,CAAK,KAAK,CAAA,CAAA;AAAA,QACnC,cAAA,EAAgB;AAAA,OAClB;AAAA,MACA,MAAM,IAAA,CAAK,SAAA,CAAU,IAAA,CAAK,GAAA,CAAI,MAAM,CAAC;AAAA,KACtC,CAAA;AACD,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,MAAA,MAAM,SAAS,MAAM,GAAA,CAAI,MAAK,CAAE,KAAA,CAAM,MAAM,EAAE,CAAA;AAC9C,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,6BAAA,EAAgC,GAAA,CAAI,MAAM,CAAA,CAAA,EAAI,GAAA,CAAI,UAAU,CAAA,EAAG,MAAA,GAAS,CAAA,GAAA,EAAM,MAAM,CAAA,CAAA,GAAK,EAAE,CAAA,CAAE,CAAA;AAAA,IAC/G;AACA,IAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAC7B,IAAA,IAAI,KAAK,KAAA,EAAO;AACd,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,mCAAA,EAAsC,IAAA,CAAK,KAAK,CAAA,CAAE,CAAA;AAAA,IACpE;AACA,IAAA,OAAO,IAAA,CAAK,MAAA;AAAA,EACd;AAAA,EAEA,MAAM,KAAK,GAAA,EAA8B;AACvC,IAAA,OAAO,MAAA,CAAO,MAAM,IAAA,CAAK,OAAA,CAAQ,CAAC,MAAA,EAAQ,GAAG,CAAC,CAAC,CAAA;AAAA,EACjD;AAAA,EAEA,MAAM,OAAA,CAAQ,GAAA,EAAa,EAAA,EAA2B;AACpD,IAAA,MAAM,KAAK,OAAA,CAAQ,CAAC,SAAA,EAAW,GAAA,EAAK,EAAE,CAAC,CAAA;AAAA,EACzC;AAAA,EAEA,MAAM,IAAI,GAAA,EAA8B;AACtC,IAAA,OAAOA,QAAAA,CAAQ,MAAM,IAAA,CAAK,OAAA,CAAQ,CAAC,KAAA,EAAO,GAAG,CAAC,CAAC,CAAA;AAAA,EACjD;AAAA;AAAA,EAGA,MAAc,UAAA,CACZ,MAAA,EACA,IAAA,EACA,IAAA,EACkB;AAClB,IAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,CAAC,MAAA,EAAQ,MAAA,EAAQ,IAAA,CAAK,MAAA,EAAQ,GAAG,IAAA,EAAM,GAAG,IAAI,CAAC,CAAA;AAAA,EACrE;AAAA,EAEA,MAAM,eAAA,CAAgB,GAAA,EAAa,KAAA,EAAgC;AACjE,IAAA,OAAOA,QAAAA,CAAQ,MAAM,IAAA,CAAK,UAAA,CAAW,mBAAA,EAAqB,CAAC,GAAG,CAAA,EAAG,CAAC,KAAK,CAAC,CAAC,CAAA;AAAA,EAC3E;AAAA,EAEA,MAAM,iBAAA,CACJ,UAAA,EACA,WAAA,EACA,KAAA,EAC2B;AAC3B,IAAA,MAAM,GAAA,GAAO,MAAM,IAAA,CAAK,UAAA,CAAW,qBAAA,EAAuB,CAAC,UAAA,EAAY,WAAW,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAC5F,IAAA,OAAO,CAACA,QAAAA,CAAQ,GAAA,GAAM,CAAC,CAAC,GAAGA,QAAAA,CAAQ,GAAA,GAAM,CAAC,CAAC,CAAC,CAAA;AAAA,EAC9C;AACF;;;AC7EA,SAAS,QAAQ,GAAA,EAAiC;AAChD,EAAA,MAAM,GAAA,GAAO,WAA0E,OAAA,EAAS,GAAA;AAChG,EAAA,OAAO,MAAM,GAAG,CAAA;AAClB;AASO,SAAS,aAAa,OAAA,EAA0C;AACrE,EAAA,IAAI,QAAQ,KAAA,EAAO;AACjB,IAAA,OAAO,EAAE,KAAA,EAAO,OAAA,CAAQ,OAAO,WAAA,EAAa,IAAA,EAAM,QAAQ,cAAA,EAAe;AAAA,EAC3E;AAEA,EAAA,IAAI,QAAQ,KAAA,EAAO;AACjB,IAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,OAAA,CAAQ,OAAO,EAAE,SAAA,EAAW,OAAA,CAAQ,SAAA,EAAW,CAAA;AAC5E,IAAA,OAAO;AAAA,MACL,KAAA;AAAA,MACA,WAAA,EAAa,IAAA;AAAA,MACb,MAAA,EAAQ,KAAA,CAAM,MAAA,GAAS,uBAAA,GAA0B;AAAA,KACnD;AAAA,EACF;AAEA,EAAA,MAAM,GAAA,GAAM,QAAQ,wBAAwB,CAAA;AAC5C,EAAA,MAAM,KAAA,GAAQ,QAAQ,0BAA0B,CAAA;AAChD,EAAA,IAAI,OAAO,KAAA,EAAO;AAChB,IAAA,OAAO,EAAE,KAAA,EAAO,IAAI,gBAAA,CAAiB,EAAE,GAAA,EAAK,KAAA,EAAO,CAAA,EAAG,WAAA,EAAa,IAAA,EAAM,MAAA,EAAQ,oBAAA,EAAqB;AAAA,EACxG;AAEA,EAAA,OAAO,EAAE,OAAO,IAAI,WAAA,IAAe,WAAA,EAAa,KAAA,EAAO,QAAQ,WAAA,EAAY;AAC7E;AAGO,SAAS,YAAA,GAAwB;AACtC,EAAA,OAAO,OAAA,CAAQ,UAAU,CAAA,KAAM,YAAA;AACjC;;;ACnDA,IAAM,KAAA,GAAgC;AAAA,EACpC,EAAA,EAAI,CAAA;AAAA,EACJ,CAAA,EAAG,GAAA;AAAA,EACH,CAAA,EAAG,GAAA;AAAA,EACH,CAAA,EAAG,IAAA;AAAA,EACH,CAAA,EAAG;AACL,CAAA;AAEA,IAAM,OAAA,GAAU,kCAAA;AAMT,SAAS,YAAY,MAAA,EAAiC;AAC3D,EAAA,IAAI,OAAO,WAAW,QAAA,EAAU;AAC9B,IAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,MAAM,CAAA,IAAK,UAAU,CAAA,EAAG;AAC3C,MAAA,MAAM,IAAI,UAAA,CAAW,CAAA,gEAAA,EAAmE,MAAM,CAAA,CAAE,CAAA;AAAA,IAClG;AACA,IAAA,OAAO,IAAA,CAAK,MAAM,MAAM,CAAA;AAAA,EAC1B;AAEA,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,IAAA,CAAK,MAAA,CAAO,MAAM,CAAA;AACxC,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,MAAM,IAAI,SAAA,CAAU,CAAA,0BAAA,EAA6B,MAAM,CAAA,yDAAA,CAA2D,CAAA;AAAA,EACpH;AAEA,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,CAAM,CAAC,CAAC,CAAA;AAC7B,EAAA,MAAM,IAAA,GAAO,KAAA,CAAM,KAAA,CAAM,CAAC,CAAW,CAAA;AACrC,EAAA,MAAM,EAAA,GAAK,IAAA,CAAK,KAAA,CAAM,KAAA,GAAQ,IAAI,CAAA;AAClC,EAAA,IAAI,MAAM,CAAA,EAAG;AACX,IAAA,MAAM,IAAI,UAAA,CAAW,CAAA,8CAAA,EAAiD,MAAM,CAAA,EAAA,CAAI,CAAA;AAAA,EAClF;AACA,EAAA,OAAO,EAAA;AACT;;;AC3BA,IAAM,UAAA,GAA0C;AAAA,EAC9C,cAAA,EAAgB,WAAA;AAAA,EAChB,gBAAA,EAAkB;AACpB,CAAA;AAYO,SAAS,UAAU,OAAA,EAAwC;AAChE,EAAA,MAAM,EAAE,OAAO,SAAA,GAAY,cAAA,EAAgB,SAAS,UAAA,EAAY,MAAA,GAAS,OAAM,GAAI,OAAA;AAEnF,EAAA,IAAI,CAAC,MAAA,CAAO,SAAA,CAAU,KAAK,CAAA,IAAK,SAAS,CAAA,EAAG;AAC1C,IAAA,MAAM,IAAI,UAAA,CAAW,CAAA,gDAAA,EAAmD,KAAK,CAAA,CAAE,CAAA;AAAA,EACjF;AAEA,EAAA,MAAM,GAAA,GAAM,WAAW,SAAS,CAAA;AAChC,EAAA,IAAI,CAAC,GAAA,EAAK;AACR,IAAA,MAAM,IAAI,SAAA,CAAU,CAAA,6BAAA,EAAgC,SAAS,CAAA,0CAAA,CAA4C,CAAA;AAAA,EAC3G;AAEA,EAAA,MAAM,QAAA,GAAW,WAAA,CAAY,OAAA,CAAQ,MAAM,CAAA;AAC3C,EAAA,MAAM,EAAE,KAAA,EAAO,WAAA,EAAa,MAAA,EAAO,GAAI,aAAa,OAAO,CAAA;AAE3D,EAAA,IAAI,CAAC,WAAA,IAAe,CAAC,MAAA,IAAU,cAAa,EAAG;AAG7C,IAAA,OAAA,CAAQ,IAAA;AAAA,MACN,CAAA,sNAAA;AAAA,KAEF;AAAA,EACF;AAEA,EAAA,MAAM,GAAA,GAAwB,EAAE,KAAA,EAAO,QAAA,EAAU,MAAA,EAAO;AAExD,EAAA,OAAO;AAAA,IACL,MAAM,MAAM,UAAA,EAAoB;AAC9B,MAAA,IAAI,CAAC,UAAA,EAAY;AACf,QAAA,MAAM,IAAI,UAAU,kDAAkD,CAAA;AAAA,MACxE;AACA,MAAA,OAAO,IAAI,KAAA,EAAO,GAAA,EAAK,UAAA,EAAY,IAAA,CAAK,KAAK,CAAA;AAAA,IAC/C;AAAA,GACF;AACF","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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ratefall",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A rate limiter that just works: distributed (Redis) in production, in-memory in development, same code, zero store wiring.",
5
5
  "keywords": [
6
6
  "rate-limit",