@joint-ops/hitlimit-bun 1.2.0 → 1.4.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
@@ -2,7 +2,9 @@
2
2
 
3
3
  > Rate limiting built for Bun. Not ported — built.
4
4
 
5
- **12.38M ops/sec** on memory. **8.32M at 10K IPs**. Native bun:sqlite. Atomic Redis Lua. Postgres. Zero dependencies.
5
+ <!-- BENCH:BUN_HERO -->
6
+ **7.73M ops/sec** on memory. **5.57M at 10K IPs**. Native bun:sqlite. Atomic Redis Lua. Postgres. Zero dependencies.
7
+ <!-- /BENCH:BUN_HERO -->
6
8
 
7
9
  ```bash
8
10
  bun add @joint-ops/hitlimit-bun
@@ -94,35 +96,123 @@ new Elysia()
94
96
 
95
97
  ---
96
98
 
97
- ## 4 Storage Backends
99
+ ## Pick Your Store
98
100
 
99
- All built in. No extra packages to install.
101
+ Every store is built in. Swap one line your rate limiting code stays the same.
102
+
103
+ ```
104
+ Single Server Multi-Server
105
+ ┌──────────────────────┐ ┌──────────────────────────┐
106
+ │ Memory │ SQLite │ │ Redis │ Postgres │
107
+ │ (default) (bun:sqlite) │ Valkey │ MongoDB │
108
+ │ │ │ Dragonfly MySQL │
109
+ └──────────────────────┘ └──────────────────────────┘
110
+ No dependencies at all Your existing infra, zero lock-in
111
+ ```
112
+
113
+ <!-- BENCH:BUN_STORE_TABLE -->
114
+ | Store | Ops/sec | Latency | When to use |
115
+ |-------|---------|---------|-------------|
116
+ | Memory | 5,574,103 | 179ns | Single server, maximum speed |
117
+ | bun:sqlite | 372,247 | 2.7μs | Single server, need persistence |
118
+ | MongoDB | 2,132 | 469μs | Multi-server / NoSQL infrastructure |
119
+ <!-- /BENCH:BUN_STORE_TABLE -->
120
+
121
+ > Redis, Valkey, DragonflyDB, Postgres, and MySQL are network-bound (~200–3,500 ops/sec). Benchmarks at [hitlimit.jointops.dev/docs/benchmarks](https://hitlimit.jointops.dev/docs/benchmarks).
122
+
123
+ ### The pattern is always the same
100
124
 
101
125
  ```typescript
102
126
  import { hitlimit } from '@joint-ops/hitlimit-bun'
127
+ import { ______Store } from '@joint-ops/hitlimit-bun/stores/______'
128
+
129
+ Bun.serve({ fetch: hitlimit({ store: ______Store({ /* config */ }) }, handler) })
130
+ ```
103
131
 
104
- // Memory (default) — fastest, no config
105
- Bun.serve({ fetch: hitlimit({}, handler) })
132
+ <details>
133
+ <summary><b>Memory</b> default, zero config</summary>
106
134
 
107
- // bun:sqlite — persists across restarts, native performance
135
+ ```typescript
136
+ Bun.serve({ fetch: hitlimit({}, handler) }) // that's it
137
+ ```
138
+ </details>
139
+
140
+ <details>
141
+ <summary><b>bun:sqlite</b> — native, no N-API, no FFI, survives restarts</summary>
142
+
143
+ ```typescript
108
144
  import { sqliteStore } from '@joint-ops/hitlimit-bun'
109
145
  Bun.serve({ fetch: hitlimit({ store: sqliteStore({ path: './ratelimit.db' }) }, handler) })
146
+ ```
147
+ No peer dependency — `bun:sqlite` is built into Bun.
148
+ </details>
149
+
150
+ <details>
151
+ <summary><b>Redis</b> — distributed, atomic Lua scripts</summary>
110
152
 
111
- // Redis — distributed, atomic Lua scripts
153
+ ```typescript
112
154
  import { redisStore } from '@joint-ops/hitlimit-bun/stores/redis'
113
155
  Bun.serve({ fetch: hitlimit({ store: redisStore({ url: 'redis://localhost:6379' }) }, handler) })
156
+ ```
157
+ Peer dep: `ioredis`
158
+ </details>
159
+
160
+ <details>
161
+ <summary><b>Valkey</b> — open-source Redis fork, drop-in replacement</summary>
114
162
 
115
- // Postgres — distributed, atomic upserts
163
+ ```typescript
164
+ import { valkeyStore } from '@joint-ops/hitlimit-bun/stores/valkey'
165
+ Bun.serve({ fetch: hitlimit({ store: valkeyStore({ url: 'redis://localhost:6379' }) }, handler) })
166
+ ```
167
+ Peer dep: `ioredis`
168
+ </details>
169
+
170
+ <details>
171
+ <summary><b>DragonflyDB</b> — Redis-compatible, higher throughput</summary>
172
+
173
+ ```typescript
174
+ import { dragonflyStore } from '@joint-ops/hitlimit-bun/stores/dragonfly'
175
+ Bun.serve({ fetch: hitlimit({ store: dragonflyStore({ url: 'redis://localhost:6379' }) }, handler) })
176
+ ```
177
+ Peer dep: `ioredis`
178
+ </details>
179
+
180
+ <details>
181
+ <summary><b>PostgreSQL</b> — use your existing database</summary>
182
+
183
+ ```typescript
116
184
  import { postgresStore } from '@joint-ops/hitlimit-bun/stores/postgres'
117
185
  Bun.serve({ fetch: hitlimit({ store: postgresStore({ url: 'postgres://localhost:5432/mydb' }) }, handler) })
118
186
  ```
187
+ Peer dep: `pg`
188
+ </details>
119
189
 
120
- | Store | Ops/sec | Latency | When to use |
121
- |-------|---------|---------|-------------|
122
- | Memory | 8,320,000 | 120ns | Single server, maximum speed |
123
- | bun:sqlite | 325,000 | 3.1μs | Single server, need persistence |
124
- | Redis | 6,700 | 148μs | Multi-server / distributed |
125
- | Postgres | 3,700 | 273μs | Multi-server / already using Postgres |
190
+ <details>
191
+ <summary><b>MongoDB</b> — NoSQL, TTL indexes, MEAN/MERN stacks</summary>
192
+
193
+ ```typescript
194
+ import { mongoStore } from '@joint-ops/hitlimit-bun/stores/mongodb'
195
+ import { MongoClient } from 'mongodb'
196
+
197
+ const client = new MongoClient('mongodb://localhost:27017')
198
+ const db = client.db('myapp')
199
+ Bun.serve({ fetch: hitlimit({ store: mongoStore({ db }) }, handler) })
200
+ ```
201
+ Peer dep: `mongodb`
202
+ </details>
203
+
204
+ <details>
205
+ <summary><b>MySQL</b> — SQL distributed, LAMP stacks</summary>
206
+
207
+ ```typescript
208
+ import { mysqlStore } from '@joint-ops/hitlimit-bun/stores/mysql'
209
+ import mysql from 'mysql2/promise'
210
+
211
+ const pool = mysql.createPool('mysql://root@localhost:3306/mydb')
212
+ Bun.serve({ fetch: hitlimit({ store: mysqlStore({ pool }) }, handler) })
213
+ ```
214
+ Peer dep: `mysql2`
215
+ </details>
126
216
 
127
217
  ---
128
218
 
@@ -130,14 +220,18 @@ Bun.serve({ fetch: hitlimit({ store: postgresStore({ url: 'postgres://localhost:
130
220
 
131
221
  ### Bun vs Node.js — Memory Store, 10K unique IPs
132
222
 
223
+ <!-- BENCH:BUN_VS_NODE_TABLE -->
133
224
  | Runtime | Ops/sec | |
134
225
  |---------|---------|---|
135
- | **Bun** | **8,320,000** | ████████████████████ |
136
- | Node.js | 3,160,000 | ████████ |
226
+ | **Bun** | **5,574,103** | ████████████████████ |
227
+ | Node.js | 4,082,874 | ███████████████ |
228
+ <!-- /BENCH:BUN_VS_NODE_TABLE -->
137
229
 
138
- Bun leads at 10K IPs (8.32M vs 3.16M) and single-IP (12.38M vs 4.83M). Same library, same algorithm, **memory store**. For Redis, Postgres, and cross-store breakdowns, see the [full benchmark results](https://github.com/JointOps/hitlimit-monorepo/tree/main/benchmarks). Controlled-environment microbenchmarks with transparent methodology. Run them yourself.
230
+ <!-- BENCH:BUN_VS_NODE_TEXT -->
231
+ Bun leads at 10K IPs (5.57M vs 4.08M) and single-IP (7.73M vs 5.96M). Same library, same algorithm, **memory store**. For Redis, Postgres, and cross-store breakdowns, see the [full benchmark results](https://github.com/JointOps/hitlimit-monorepo/tree/main/benchmarks). Controlled-environment microbenchmarks with transparent methodology. Run them yourself.
232
+ <!-- /BENCH:BUN_VS_NODE_TEXT -->
139
233
 
140
- ### Why bun:sqlite is faster than better-sqlite3
234
+ ### Why bun:sqlite doesn't need bindings
141
235
 
142
236
  ```
143
237
  Node.js: JS → N-API → C++ binding → SQLite
@@ -0,0 +1,9 @@
1
+ import type { HitLimitStore } from '@joint-ops/hitlimit-types';
2
+ export interface DragonflyStoreOptions {
3
+ /** DragonflyDB connection URL. Default: 'redis://localhost:6379' */
4
+ url?: string;
5
+ /** Key prefix for all rate limit keys. Default: 'hitlimit:' */
6
+ keyPrefix?: string;
7
+ }
8
+ export declare function dragonflyStore(options?: DragonflyStoreOptions): HitLimitStore;
9
+ //# sourceMappingURL=dragonfly.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dragonfly.d.ts","sourceRoot":"","sources":["../../src/stores/dragonfly.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAG9D,MAAM,WAAW,qBAAqB;IACpC,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,aAAa,CAE7E"}
@@ -0,0 +1,133 @@
1
+ // @bun
2
+ // src/stores/redis.ts
3
+ import Redis from "ioredis";
4
+ var HIT_SCRIPT = `
5
+ local key = KEYS[1]
6
+ local windowMs = tonumber(ARGV[1])
7
+ local count = redis.call('INCR', key)
8
+ local ttl = redis.call('PTTL', key)
9
+ if ttl < 0 then
10
+ redis.call('PEXPIRE', key, windowMs)
11
+ ttl = windowMs
12
+ end
13
+ return {count, ttl}
14
+ `;
15
+ var HIT_WITH_BAN_SCRIPT = `
16
+ local hitKey = KEYS[1]
17
+ local banKey = KEYS[2]
18
+ local violationKey = KEYS[3]
19
+ local windowMs = tonumber(ARGV[1])
20
+ local limit = tonumber(ARGV[2])
21
+ local banThreshold = tonumber(ARGV[3])
22
+ local banDurationMs = tonumber(ARGV[4])
23
+
24
+ -- Check ban first
25
+ local banTTL = redis.call('PTTL', banKey)
26
+ if banTTL > 0 then
27
+ return {-1, banTTL, 1, 0}
28
+ end
29
+
30
+ -- Hit counter
31
+ local count = redis.call('INCR', hitKey)
32
+ local ttl = redis.call('PTTL', hitKey)
33
+ if ttl < 0 then
34
+ redis.call('PEXPIRE', hitKey, windowMs)
35
+ ttl = windowMs
36
+ end
37
+
38
+ -- Track violations if over limit
39
+ local banned = 0
40
+ local violations = 0
41
+ if count > limit then
42
+ violations = redis.call('INCR', violationKey)
43
+ local vTTL = redis.call('PTTL', violationKey)
44
+ if vTTL < 0 then
45
+ redis.call('PEXPIRE', violationKey, banDurationMs)
46
+ end
47
+ if violations >= banThreshold then
48
+ redis.call('SET', banKey, '1', 'PX', banDurationMs)
49
+ banned = 1
50
+ end
51
+ end
52
+ return {count, ttl, banned, violations}
53
+ `;
54
+
55
+ class RedisStore {
56
+ redis;
57
+ prefix;
58
+ banPrefix;
59
+ violationPrefix;
60
+ constructor(options = {}) {
61
+ this.redis = new Redis(options.url ?? "redis://localhost:6379");
62
+ this.prefix = options.keyPrefix ?? "hitlimit:";
63
+ this.banPrefix = (options.keyPrefix ?? "hitlimit:") + "ban:";
64
+ this.violationPrefix = (options.keyPrefix ?? "hitlimit:") + "violations:";
65
+ this.redis.defineCommand("hitlimitHit", {
66
+ numberOfKeys: 1,
67
+ lua: HIT_SCRIPT
68
+ });
69
+ this.redis.defineCommand("hitlimitHitWithBan", {
70
+ numberOfKeys: 3,
71
+ lua: HIT_WITH_BAN_SCRIPT
72
+ });
73
+ }
74
+ async hit(key, windowMs, _limit) {
75
+ const redisKey = this.prefix + key;
76
+ const result = await this.redis.hitlimitHit(redisKey, windowMs);
77
+ const count = result[0];
78
+ const ttl = result[1];
79
+ return { count, resetAt: Date.now() + ttl };
80
+ }
81
+ async hitWithBan(key, windowMs, limit, banThreshold, banDurationMs) {
82
+ const hitKey = this.prefix + key;
83
+ const banKey = this.banPrefix + key;
84
+ const violationKey = this.violationPrefix + key;
85
+ const result = await this.redis.hitlimitHitWithBan(hitKey, banKey, violationKey, windowMs, limit, banThreshold, banDurationMs);
86
+ const count = result[0];
87
+ const ttl = result[1];
88
+ const banned = result[2] === 1;
89
+ const violations = result[3];
90
+ if (count === -1) {
91
+ return { count: 0, resetAt: Date.now() + ttl, banned: true, violations: 0, banExpiresAt: Date.now() + ttl };
92
+ }
93
+ return {
94
+ count,
95
+ resetAt: Date.now() + ttl,
96
+ banned,
97
+ violations,
98
+ banExpiresAt: banned ? Date.now() + banDurationMs : 0
99
+ };
100
+ }
101
+ async isBanned(key) {
102
+ const result = await this.redis.exists(this.banPrefix + key);
103
+ return result === 1;
104
+ }
105
+ async ban(key, durationMs) {
106
+ await this.redis.set(this.banPrefix + key, "1", "PX", durationMs);
107
+ }
108
+ async recordViolation(key, windowMs) {
109
+ const redisKey = this.violationPrefix + key;
110
+ const count = await this.redis.incr(redisKey);
111
+ if (count === 1) {
112
+ await this.redis.pexpire(redisKey, windowMs);
113
+ }
114
+ return count;
115
+ }
116
+ async reset(key) {
117
+ await this.redis.del(this.prefix + key, this.banPrefix + key, this.violationPrefix + key);
118
+ }
119
+ async shutdown() {
120
+ await this.redis.quit();
121
+ }
122
+ }
123
+ function redisStore(options) {
124
+ return new RedisStore(options);
125
+ }
126
+
127
+ // src/stores/dragonfly.ts
128
+ function dragonflyStore(options) {
129
+ return new RedisStore(options);
130
+ }
131
+ export {
132
+ dragonflyStore
133
+ };
@@ -0,0 +1,11 @@
1
+ import type { HitLimitStore } from '@joint-ops/hitlimit-types';
2
+ export interface MongoStoreOptions {
3
+ /** MongoDB Db instance (from MongoClient.db()) */
4
+ db: any;
5
+ /** Collection name prefix. Default: 'hitlimit' */
6
+ collectionPrefix?: string;
7
+ /** Skip TTL index creation (if you manage indexes yourself). Default: false */
8
+ skipIndexCreation?: boolean;
9
+ }
10
+ export declare function mongoStore(options: MongoStoreOptions): HitLimitStore;
11
+ //# sourceMappingURL=mongodb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mongodb.d.ts","sourceRoot":"","sources":["../../src/stores/mongodb.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,2BAA2B,CAAA;AAE7F,MAAM,WAAW,iBAAiB;IAChC,kDAAkD;IAClD,EAAE,EAAE,GAAG,CAAA;IACP,kDAAkD;IAClD,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,+EAA+E;IAC/E,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AA+KD,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,aAAa,CAEpE"}
@@ -0,0 +1,133 @@
1
+ // @bun
2
+ // src/stores/mongodb.ts
3
+ class MongoStore {
4
+ db;
5
+ prefix;
6
+ indexesReady = null;
7
+ ready;
8
+ constructor(options) {
9
+ this.db = options.db;
10
+ this.prefix = options.collectionPrefix ?? "hitlimit";
11
+ if (options.skipIndexCreation) {
12
+ this.ready = true;
13
+ } else {
14
+ this.ready = false;
15
+ this.indexesReady = this.createIndexes().then(() => {
16
+ this.ready = true;
17
+ });
18
+ }
19
+ }
20
+ col(name) {
21
+ return this.db.collection(`${this.prefix}_${name}`);
22
+ }
23
+ async createIndexes() {
24
+ await this.col("hits").createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 });
25
+ await this.col("bans").createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 });
26
+ await this.col("violations").createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 });
27
+ await this.col("hits").createIndex({ key: 1 }, { unique: true });
28
+ await this.col("bans").createIndex({ key: 1 }, { unique: true });
29
+ await this.col("violations").createIndex({ key: 1 }, { unique: true });
30
+ }
31
+ async atomicIncrement(collection, key, windowMs) {
32
+ const now = Date.now();
33
+ const resetAt = now + windowMs;
34
+ const expireAt = new Date(resetAt);
35
+ try {
36
+ const result = await collection.findOneAndUpdate({ key, resetAt: { $gt: now } }, {
37
+ $inc: { count: 1 },
38
+ $setOnInsert: { key, resetAt, expireAt }
39
+ }, { upsert: true, returnDocument: "after" });
40
+ return { count: result.count, resetAt: result.resetAt };
41
+ } catch (err) {
42
+ if (err?.code === 11000) {
43
+ const replaced = await collection.findOneAndUpdate({ key, resetAt: { $lte: now } }, { $set: { count: 1, resetAt, expireAt } }, { returnDocument: "after" });
44
+ if (replaced) {
45
+ return { count: replaced.count, resetAt: replaced.resetAt };
46
+ }
47
+ return this.atomicIncrement(collection, key, windowMs);
48
+ }
49
+ throw err;
50
+ }
51
+ }
52
+ async hit(key, windowMs, _limit) {
53
+ if (!this.ready)
54
+ await this.indexesReady;
55
+ return this.atomicIncrement(this.col("hits"), key, windowMs);
56
+ }
57
+ async hitWithBan(key, windowMs, limit, banThreshold, banDurationMs) {
58
+ if (!this.ready)
59
+ await this.indexesReady;
60
+ const now = Date.now();
61
+ const resetAt = now + windowMs;
62
+ const banExpiresAt = now + banDurationMs;
63
+ const ban = await this.col("bans").findOne({ key, expiresAt: { $gt: now } });
64
+ if (ban) {
65
+ return {
66
+ count: 0,
67
+ resetAt,
68
+ banned: true,
69
+ violations: 0,
70
+ banExpiresAt: ban.expiresAt
71
+ };
72
+ }
73
+ const hitResult = await this.atomicIncrement(this.col("hits"), key, windowMs);
74
+ const hitCount = hitResult.count;
75
+ const hitResetAt = hitResult.resetAt;
76
+ if (hitCount > limit) {
77
+ const violationResult = await this.atomicIncrement(this.col("violations"), key, banDurationMs);
78
+ const violations = violationResult.count;
79
+ const shouldBan = violations >= banThreshold;
80
+ if (shouldBan) {
81
+ await this.col("bans").updateOne({ key }, { $set: { expiresAt: banExpiresAt, expireAt: new Date(banExpiresAt) } }, { upsert: true });
82
+ }
83
+ return {
84
+ count: hitCount,
85
+ resetAt: hitResetAt,
86
+ banned: shouldBan,
87
+ violations,
88
+ banExpiresAt: shouldBan ? banExpiresAt : 0
89
+ };
90
+ }
91
+ return {
92
+ count: hitCount,
93
+ resetAt: hitResetAt,
94
+ banned: false,
95
+ violations: 0,
96
+ banExpiresAt: 0
97
+ };
98
+ }
99
+ async isBanned(key) {
100
+ if (!this.ready)
101
+ await this.indexesReady;
102
+ const ban = await this.col("bans").findOne({ key, expiresAt: { $gt: Date.now() } });
103
+ return ban !== null;
104
+ }
105
+ async ban(key, durationMs) {
106
+ if (!this.ready)
107
+ await this.indexesReady;
108
+ const expiresAt = Date.now() + durationMs;
109
+ await this.col("bans").updateOne({ key }, { $set: { expiresAt, expireAt: new Date(expiresAt) } }, { upsert: true });
110
+ }
111
+ async recordViolation(key, windowMs) {
112
+ if (!this.ready)
113
+ await this.indexesReady;
114
+ const result = await this.atomicIncrement(this.col("violations"), key, windowMs);
115
+ return result.count;
116
+ }
117
+ async reset(key) {
118
+ if (!this.ready)
119
+ await this.indexesReady;
120
+ await Promise.all([
121
+ this.col("hits").deleteOne({ key }),
122
+ this.col("bans").deleteOne({ key }),
123
+ this.col("violations").deleteOne({ key })
124
+ ]);
125
+ }
126
+ shutdown() {}
127
+ }
128
+ function mongoStore(options) {
129
+ return new MongoStore(options);
130
+ }
131
+ export {
132
+ mongoStore
133
+ };
@@ -0,0 +1,13 @@
1
+ import type { HitLimitStore } from '@joint-ops/hitlimit-types';
2
+ export interface MySQLStoreOptions {
3
+ /** mysql2/promise Pool instance */
4
+ pool: any;
5
+ /** Table name prefix. Default: 'hitlimit' */
6
+ tablePrefix?: string;
7
+ /** Cleanup interval in ms. Default: 60000 (1 minute) */
8
+ cleanupInterval?: number;
9
+ /** Skip table creation (if you manage schema yourself). Default: false */
10
+ skipTableCreation?: boolean;
11
+ }
12
+ export declare function mysqlStore(options: MySQLStoreOptions): HitLimitStore;
13
+ //# sourceMappingURL=mysql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mysql.d.ts","sourceRoot":"","sources":["../../src/stores/mysql.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,2BAA2B,CAAA;AAE7F,MAAM,WAAW,iBAAiB;IAChC,mCAAmC;IACnC,IAAI,EAAE,GAAG,CAAA;IACT,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,wDAAwD;IACxD,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,0EAA0E;IAC1E,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AA2ND,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,aAAa,CAEpE"}
@@ -0,0 +1,195 @@
1
+ // @bun
2
+ // src/stores/mysql.ts
3
+ class MySQLStore {
4
+ pool;
5
+ prefix;
6
+ cleanupTimer = null;
7
+ tablesReady = null;
8
+ ready;
9
+ constructor(options) {
10
+ this.pool = options.pool;
11
+ this.prefix = options.tablePrefix ?? "hitlimit";
12
+ if (options.skipTableCreation) {
13
+ this.ready = true;
14
+ } else {
15
+ this.ready = false;
16
+ this.tablesReady = this.createTables().then(() => {
17
+ this.ready = true;
18
+ });
19
+ }
20
+ const interval = options.cleanupInterval ?? 60000;
21
+ this.cleanupTimer = setInterval(() => this.cleanup(), interval);
22
+ if (typeof this.cleanupTimer.unref === "function") {
23
+ this.cleanupTimer.unref();
24
+ }
25
+ }
26
+ async createTables() {
27
+ await this.pool.execute(`
28
+ CREATE TABLE IF NOT EXISTS ${this.prefix}_hits (
29
+ \`key\` VARCHAR(255) NOT NULL PRIMARY KEY,
30
+ count INT NOT NULL DEFAULT 1,
31
+ reset_at BIGINT NOT NULL
32
+ ) ENGINE=InnoDB
33
+ `);
34
+ await this.pool.execute(`
35
+ CREATE TABLE IF NOT EXISTS ${this.prefix}_bans (
36
+ \`key\` VARCHAR(255) NOT NULL PRIMARY KEY,
37
+ expires_at BIGINT NOT NULL
38
+ ) ENGINE=InnoDB
39
+ `);
40
+ await this.pool.execute(`
41
+ CREATE TABLE IF NOT EXISTS ${this.prefix}_violations (
42
+ \`key\` VARCHAR(255) NOT NULL PRIMARY KEY,
43
+ count INT NOT NULL DEFAULT 1,
44
+ reset_at BIGINT NOT NULL
45
+ ) ENGINE=InnoDB
46
+ `);
47
+ }
48
+ async hit(key, windowMs, _limit) {
49
+ if (!this.ready)
50
+ await this.tablesReady;
51
+ const now = Date.now();
52
+ const resetAt = now + windowMs;
53
+ const conn = await this.pool.getConnection();
54
+ try {
55
+ await conn.execute(`
56
+ INSERT INTO ${this.prefix}_hits (\`key\`, count, reset_at)
57
+ VALUES (?, LAST_INSERT_ID(1), ?)
58
+ ON DUPLICATE KEY UPDATE
59
+ count = LAST_INSERT_ID(IF(reset_at <= ?, 1, count + 1)),
60
+ reset_at = IF(reset_at <= ?, ?, reset_at)
61
+ `, [key, resetAt, now, now, resetAt]);
62
+ const [rows] = await conn.execute(`SELECT LAST_INSERT_ID() AS count, reset_at FROM ${this.prefix}_hits WHERE \`key\` = ?`, [key]);
63
+ return { count: Number(rows[0].count), resetAt: Number(rows[0].reset_at) };
64
+ } finally {
65
+ conn.release();
66
+ }
67
+ }
68
+ async hitWithBan(key, windowMs, limit, banThreshold, banDurationMs) {
69
+ if (!this.ready)
70
+ await this.tablesReady;
71
+ const now = Date.now();
72
+ const resetAt = now + windowMs;
73
+ const banExpiresAt = now + banDurationMs;
74
+ const [banRows] = await this.pool.execute(`SELECT expires_at FROM ${this.prefix}_bans WHERE \`key\` = ? AND expires_at > ?`, [key, now]);
75
+ if (banRows.length > 0) {
76
+ return {
77
+ count: 0,
78
+ resetAt,
79
+ banned: true,
80
+ violations: 0,
81
+ banExpiresAt: Number(banRows[0].expires_at)
82
+ };
83
+ }
84
+ const conn = await this.pool.getConnection();
85
+ try {
86
+ await conn.execute(`
87
+ INSERT INTO ${this.prefix}_hits (\`key\`, count, reset_at)
88
+ VALUES (?, LAST_INSERT_ID(1), ?)
89
+ ON DUPLICATE KEY UPDATE
90
+ count = LAST_INSERT_ID(IF(reset_at <= ?, 1, count + 1)),
91
+ reset_at = IF(reset_at <= ?, ?, reset_at)
92
+ `, [key, resetAt, now, now, resetAt]);
93
+ const [hitRows] = await conn.execute(`SELECT LAST_INSERT_ID() AS count, reset_at FROM ${this.prefix}_hits WHERE \`key\` = ?`, [key]);
94
+ const hitCount = Number(hitRows[0].count);
95
+ const hitResetAt = Number(hitRows[0].reset_at);
96
+ if (hitCount > limit) {
97
+ await conn.execute(`
98
+ INSERT INTO ${this.prefix}_violations (\`key\`, count, reset_at)
99
+ VALUES (?, LAST_INSERT_ID(1), ?)
100
+ ON DUPLICATE KEY UPDATE
101
+ count = LAST_INSERT_ID(IF(reset_at <= ?, 1, count + 1)),
102
+ reset_at = IF(reset_at <= ?, ?, reset_at)
103
+ `, [key, banExpiresAt, now, now, banExpiresAt]);
104
+ const [violRows] = await conn.execute(`SELECT LAST_INSERT_ID() AS count FROM ${this.prefix}_violations WHERE \`key\` = ?`, [key]);
105
+ const violations = Number(violRows[0].count);
106
+ const shouldBan = violations >= banThreshold;
107
+ if (shouldBan) {
108
+ await conn.execute(`
109
+ INSERT INTO ${this.prefix}_bans (\`key\`, expires_at) VALUES (?, ?)
110
+ ON DUPLICATE KEY UPDATE expires_at = ?
111
+ `, [key, banExpiresAt, banExpiresAt]);
112
+ }
113
+ return {
114
+ count: hitCount,
115
+ resetAt: hitResetAt,
116
+ banned: shouldBan,
117
+ violations,
118
+ banExpiresAt: shouldBan ? banExpiresAt : 0
119
+ };
120
+ }
121
+ return {
122
+ count: hitCount,
123
+ resetAt: hitResetAt,
124
+ banned: false,
125
+ violations: 0,
126
+ banExpiresAt: 0
127
+ };
128
+ } finally {
129
+ conn.release();
130
+ }
131
+ }
132
+ async isBanned(key) {
133
+ if (!this.ready)
134
+ await this.tablesReady;
135
+ const [rows] = await this.pool.execute(`SELECT 1 FROM ${this.prefix}_bans WHERE \`key\` = ? AND expires_at > ?`, [key, Date.now()]);
136
+ return rows.length > 0;
137
+ }
138
+ async ban(key, durationMs) {
139
+ if (!this.ready)
140
+ await this.tablesReady;
141
+ const expiresAt = Date.now() + durationMs;
142
+ await this.pool.execute(`
143
+ INSERT INTO ${this.prefix}_bans (\`key\`, expires_at) VALUES (?, ?)
144
+ ON DUPLICATE KEY UPDATE expires_at = ?
145
+ `, [key, expiresAt, expiresAt]);
146
+ }
147
+ async recordViolation(key, windowMs) {
148
+ if (!this.ready)
149
+ await this.tablesReady;
150
+ const now = Date.now();
151
+ const resetAt = now + windowMs;
152
+ const conn = await this.pool.getConnection();
153
+ try {
154
+ await conn.execute(`
155
+ INSERT INTO ${this.prefix}_violations (\`key\`, count, reset_at) VALUES (?, LAST_INSERT_ID(1), ?)
156
+ ON DUPLICATE KEY UPDATE
157
+ count = LAST_INSERT_ID(IF(reset_at <= ?, 1, count + 1)),
158
+ reset_at = IF(reset_at <= ?, ?, reset_at)
159
+ `, [key, resetAt, now, now, resetAt]);
160
+ const [rows] = await conn.execute(`SELECT LAST_INSERT_ID() AS count FROM ${this.prefix}_violations WHERE \`key\` = ?`, [key]);
161
+ return Number(rows[0].count);
162
+ } finally {
163
+ conn.release();
164
+ }
165
+ }
166
+ async reset(key) {
167
+ if (!this.ready)
168
+ await this.tablesReady;
169
+ await Promise.all([
170
+ this.pool.execute(`DELETE FROM ${this.prefix}_hits WHERE \`key\` = ?`, [key]),
171
+ this.pool.execute(`DELETE FROM ${this.prefix}_bans WHERE \`key\` = ?`, [key]),
172
+ this.pool.execute(`DELETE FROM ${this.prefix}_violations WHERE \`key\` = ?`, [key])
173
+ ]);
174
+ }
175
+ async cleanup() {
176
+ try {
177
+ const now = Date.now();
178
+ await this.pool.execute(`DELETE FROM ${this.prefix}_hits WHERE reset_at <= ?`, [now]);
179
+ await this.pool.execute(`DELETE FROM ${this.prefix}_bans WHERE expires_at <= ?`, [now]);
180
+ await this.pool.execute(`DELETE FROM ${this.prefix}_violations WHERE reset_at <= ?`, [now]);
181
+ } catch {}
182
+ }
183
+ shutdown() {
184
+ if (this.cleanupTimer) {
185
+ clearInterval(this.cleanupTimer);
186
+ this.cleanupTimer = null;
187
+ }
188
+ }
189
+ }
190
+ function mysqlStore(options) {
191
+ return new MySQLStore(options);
192
+ }
193
+ export {
194
+ mysqlStore
195
+ };
@@ -1,7 +1,23 @@
1
- import type { HitLimitStore } from '@joint-ops/hitlimit-types';
1
+ import type { HitLimitStore, HitWithBanResult, StoreResult } from '@joint-ops/hitlimit-types';
2
2
  export interface RedisStoreOptions {
3
3
  url?: string;
4
4
  keyPrefix?: string;
5
5
  }
6
+ export declare const HIT_SCRIPT = "\nlocal key = KEYS[1]\nlocal windowMs = tonumber(ARGV[1])\nlocal count = redis.call('INCR', key)\nlocal ttl = redis.call('PTTL', key)\nif ttl < 0 then\n redis.call('PEXPIRE', key, windowMs)\n ttl = windowMs\nend\nreturn {count, ttl}\n";
7
+ export declare const HIT_WITH_BAN_SCRIPT = "\nlocal hitKey = KEYS[1]\nlocal banKey = KEYS[2]\nlocal violationKey = KEYS[3]\nlocal windowMs = tonumber(ARGV[1])\nlocal limit = tonumber(ARGV[2])\nlocal banThreshold = tonumber(ARGV[3])\nlocal banDurationMs = tonumber(ARGV[4])\n\n-- Check ban first\nlocal banTTL = redis.call('PTTL', banKey)\nif banTTL > 0 then\n return {-1, banTTL, 1, 0}\nend\n\n-- Hit counter\nlocal count = redis.call('INCR', hitKey)\nlocal ttl = redis.call('PTTL', hitKey)\nif ttl < 0 then\n redis.call('PEXPIRE', hitKey, windowMs)\n ttl = windowMs\nend\n\n-- Track violations if over limit\nlocal banned = 0\nlocal violations = 0\nif count > limit then\n violations = redis.call('INCR', violationKey)\n local vTTL = redis.call('PTTL', violationKey)\n if vTTL < 0 then\n redis.call('PEXPIRE', violationKey, banDurationMs)\n end\n if violations >= banThreshold then\n redis.call('SET', banKey, '1', 'PX', banDurationMs)\n banned = 1\n end\nend\nreturn {count, ttl, banned, violations}\n";
8
+ export declare class RedisStore implements HitLimitStore {
9
+ private redis;
10
+ private prefix;
11
+ private banPrefix;
12
+ private violationPrefix;
13
+ constructor(options?: RedisStoreOptions);
14
+ hit(key: string, windowMs: number, _limit: number): Promise<StoreResult>;
15
+ hitWithBan(key: string, windowMs: number, limit: number, banThreshold: number, banDurationMs: number): Promise<HitWithBanResult>;
16
+ isBanned(key: string): Promise<boolean>;
17
+ ban(key: string, durationMs: number): Promise<void>;
18
+ recordViolation(key: string, windowMs: number): Promise<number>;
19
+ reset(key: string): Promise<void>;
20
+ shutdown(): Promise<void>;
21
+ }
6
22
  export declare function redisStore(options?: RedisStoreOptions): HitLimitStore;
7
23
  //# sourceMappingURL=redis.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../src/stores/redis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,2BAA2B,CAAA;AAG7F,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAmJD,wBAAgB,UAAU,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,aAAa,CAErE"}
1
+ {"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../src/stores/redis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAG7F,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAGD,eAAO,MAAM,UAAU,iPAUtB,CAAA;AAGD,eAAO,MAAM,mBAAmB,s9BAsC/B,CAAA;AAED,qBAAa,UAAW,YAAW,aAAa;IAC9C,OAAO,CAAC,KAAK,CAAO;IACpB,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,eAAe,CAAQ;gBAEnB,OAAO,GAAE,iBAAsB;IAiBrC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IASxE,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA4BhI,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKvC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAS/D,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQjC,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAGhC;AAED,wBAAgB,UAAU,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,aAAa,CAErE"}
@@ -124,5 +124,8 @@ function redisStore(options) {
124
124
  return new RedisStore(options);
125
125
  }
126
126
  export {
127
- redisStore
127
+ redisStore,
128
+ RedisStore,
129
+ HIT_WITH_BAN_SCRIPT,
130
+ HIT_SCRIPT
128
131
  };
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/stores/sqlite.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAA;AAE3E,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAgHD,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,aAAa,CAEvE"}
1
+ {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/stores/sqlite.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAA;AAE3E,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAiHD,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,aAAa,CAEvE"}
@@ -17,6 +17,7 @@ class BunSqliteStore {
17
17
  cleanupTimer;
18
18
  constructor(options = {}) {
19
19
  this.db = new Database(options.path ?? ":memory:");
20
+ this.db.exec("PRAGMA journal_mode = WAL");
20
21
  this.db.exec(`
21
22
  CREATE TABLE IF NOT EXISTS hitlimit (
22
23
  key TEXT PRIMARY KEY,
@@ -0,0 +1,9 @@
1
+ import type { HitLimitStore } from '@joint-ops/hitlimit-types';
2
+ export interface ValkeyStoreOptions {
3
+ /** Valkey connection URL. Default: 'redis://localhost:6379' */
4
+ url?: string;
5
+ /** Key prefix for all rate limit keys. Default: 'hitlimit:' */
6
+ keyPrefix?: string;
7
+ }
8
+ export declare function valkeyStore(options?: ValkeyStoreOptions): HitLimitStore;
9
+ //# sourceMappingURL=valkey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"valkey.d.ts","sourceRoot":"","sources":["../../src/stores/valkey.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAG9D,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,aAAa,CAEvE"}
@@ -0,0 +1,133 @@
1
+ // @bun
2
+ // src/stores/redis.ts
3
+ import Redis from "ioredis";
4
+ var HIT_SCRIPT = `
5
+ local key = KEYS[1]
6
+ local windowMs = tonumber(ARGV[1])
7
+ local count = redis.call('INCR', key)
8
+ local ttl = redis.call('PTTL', key)
9
+ if ttl < 0 then
10
+ redis.call('PEXPIRE', key, windowMs)
11
+ ttl = windowMs
12
+ end
13
+ return {count, ttl}
14
+ `;
15
+ var HIT_WITH_BAN_SCRIPT = `
16
+ local hitKey = KEYS[1]
17
+ local banKey = KEYS[2]
18
+ local violationKey = KEYS[3]
19
+ local windowMs = tonumber(ARGV[1])
20
+ local limit = tonumber(ARGV[2])
21
+ local banThreshold = tonumber(ARGV[3])
22
+ local banDurationMs = tonumber(ARGV[4])
23
+
24
+ -- Check ban first
25
+ local banTTL = redis.call('PTTL', banKey)
26
+ if banTTL > 0 then
27
+ return {-1, banTTL, 1, 0}
28
+ end
29
+
30
+ -- Hit counter
31
+ local count = redis.call('INCR', hitKey)
32
+ local ttl = redis.call('PTTL', hitKey)
33
+ if ttl < 0 then
34
+ redis.call('PEXPIRE', hitKey, windowMs)
35
+ ttl = windowMs
36
+ end
37
+
38
+ -- Track violations if over limit
39
+ local banned = 0
40
+ local violations = 0
41
+ if count > limit then
42
+ violations = redis.call('INCR', violationKey)
43
+ local vTTL = redis.call('PTTL', violationKey)
44
+ if vTTL < 0 then
45
+ redis.call('PEXPIRE', violationKey, banDurationMs)
46
+ end
47
+ if violations >= banThreshold then
48
+ redis.call('SET', banKey, '1', 'PX', banDurationMs)
49
+ banned = 1
50
+ end
51
+ end
52
+ return {count, ttl, banned, violations}
53
+ `;
54
+
55
+ class RedisStore {
56
+ redis;
57
+ prefix;
58
+ banPrefix;
59
+ violationPrefix;
60
+ constructor(options = {}) {
61
+ this.redis = new Redis(options.url ?? "redis://localhost:6379");
62
+ this.prefix = options.keyPrefix ?? "hitlimit:";
63
+ this.banPrefix = (options.keyPrefix ?? "hitlimit:") + "ban:";
64
+ this.violationPrefix = (options.keyPrefix ?? "hitlimit:") + "violations:";
65
+ this.redis.defineCommand("hitlimitHit", {
66
+ numberOfKeys: 1,
67
+ lua: HIT_SCRIPT
68
+ });
69
+ this.redis.defineCommand("hitlimitHitWithBan", {
70
+ numberOfKeys: 3,
71
+ lua: HIT_WITH_BAN_SCRIPT
72
+ });
73
+ }
74
+ async hit(key, windowMs, _limit) {
75
+ const redisKey = this.prefix + key;
76
+ const result = await this.redis.hitlimitHit(redisKey, windowMs);
77
+ const count = result[0];
78
+ const ttl = result[1];
79
+ return { count, resetAt: Date.now() + ttl };
80
+ }
81
+ async hitWithBan(key, windowMs, limit, banThreshold, banDurationMs) {
82
+ const hitKey = this.prefix + key;
83
+ const banKey = this.banPrefix + key;
84
+ const violationKey = this.violationPrefix + key;
85
+ const result = await this.redis.hitlimitHitWithBan(hitKey, banKey, violationKey, windowMs, limit, banThreshold, banDurationMs);
86
+ const count = result[0];
87
+ const ttl = result[1];
88
+ const banned = result[2] === 1;
89
+ const violations = result[3];
90
+ if (count === -1) {
91
+ return { count: 0, resetAt: Date.now() + ttl, banned: true, violations: 0, banExpiresAt: Date.now() + ttl };
92
+ }
93
+ return {
94
+ count,
95
+ resetAt: Date.now() + ttl,
96
+ banned,
97
+ violations,
98
+ banExpiresAt: banned ? Date.now() + banDurationMs : 0
99
+ };
100
+ }
101
+ async isBanned(key) {
102
+ const result = await this.redis.exists(this.banPrefix + key);
103
+ return result === 1;
104
+ }
105
+ async ban(key, durationMs) {
106
+ await this.redis.set(this.banPrefix + key, "1", "PX", durationMs);
107
+ }
108
+ async recordViolation(key, windowMs) {
109
+ const redisKey = this.violationPrefix + key;
110
+ const count = await this.redis.incr(redisKey);
111
+ if (count === 1) {
112
+ await this.redis.pexpire(redisKey, windowMs);
113
+ }
114
+ return count;
115
+ }
116
+ async reset(key) {
117
+ await this.redis.del(this.prefix + key, this.banPrefix + key, this.violationPrefix + key);
118
+ }
119
+ async shutdown() {
120
+ await this.redis.quit();
121
+ }
122
+ }
123
+ function redisStore(options) {
124
+ return new RedisStore(options);
125
+ }
126
+
127
+ // src/stores/valkey.ts
128
+ function valkeyStore(options) {
129
+ return new RedisStore(options);
130
+ }
131
+ export {
132
+ valkeyStore
133
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joint-ops/hitlimit-bun",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Ultra-fast Bun-native rate limiting - Memory-first with 6M+ ops/sec for Bun.serve, Elysia & Hono",
5
5
  "author": {
6
6
  "name": "Shayan M Hussain",
@@ -23,6 +23,14 @@
23
23
  {
24
24
  "name": "sultandilaram",
25
25
  "url": "https://github.com/sultandilaram"
26
+ },
27
+ {
28
+ "name": "MoizYousuf",
29
+ "url": "https://github.com/MoizYousuf"
30
+ },
31
+ {
32
+ "name": "shoaib-tumi",
33
+ "url": "https://github.com/shoaib-tumi"
26
34
  }
27
35
  ],
28
36
  "license": "MIT",
@@ -86,7 +94,20 @@
86
94
  "bun-framework",
87
95
  "lightweight",
88
96
  "zero-dependency",
89
- "esm"
97
+ "esm",
98
+ "valkey",
99
+ "valkey-rate-limit",
100
+ "dragonfly",
101
+ "dragonflydb",
102
+ "dragonfly-rate-limit",
103
+ "redis-alternative",
104
+ "mongodb",
105
+ "mongodb-rate-limit",
106
+ "mongoose-rate-limit",
107
+ "mysql",
108
+ "mysql-rate-limit",
109
+ "mysql2-rate-limit",
110
+ "database-rate-limit"
90
111
  ],
91
112
  "type": "module",
92
113
  "main": "./dist/index.js",
@@ -127,6 +148,26 @@
127
148
  "types": "./dist/stores/postgres.d.ts",
128
149
  "bun": "./dist/stores/postgres.js",
129
150
  "import": "./dist/stores/postgres.js"
151
+ },
152
+ "./stores/valkey": {
153
+ "types": "./dist/stores/valkey.d.ts",
154
+ "bun": "./dist/stores/valkey.js",
155
+ "import": "./dist/stores/valkey.js"
156
+ },
157
+ "./stores/dragonfly": {
158
+ "types": "./dist/stores/dragonfly.d.ts",
159
+ "bun": "./dist/stores/dragonfly.js",
160
+ "import": "./dist/stores/dragonfly.js"
161
+ },
162
+ "./stores/mongodb": {
163
+ "types": "./dist/stores/mongodb.d.ts",
164
+ "bun": "./dist/stores/mongodb.js",
165
+ "import": "./dist/stores/mongodb.js"
166
+ },
167
+ "./stores/mysql": {
168
+ "types": "./dist/stores/mysql.d.ts",
169
+ "bun": "./dist/stores/mysql.js",
170
+ "import": "./dist/stores/mysql.js"
130
171
  }
131
172
  },
132
173
  "files": [
@@ -134,18 +175,20 @@
134
175
  ],
135
176
  "sideEffects": false,
136
177
  "scripts": {
137
- "build": "bun build ./src/index.ts ./src/elysia.ts ./src/hono.ts ./src/stores/memory.ts ./src/stores/redis.ts ./src/stores/sqlite.ts ./src/stores/postgres.ts --outdir=./dist --target=bun --external=elysia --external=hono --external=ioredis --external=pg --external=@sinclair/typebox && tsc --emitDeclarationOnly",
178
+ "build": "bun build ./src/index.ts ./src/elysia.ts ./src/hono.ts ./src/stores/memory.ts ./src/stores/redis.ts ./src/stores/sqlite.ts ./src/stores/postgres.ts ./src/stores/valkey.ts ./src/stores/dragonfly.ts ./src/stores/mongodb.ts ./src/stores/mysql.ts --outdir=./dist --root=./src --target=bun --external=elysia --external=hono --external=ioredis --external=pg --external=mongodb --external=mysql2 --external=@sinclair/typebox && tsc --emitDeclarationOnly",
138
179
  "clean": "rm -rf dist",
139
180
  "test": "bun test",
140
181
  "test:watch": "bun test --watch"
141
182
  },
142
183
  "dependencies": {
143
- "@joint-ops/hitlimit-types": "1.2.0"
184
+ "@joint-ops/hitlimit-types": "1.4.0"
144
185
  },
145
186
  "peerDependencies": {
146
187
  "elysia": ">=1.0.0",
147
188
  "hono": ">=4.0.0",
148
189
  "ioredis": ">=5.0.0",
190
+ "mongodb": ">=6.0.0",
191
+ "mysql2": ">=3.0.0",
149
192
  "pg": ">=8.0.0"
150
193
  },
151
194
  "peerDependenciesMeta": {
@@ -158,6 +201,12 @@
158
201
  "ioredis": {
159
202
  "optional": true
160
203
  },
204
+ "mongodb": {
205
+ "optional": true
206
+ },
207
+ "mysql2": {
208
+ "optional": true
209
+ },
161
210
  "pg": {
162
211
  "optional": true
163
212
  }
@@ -169,6 +218,8 @@
169
218
  "elysia": "^1.0.0",
170
219
  "hono": "^4.11.9",
171
220
  "ioredis": "^5.3.0",
221
+ "mongodb": "^7.1.0",
222
+ "mysql2": "^3.18.2",
172
223
  "pg": "^8.13.0",
173
224
  "typescript": "^5.3.0"
174
225
  }