@joint-ops/hitlimit-bun 1.1.2 → 1.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
@@ -1,61 +1,40 @@
1
1
  # @joint-ops/hitlimit-bun
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@joint-ops/hitlimit-bun.svg)](https://www.npmjs.com/package/@joint-ops/hitlimit-bun)
4
- [![npm downloads](https://img.shields.io/npm/dm/@joint-ops/hitlimit-bun.svg)](https://www.npmjs.com/package/@joint-ops/hitlimit-bun)
5
- [![GitHub](https://img.shields.io/github/license/JointOps/hitlimit-monorepo)](https://github.com/JointOps/hitlimit-monorepo)
6
- [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
7
- [![Bun](https://img.shields.io/badge/Bun-Native-black.svg)](https://bun.sh)
3
+ > Rate limiting built for Bun. Not ported — built.
8
4
 
9
- > The fastest rate limiter for Bun - 5M+ ops/sec under real-world load | Elysia, Hono & Bun.serve
10
-
11
- **hitlimit-bun** is a blazing-fast, Bun-native rate limiting library for Bun.serve, Elysia, and Hono applications. **Memory-first by default** with 5.62M ops/sec under real-world load (~15x faster than SQLite). Optional persistence with native bun:sqlite or Redis when you need it.
12
-
13
- **[Documentation](https://hitlimit.jointops.dev/docs/bun)** | **[GitHub](https://github.com/JointOps/hitlimit-monorepo)** | **[npm](https://www.npmjs.com/package/@joint-ops/hitlimit-bun)**
14
-
15
- ## ⚡ Why hitlimit-bun?
16
-
17
- **Memory-first for maximum performance.** ~15x faster than SQLite.
5
+ **12.38M ops/sec** on memory. **8.32M at 10K IPs**. Native bun:sqlite. Atomic Redis Lua. Postgres. Zero dependencies.
18
6
 
7
+ ```bash
8
+ bun add @joint-ops/hitlimit-bun
19
9
  ```
20
- ┌─────────────────────────────────────────────────────────────────┐
21
- │ │
22
- │ Memory (v1.1+) ██████████████████████████████ 5.62M ops/s │
23
- SQLite (v1.0) █░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 383K ops/s │
24
- │ │
25
- │ ~15x performance improvement with memory default (10K IPs) │
26
- │ │
27
- └─────────────────────────────────────────────────────────────────┘
10
+
11
+ ```typescript
12
+ Bun.serve({
13
+ fetch: hitlimit({}, (req) => new Response('Hello!'))
14
+ })
28
15
  ```
29
16
 
30
- - **🚀 Memory-First** - 5.62M ops/sec under real-world load (v1.1+), ~15x faster than SQLite
31
- - **Bun Native** - Built specifically for Bun's runtime, not a Node.js port
32
- - **Zero Config** - Works out of the box with sensible defaults
33
- - **Framework Support** - First-class Elysia and Hono integration
34
- - **Optional Persistence** - SQLite (383K ops/sec) or Redis (6.6K ops/sec) when needed
35
- - **TypeScript First** - Full type safety and IntelliSense support
36
- - **Auto-Ban** - Automatically ban repeat offenders after threshold violations
37
- - **Shared Limits** - Group rate limits via groupId for teams/tenants
38
- - **Tiny Footprint** - ~18KB core bundle, zero runtime dependencies
17
+ One line. Done. Works with **Bun.serve**, **Elysia**, and **Hono** out of the box.
39
18
 
40
- ## Installation
19
+ **[Docs](https://hitlimit.jointops.dev/docs/bun)** · **[GitHub](https://github.com/JointOps/hitlimit-monorepo)** · **[Benchmarks](https://github.com/JointOps/hitlimit-monorepo/tree/main/benchmarks)**
41
20
 
42
- ```bash
43
- bun add @joint-ops/hitlimit-bun
44
- ```
21
+ ---
45
22
 
46
- ## Quick Start
23
+ ## 30 Seconds to Production
47
24
 
48
- ### Bun.serve Rate Limiting
25
+ ### Bun.serve
49
26
 
50
27
  ```typescript
51
28
  import { hitlimit } from '@joint-ops/hitlimit-bun'
52
29
 
53
30
  Bun.serve({
54
- fetch: hitlimit({}, (req) => new Response('Hello!'))
31
+ fetch: hitlimit({ limit: 100, window: '1m' }, (req) => {
32
+ return new Response('Hello!')
33
+ })
55
34
  })
56
35
  ```
57
36
 
58
- ### Elysia Rate Limiting
37
+ ### Elysia
59
38
 
60
39
  ```typescript
61
40
  import { Elysia } from 'elysia'
@@ -63,380 +42,116 @@ import { hitlimit } from '@joint-ops/hitlimit-bun/elysia'
63
42
 
64
43
  new Elysia()
65
44
  .use(hitlimit({ limit: 100, window: '1m' }))
66
- .get('/', () => 'Hello World!')
45
+ .get('/', () => 'Hello!')
67
46
  .listen(3000)
68
47
  ```
69
48
 
70
- ### Hono Rate Limiting
49
+ ### Hono
71
50
 
72
51
  ```typescript
73
52
  import { Hono } from 'hono'
74
53
  import { hitlimit } from '@joint-ops/hitlimit-bun/hono'
75
54
 
76
55
  const app = new Hono()
77
-
78
56
  app.use(hitlimit({ limit: 100, window: '1m' }))
79
- app.get('/', (c) => c.text('Hello Bun!'))
80
-
57
+ app.get('/', (c) => c.text('Hello!'))
81
58
  Bun.serve({ port: 3000, fetch: app.fetch })
82
59
  ```
83
60
 
84
- ### Using createHitLimit
61
+ ---
85
62
 
86
- ```typescript
87
- import { createHitLimit } from '@joint-ops/hitlimit-bun'
88
-
89
- const limiter = createHitLimit({ limit: 100, window: '1m' })
63
+ ## What You Get
90
64
 
91
- Bun.serve({
92
- async fetch(req, server) {
93
- // Returns a 429 Response if blocked, or null if allowed
94
- const blocked = await limiter.check(req, server)
95
- if (blocked) return blocked
96
-
97
- return new Response('Hello!')
98
- }
99
- })
100
- ```
101
-
102
- ## Features
103
-
104
- ### API Rate Limiting
105
-
106
- Protect your Bun APIs from abuse with high-performance rate limiting.
107
-
108
- ```typescript
109
- Bun.serve({
110
- fetch: hitlimit({ limit: 1000, window: '1h' }, handler)
111
- })
112
- ```
113
-
114
- ### Login & Authentication Protection
115
-
116
- Prevent brute force attacks on login endpoints.
117
-
118
- ```typescript
119
- const authLimiter = createHitLimit({ limit: 5, window: '15m' })
120
-
121
- Bun.serve({
122
- async fetch(req, server) {
123
- const url = new URL(req.url)
124
-
125
- if (url.pathname.startsWith('/auth')) {
126
- const blocked = await authLimiter.check(req, server)
127
- if (blocked) return blocked
128
- }
129
-
130
- return handler(req, server)
131
- }
132
- })
133
- ```
134
-
135
- ### Tiered Rate Limits
136
-
137
- Different limits for different user tiers (free, pro, enterprise).
65
+ **Tiered limits** — Free, Pro, Enterprise:
138
66
 
139
67
  ```typescript
140
68
  hitlimit({
141
- tiers: {
142
- free: { limit: 100, window: '1h' },
143
- pro: { limit: 5000, window: '1h' },
144
- enterprise: { limit: Infinity }
145
- },
69
+ tiers: { free: { limit: 100, window: '1h' }, pro: { limit: 5000, window: '1h' } },
146
70
  tier: (req) => req.headers.get('x-tier') || 'free'
147
71
  }, handler)
148
72
  ```
149
73
 
150
- ### Custom Rate Limit Keys
151
-
152
- Rate limit by IP address, user ID, API key, or any custom identifier.
74
+ **Auto-ban** Repeat offenders get blocked:
153
75
 
154
76
  ```typescript
155
- hitlimit({
156
- key: (req) => req.headers.get('x-api-key') || 'anonymous'
157
- }, handler)
77
+ hitlimit({ limit: 10, window: '1m', ban: { threshold: 5, duration: '1h' } }, handler)
158
78
  ```
159
79
 
160
- ### Auto-Ban Repeat Offenders
161
-
162
- Automatically ban clients that repeatedly exceed rate limits.
80
+ **Custom keys** Rate limit by anything:
163
81
 
164
82
  ```typescript
165
- hitlimit({
166
- limit: 10,
167
- window: '1m',
168
- ban: {
169
- threshold: 5, // Ban after 5 violations
170
- duration: '1h' // Ban lasts 1 hour
171
- }
172
- }, handler)
83
+ hitlimit({ key: (req) => req.headers.get('x-api-key') || 'anon' }, handler)
173
84
  ```
174
85
 
175
- Banned clients receive `X-RateLimit-Ban: true` header and `banned: true` in the response body.
176
-
177
- ### Grouped / Shared Limits
178
-
179
- Rate limit by organization, API key, or any shared identifier.
180
-
181
- ```typescript
182
- // Per-API-key rate limiting
183
- hitlimit({
184
- limit: 1000,
185
- window: '1h',
186
- group: (req) => req.headers.get('x-api-key') || 'anonymous'
187
- }, handler)
188
- ```
189
-
190
- ### Elysia Route-Specific Limits
191
-
192
- Apply different limits to different route groups in Elysia.
86
+ **Route-specific limits** (Elysia):
193
87
 
194
88
  ```typescript
195
89
  new Elysia()
196
- // Global limit
197
90
  .use(hitlimit({ limit: 100, window: '1m', name: 'global' }))
198
-
199
- // Stricter limit for auth
200
- .group('/auth', (app) =>
201
- app
202
- .use(hitlimit({ limit: 5, window: '15m', name: 'auth' }))
203
- .post('/login', handler)
204
- )
205
-
206
- // Higher limit for API
207
- .group('/api', (app) =>
208
- app
209
- .use(hitlimit({ limit: 1000, window: '1m', name: 'api' }))
210
- .get('/data', handler)
211
- )
91
+ .group('/auth', app => app.use(hitlimit({ limit: 5, window: '15m', name: 'auth' })))
212
92
  .listen(3000)
213
93
  ```
214
94
 
215
- ## Configuration Options
216
-
217
- ```typescript
218
- hitlimit({
219
- // Basic options
220
- limit: 100, // Max requests per window (default: 100)
221
- window: '1m', // Time window: 30s, 15m, 1h, 1d (default: '1m')
222
-
223
- // Custom key extraction
224
- key: (req) => req.headers.get('x-api-key') || 'anonymous',
225
-
226
- // Tiered rate limits
227
- tiers: {
228
- free: { limit: 100, window: '1h' },
229
- pro: { limit: 5000, window: '1h' },
230
- enterprise: { limit: Infinity }
231
- },
232
- tier: (req) => req.headers.get('x-tier') || 'free',
233
-
234
- // Custom 429 response
235
- response: {
236
- message: 'Too many requests',
237
- statusCode: 429
238
- },
239
- // Or function:
240
- response: (info) => ({
241
- error: 'RATE_LIMITED',
242
- retryIn: info.resetIn
243
- }),
244
-
245
- // Headers configuration
246
- headers: {
247
- standard: true, // RateLimit-* headers
248
- legacy: true, // X-RateLimit-* headers
249
- retryAfter: true // Retry-After header on 429
250
- },
251
-
252
- // Store (default: memory)
253
- store: sqliteStore({ path: './ratelimit.db' }),
254
-
255
- // Skip rate limiting
256
- skip: (req) => req.url.includes('/health'),
257
-
258
- // Error handling
259
- onStoreError: (error, req) => {
260
- console.error('Store error:', error)
261
- return 'allow' // or 'deny'
262
- },
263
-
264
- // Ban repeat offenders
265
- ban: {
266
- threshold: 5, // violations before ban
267
- duration: '1h' // ban duration
268
- },
269
-
270
- // Group/shared limits
271
- group: (req) => req.headers.get('x-api-key') || 'default'
272
- }, handler)
273
- ```
95
+ ---
274
96
 
275
- ## Storage Backends
97
+ ## 4 Storage Backends
276
98
 
277
- ### Memory Store (Default)
278
-
279
- Fastest option, used by default. No persistence.
99
+ All built in. No extra packages to install.
280
100
 
281
101
  ```typescript
282
102
  import { hitlimit } from '@joint-ops/hitlimit-bun'
283
103
 
284
- // Default - uses memory store (no config needed)
285
- Bun.serve({
286
- fetch: hitlimit({}, handler)
287
- })
288
- ```
104
+ // Memory (default) fastest, no config
105
+ Bun.serve({ fetch: hitlimit({}, handler) })
289
106
 
290
- ### SQLite Store
291
-
292
- Uses Bun's native bun:sqlite for persistent rate limiting.
293
-
294
- ```typescript
295
- import { hitlimit } from '@joint-ops/hitlimit-bun'
107
+ // bun:sqlite — persists across restarts, native performance
296
108
  import { sqliteStore } from '@joint-ops/hitlimit-bun'
109
+ Bun.serve({ fetch: hitlimit({ store: sqliteStore({ path: './ratelimit.db' }) }, handler) })
297
110
 
298
- Bun.serve({
299
- fetch: hitlimit({
300
- store: sqliteStore({ path: './ratelimit.db' })
301
- }, handler)
302
- })
303
- ```
304
-
305
- ### Redis Store
306
-
307
- For distributed systems and multi-server deployments.
308
-
309
- ```typescript
310
- import { hitlimit } from '@joint-ops/hitlimit-bun'
111
+ // Redis — distributed, atomic Lua scripts
311
112
  import { redisStore } from '@joint-ops/hitlimit-bun/stores/redis'
113
+ Bun.serve({ fetch: hitlimit({ store: redisStore({ url: 'redis://localhost:6379' }) }, handler) })
312
114
 
313
- Bun.serve({
314
- fetch: hitlimit({
315
- store: redisStore({ url: 'redis://localhost:6379' })
316
- }, handler)
317
- })
318
- ```
319
-
320
- ## Response Headers
321
-
322
- Every response includes rate limit information:
323
-
324
- ```
325
- RateLimit-Limit: 100
326
- RateLimit-Remaining: 99
327
- RateLimit-Reset: 1234567890
328
- X-RateLimit-Limit: 100
329
- X-RateLimit-Remaining: 99
330
- X-RateLimit-Reset: 1234567890
331
- ```
332
-
333
- When rate limited (429 Too Many Requests):
334
-
335
- ```
336
- Retry-After: 42
115
+ // Postgres — distributed, atomic upserts
116
+ import { postgresStore } from '@joint-ops/hitlimit-bun/stores/postgres'
117
+ Bun.serve({ fetch: hitlimit({ store: postgresStore({ url: 'postgres://localhost:5432/mydb' }) }, handler) })
337
118
  ```
338
119
 
339
- ## Default 429 Response
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 |
340
126
 
341
- ```json
342
- {
343
- "hitlimit": true,
344
- "message": "Whoa there! Rate limit exceeded.",
345
- "limit": 100,
346
- "remaining": 0,
347
- "resetIn": 42
348
- }
349
- ```
127
+ ---
350
128
 
351
129
  ## Performance
352
130
 
353
- hitlimit-bun is optimized for Bun's runtime with native performance:
354
-
355
- ### Store Benchmarks (Bun 1.3)
131
+ ### Bun vs Node.js Memory Store, 10K unique IPs
356
132
 
357
- | Store | Operations/sec | vs Node.js |
358
- |-------|----------------|------------|
359
- | **Memory** | 5,620,000+ | +68% faster |
360
- | **bun:sqlite** | 383,000+ | ~same |
361
- | **Redis** | 6,600+ | ~same |
133
+ | Runtime | Ops/sec | |
134
+ |---------|---------|---|
135
+ | **Bun** | **8,320,000** | ████████████████████ |
136
+ | Node.js | 3,160,000 | ████████ |
362
137
 
363
- ### HTTP Throughput
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.
364
139
 
365
- | Framework | With hitlimit-bun | Overhead |
366
- |-----------|-------------------|----------|
367
- | **Bun.serve** | 105,000 req/s | 12% |
368
- | **Elysia** | 115,000 req/s | 11% |
369
-
370
- > **Note:** Benchmark results vary by hardware and environment. Run your own benchmarks to see results on your specific setup.
371
-
372
- ### Why bun:sqlite is So Fast
140
+ ### Why bun:sqlite is faster than better-sqlite3
373
141
 
374
142
  ```
375
- Node.js (better-sqlite3) Bun (bun:sqlite)
376
- ───────────────────────── ─────────────────
377
- JavaScript JavaScript
378
- ↓ ↓
379
- N-API Direct Call
380
- ↓ ↓
381
- C++ Binding Native SQLite
382
- ↓ (No overhead!)
383
- SQLite
143
+ Node.js: JS → N-API → C++ binding → SQLite
144
+ Bun: JS → Native call → SQLite (no overhead)
384
145
  ```
385
146
 
386
- better-sqlite3 uses N-API bindings with C++ overhead.
387
- bun:sqlite calls SQLite directly from Bun's native layer.
147
+ No N-API. No C++ bindings. No FFI. Bun calls SQLite directly.
388
148
 
389
- <details>
390
- <summary>Run benchmarks yourself</summary>
149
+ ---
391
150
 
392
- ```bash
393
- git clone https://github.com/JointOps/hitlimit-monorepo
394
- cd hitlimit-monorepo
395
- bun install
396
- bun run benchmark:bun
397
- ```
151
+ ## Related
398
152
 
399
- </details>
400
-
401
- ## Elysia Plugin Options
402
-
403
- ```typescript
404
- import { Elysia } from 'elysia'
405
- import { hitlimit } from '@joint-ops/hitlimit-bun/elysia'
406
-
407
- new Elysia()
408
- .use(hitlimit({
409
- limit: 100,
410
- window: '1m',
411
- key: ({ request }) => request.headers.get('x-api-key') || 'anonymous',
412
- tiers: {
413
- free: { limit: 100, window: '1h' },
414
- pro: { limit: 5000, window: '1h' }
415
- },
416
- tier: ({ request }) => request.headers.get('x-tier') || 'free'
417
- }))
418
- .get('/', () => 'Hello!')
419
- .listen(3000)
420
- ```
421
-
422
- ## Related Packages
423
-
424
- - [@joint-ops/hitlimit](https://www.npmjs.com/package/@joint-ops/hitlimit) - Node.js rate limiting for Express, NestJS
425
-
426
- ## Why Not Use Node.js Rate Limiters in Bun?
427
-
428
- Node.js rate limiters like express-rate-limit use better-sqlite3 which relies on N-API bindings. In Bun, this adds overhead and loses the performance benefits of Bun's native runtime.
429
-
430
- **hitlimit-bun** is built specifically for Bun:
431
- - Uses native `bun:sqlite` (no N-API overhead)
432
- - No FFI overhead or Node.js polyfills
433
- - First-class Elysia framework support
434
- - Optimized for Bun.serve's request handling
153
+ - **[@joint-ops/hitlimit](https://www.npmjs.com/package/@joint-ops/hitlimit)** — Node.js variant for Express, Fastify, Hono, NestJS
435
154
 
436
155
  ## License
437
156
 
438
- MIT - Use freely in personal and commercial projects.
439
-
440
- ## Keywords
441
-
442
- bun rate limit, bun rate limiter, bun middleware, bun api, bun server, bun serve, bun framework, bun native, bun sqlite, elysia rate limit, elysia plugin, elysia middleware, elysia throttle, elysia framework, api rate limiting, throttle requests, request throttling, bun api protection, ddos protection, brute force protection, login protection, redis rate limit, high performance rate limit, fast rate limiter, sliding window, fixed window, rate-limiter-flexible bun, express-rate-limit bun, bun http, bun backend, bun rest api
157
+ MIT
@@ -1 +1 @@
1
- {"version":3,"file":"limiter.d.ts","sourceRoot":"","sources":["../../src/core/limiter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,cAAc,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAM7F,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;CAChB;AAsCD,wBAAsB,cAAc,CAAC,QAAQ,EAC3C,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,EAChC,GAAG,EAAE,QAAQ,GACZ,OAAO,CAAC,UAAU,CAAC,CA8CrB;AAGD,wBAAsB,UAAU,CAAC,QAAQ,EACvC,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,EAChC,GAAG,EAAE,QAAQ,GACZ,OAAO,CAAC,cAAc,CAAC,CA6EzB"}
1
+ {"version":3,"file":"limiter.d.ts","sourceRoot":"","sources":["../../src/core/limiter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,cAAc,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAM7F,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;CAChB;AAsCD,wBAAsB,cAAc,CAAC,QAAQ,EAC3C,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,EAChC,GAAG,EAAE,QAAQ,GACZ,OAAO,CAAC,UAAU,CAAC,CAgErB;AAGD,wBAAsB,UAAU,CAAC,QAAQ,EACvC,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,EAChC,GAAG,EAAE,QAAQ,GACZ,OAAO,CAAC,cAAc,CAAC,CA2GzB"}
package/dist/elysia.js CHANGED
@@ -202,6 +202,49 @@ async function checkLimit(config, req) {
202
202
  tierName = await config.tier(req);
203
203
  }
204
204
  const { limit, windowMs } = resolveTier(config, tierName);
205
+ if (limit === Infinity) {
206
+ return {
207
+ allowed: true,
208
+ info: { limit, remaining: Infinity, resetIn: 0, resetAt: 0, key, tier: tierName, group: groupId },
209
+ headers: {},
210
+ body: {}
211
+ };
212
+ }
213
+ if (config.ban && config.store.hitWithBan) {
214
+ const r = await config.store.hitWithBan(key, windowMs, limit, config.ban.threshold, config.ban.durationMs);
215
+ if (r.banned && r.count === 0) {
216
+ const info3 = {
217
+ limit,
218
+ remaining: 0,
219
+ resetIn: Math.ceil((r.banExpiresAt - Date.now()) / 1000),
220
+ resetAt: r.banExpiresAt,
221
+ key,
222
+ tier: tierName,
223
+ banned: true,
224
+ banExpiresAt: r.banExpiresAt,
225
+ group: groupId
226
+ };
227
+ return { allowed: false, info: info3, headers: buildHeaders(info3, config.headers, false), body: buildBody(config.response, info3) };
228
+ }
229
+ const now2 = Date.now();
230
+ const allowed2 = r.count <= limit;
231
+ const info2 = {
232
+ limit,
233
+ remaining: Math.max(0, limit - r.count),
234
+ resetIn: Math.max(0, Math.ceil((r.resetAt - now2) / 1000)),
235
+ resetAt: r.resetAt,
236
+ key,
237
+ tier: tierName,
238
+ group: groupId
239
+ };
240
+ if (r.violations > 0)
241
+ info2.violations = r.violations;
242
+ if (r.banned) {
243
+ info2.banned = true;
244
+ info2.banExpiresAt = r.banExpiresAt;
245
+ }
246
+ return { allowed: allowed2, info: info2, headers: buildHeaders(info2, config.headers, allowed2), body: allowed2 ? {} : buildBody(config.response, info2) };
247
+ }
205
248
  if (config.ban && config.store.isBanned) {
206
249
  const banned = await config.store.isBanned(key);
207
250
  if (banned) {
@@ -225,14 +268,6 @@ async function checkLimit(config, req) {
225
268
  };
226
269
  }
227
270
  }
228
- if (limit === Infinity) {
229
- return {
230
- allowed: true,
231
- info: { limit, remaining: Infinity, resetIn: 0, resetAt: 0, key, tier: tierName, group: groupId },
232
- headers: {},
233
- body: {}
234
- };
235
- }
236
271
  const result = await config.store.hit(key, windowMs, limit);
237
272
  const now = Date.now();
238
273
  const resetIn = Math.max(0, Math.ceil((result.resetAt - now) / 1000));
package/dist/hono.js CHANGED
@@ -202,6 +202,49 @@ async function checkLimit(config, req) {
202
202
  tierName = await config.tier(req);
203
203
  }
204
204
  const { limit, windowMs } = resolveTier(config, tierName);
205
+ if (limit === Infinity) {
206
+ return {
207
+ allowed: true,
208
+ info: { limit, remaining: Infinity, resetIn: 0, resetAt: 0, key, tier: tierName, group: groupId },
209
+ headers: {},
210
+ body: {}
211
+ };
212
+ }
213
+ if (config.ban && config.store.hitWithBan) {
214
+ const r = await config.store.hitWithBan(key, windowMs, limit, config.ban.threshold, config.ban.durationMs);
215
+ if (r.banned && r.count === 0) {
216
+ const info3 = {
217
+ limit,
218
+ remaining: 0,
219
+ resetIn: Math.ceil((r.banExpiresAt - Date.now()) / 1000),
220
+ resetAt: r.banExpiresAt,
221
+ key,
222
+ tier: tierName,
223
+ banned: true,
224
+ banExpiresAt: r.banExpiresAt,
225
+ group: groupId
226
+ };
227
+ return { allowed: false, info: info3, headers: buildHeaders(info3, config.headers, false), body: buildBody(config.response, info3) };
228
+ }
229
+ const now2 = Date.now();
230
+ const allowed2 = r.count <= limit;
231
+ const info2 = {
232
+ limit,
233
+ remaining: Math.max(0, limit - r.count),
234
+ resetIn: Math.max(0, Math.ceil((r.resetAt - now2) / 1000)),
235
+ resetAt: r.resetAt,
236
+ key,
237
+ tier: tierName,
238
+ group: groupId
239
+ };
240
+ if (r.violations > 0)
241
+ info2.violations = r.violations;
242
+ if (r.banned) {
243
+ info2.banned = true;
244
+ info2.banExpiresAt = r.banExpiresAt;
245
+ }
246
+ return { allowed: allowed2, info: info2, headers: buildHeaders(info2, config.headers, allowed2), body: allowed2 ? {} : buildBody(config.response, info2) };
247
+ }
205
248
  if (config.ban && config.store.isBanned) {
206
249
  const banned = await config.store.isBanned(key);
207
250
  if (banned) {
@@ -225,14 +268,6 @@ async function checkLimit(config, req) {
225
268
  };
226
269
  }
227
270
  }
228
- if (limit === Infinity) {
229
- return {
230
- allowed: true,
231
- info: { limit, remaining: Infinity, resetIn: 0, resetAt: 0, key, tier: tierName, group: groupId },
232
- headers: {},
233
- body: {}
234
- };
235
- }
236
271
  const result = await config.store.hit(key, windowMs, limit);
237
272
  const now = Date.now();
238
273
  const resetIn = Math.max(0, Math.ceil((result.resetAt - now) / 1000));
package/dist/index.js CHANGED
@@ -199,6 +199,49 @@ async function checkLimit(config, req) {
199
199
  tierName = await config.tier(req);
200
200
  }
201
201
  const { limit, windowMs } = resolveTier(config, tierName);
202
+ if (limit === Infinity) {
203
+ return {
204
+ allowed: true,
205
+ info: { limit, remaining: Infinity, resetIn: 0, resetAt: 0, key, tier: tierName, group: groupId },
206
+ headers: {},
207
+ body: {}
208
+ };
209
+ }
210
+ if (config.ban && config.store.hitWithBan) {
211
+ const r = await config.store.hitWithBan(key, windowMs, limit, config.ban.threshold, config.ban.durationMs);
212
+ if (r.banned && r.count === 0) {
213
+ const info3 = {
214
+ limit,
215
+ remaining: 0,
216
+ resetIn: Math.ceil((r.banExpiresAt - Date.now()) / 1000),
217
+ resetAt: r.banExpiresAt,
218
+ key,
219
+ tier: tierName,
220
+ banned: true,
221
+ banExpiresAt: r.banExpiresAt,
222
+ group: groupId
223
+ };
224
+ return { allowed: false, info: info3, headers: buildHeaders(info3, config.headers, false), body: buildBody(config.response, info3) };
225
+ }
226
+ const now2 = Date.now();
227
+ const allowed2 = r.count <= limit;
228
+ const info2 = {
229
+ limit,
230
+ remaining: Math.max(0, limit - r.count),
231
+ resetIn: Math.max(0, Math.ceil((r.resetAt - now2) / 1000)),
232
+ resetAt: r.resetAt,
233
+ key,
234
+ tier: tierName,
235
+ group: groupId
236
+ };
237
+ if (r.violations > 0)
238
+ info2.violations = r.violations;
239
+ if (r.banned) {
240
+ info2.banned = true;
241
+ info2.banExpiresAt = r.banExpiresAt;
242
+ }
243
+ return { allowed: allowed2, info: info2, headers: buildHeaders(info2, config.headers, allowed2), body: allowed2 ? {} : buildBody(config.response, info2) };
244
+ }
202
245
  if (config.ban && config.store.isBanned) {
203
246
  const banned = await config.store.isBanned(key);
204
247
  if (banned) {
@@ -222,14 +265,6 @@ async function checkLimit(config, req) {
222
265
  };
223
266
  }
224
267
  }
225
- if (limit === Infinity) {
226
- return {
227
- allowed: true,
228
- info: { limit, remaining: Infinity, resetIn: 0, resetAt: 0, key, tier: tierName, group: groupId },
229
- headers: {},
230
- body: {}
231
- };
232
- }
233
268
  const result = await config.store.hit(key, windowMs, limit);
234
269
  const now = Date.now();
235
270
  const resetIn = Math.max(0, Math.ceil((result.resetAt - now) / 1000));
@@ -0,0 +1,9 @@
1
+ import type { HitLimitStore } from '@joint-ops/hitlimit-types';
2
+ export interface PostgresStoreOptions {
3
+ pool: any;
4
+ tablePrefix?: string;
5
+ cleanupInterval?: number;
6
+ skipTableCreation?: boolean;
7
+ }
8
+ export declare function postgresStore(options: PostgresStoreOptions): HitLimitStore;
9
+ //# sourceMappingURL=postgres.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/stores/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,2BAA2B,CAAA;AAE7F,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,GAAG,CAAA;IACT,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAqPD,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAE1E"}
@@ -0,0 +1,220 @@
1
+ // @bun
2
+ // src/stores/postgres.ts
3
+ class PostgresStore {
4
+ pool;
5
+ cleanupTimer = null;
6
+ tablesReady = null;
7
+ ready;
8
+ hitQ;
9
+ banCheckQ;
10
+ hitUpsertQ;
11
+ violationQ;
12
+ banSetQ;
13
+ isBannedQ;
14
+ banQ;
15
+ recordViolationQ;
16
+ resetHitsQ;
17
+ resetBansQ;
18
+ resetViolationsQ;
19
+ cleanupHitsQ;
20
+ cleanupBansQ;
21
+ cleanupViolationsQ;
22
+ constructor(options) {
23
+ this.pool = options.pool;
24
+ const t = options.tablePrefix ?? "hitlimit";
25
+ const skipTableCreation = options.skipTableCreation ?? false;
26
+ if (skipTableCreation) {
27
+ this.ready = true;
28
+ } else {
29
+ this.ready = false;
30
+ this.tablesReady = this.createTables(t).then(() => {
31
+ this.ready = true;
32
+ });
33
+ }
34
+ this.hitQ = {
35
+ name: `hl_hit_${t}`,
36
+ text: `INSERT INTO ${t}_hits (key, count, reset_at) VALUES ($1, 1, $2) ON CONFLICT (key) DO UPDATE SET count = CASE WHEN ${t}_hits.reset_at <= $3 THEN 1 ELSE ${t}_hits.count + 1 END, reset_at = CASE WHEN ${t}_hits.reset_at <= $3 THEN $2 ELSE ${t}_hits.reset_at END RETURNING count, reset_at`
37
+ };
38
+ this.banCheckQ = {
39
+ name: `hl_banchk_${t}`,
40
+ text: `SELECT expires_at FROM ${t}_bans WHERE key = $1 AND expires_at > $2`
41
+ };
42
+ this.hitUpsertQ = this.hitQ;
43
+ this.violationQ = {
44
+ name: `hl_viol_${t}`,
45
+ text: `INSERT INTO ${t}_violations (key, count, reset_at) VALUES ($1, 1, $2) ON CONFLICT (key) DO UPDATE SET count = CASE WHEN ${t}_violations.reset_at <= $3 THEN 1 ELSE ${t}_violations.count + 1 END, reset_at = CASE WHEN ${t}_violations.reset_at <= $3 THEN $2 ELSE ${t}_violations.reset_at END RETURNING count`
46
+ };
47
+ this.banSetQ = {
48
+ name: `hl_banset_${t}`,
49
+ text: `INSERT INTO ${t}_bans (key, expires_at) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET expires_at = $2`
50
+ };
51
+ this.isBannedQ = {
52
+ name: `hl_isbanned_${t}`,
53
+ text: `SELECT 1 FROM ${t}_bans WHERE key = $1 AND expires_at > $2`
54
+ };
55
+ this.banQ = this.banSetQ;
56
+ this.recordViolationQ = this.violationQ;
57
+ this.resetHitsQ = `DELETE FROM ${t}_hits WHERE key = $1`;
58
+ this.resetBansQ = `DELETE FROM ${t}_bans WHERE key = $1`;
59
+ this.resetViolationsQ = `DELETE FROM ${t}_violations WHERE key = $1`;
60
+ this.cleanupHitsQ = `DELETE FROM ${t}_hits WHERE reset_at <= $1`;
61
+ this.cleanupBansQ = `DELETE FROM ${t}_bans WHERE expires_at <= $1`;
62
+ this.cleanupViolationsQ = `DELETE FROM ${t}_violations WHERE reset_at <= $1`;
63
+ const interval = options.cleanupInterval ?? 60000;
64
+ this.cleanupTimer = setInterval(() => this.cleanup(), interval);
65
+ if (typeof this.cleanupTimer.unref === "function") {
66
+ this.cleanupTimer.unref();
67
+ }
68
+ }
69
+ async createTables(t) {
70
+ await this.pool.query(`
71
+ CREATE TABLE IF NOT EXISTS ${t}_hits (
72
+ key TEXT PRIMARY KEY,
73
+ count INTEGER NOT NULL DEFAULT 1,
74
+ reset_at BIGINT NOT NULL
75
+ )
76
+ `);
77
+ await this.pool.query(`
78
+ CREATE TABLE IF NOT EXISTS ${t}_bans (
79
+ key TEXT PRIMARY KEY,
80
+ expires_at BIGINT NOT NULL
81
+ )
82
+ `);
83
+ await this.pool.query(`
84
+ CREATE TABLE IF NOT EXISTS ${t}_violations (
85
+ key TEXT PRIMARY KEY,
86
+ count INTEGER NOT NULL DEFAULT 1,
87
+ reset_at BIGINT NOT NULL
88
+ )
89
+ `);
90
+ }
91
+ async hit(key, windowMs, _limit) {
92
+ if (!this.ready)
93
+ await this.tablesReady;
94
+ const now = Date.now();
95
+ const result = await this.pool.query({
96
+ name: this.hitQ.name,
97
+ text: this.hitQ.text,
98
+ values: [key, now + windowMs, now]
99
+ });
100
+ return {
101
+ count: result.rows[0].count,
102
+ resetAt: Number(result.rows[0].reset_at)
103
+ };
104
+ }
105
+ async hitWithBan(key, windowMs, limit, banThreshold, banDurationMs) {
106
+ if (!this.ready)
107
+ await this.tablesReady;
108
+ const now = Date.now();
109
+ const resetAt = now + windowMs;
110
+ const banExpiresAt = now + banDurationMs;
111
+ const banResult = await this.pool.query({
112
+ name: this.banCheckQ.name,
113
+ text: this.banCheckQ.text,
114
+ values: [key, now]
115
+ });
116
+ if (banResult.rowCount > 0) {
117
+ return {
118
+ count: 0,
119
+ resetAt,
120
+ banned: true,
121
+ violations: 0,
122
+ banExpiresAt: Number(banResult.rows[0].expires_at)
123
+ };
124
+ }
125
+ const hitResult = await this.pool.query({
126
+ name: this.hitUpsertQ.name,
127
+ text: this.hitUpsertQ.text,
128
+ values: [key, resetAt, now]
129
+ });
130
+ const hitCount = hitResult.rows[0].count;
131
+ const hitResetAt = Number(hitResult.rows[0].reset_at);
132
+ if (hitCount > limit) {
133
+ const violationResult = await this.pool.query({
134
+ name: this.violationQ.name,
135
+ text: this.violationQ.text,
136
+ values: [key, banExpiresAt, now]
137
+ });
138
+ const violations = violationResult.rows[0].count;
139
+ const shouldBan = violations >= banThreshold;
140
+ if (shouldBan) {
141
+ await this.pool.query({
142
+ name: this.banSetQ.name,
143
+ text: this.banSetQ.text,
144
+ values: [key, banExpiresAt]
145
+ });
146
+ }
147
+ return {
148
+ count: hitCount,
149
+ resetAt: hitResetAt,
150
+ banned: shouldBan,
151
+ violations,
152
+ banExpiresAt: shouldBan ? banExpiresAt : 0
153
+ };
154
+ }
155
+ return {
156
+ count: hitCount,
157
+ resetAt: hitResetAt,
158
+ banned: false,
159
+ violations: 0,
160
+ banExpiresAt: 0
161
+ };
162
+ }
163
+ async isBanned(key) {
164
+ if (!this.ready)
165
+ await this.tablesReady;
166
+ const result = await this.pool.query({
167
+ name: this.isBannedQ.name,
168
+ text: this.isBannedQ.text,
169
+ values: [key, Date.now()]
170
+ });
171
+ return result.rowCount > 0;
172
+ }
173
+ async ban(key, durationMs) {
174
+ if (!this.ready)
175
+ await this.tablesReady;
176
+ await this.pool.query({
177
+ name: this.banQ.name,
178
+ text: this.banQ.text,
179
+ values: [key, Date.now() + durationMs]
180
+ });
181
+ }
182
+ async recordViolation(key, windowMs) {
183
+ if (!this.ready)
184
+ await this.tablesReady;
185
+ const now = Date.now();
186
+ const result = await this.pool.query({
187
+ name: this.recordViolationQ.name,
188
+ text: this.recordViolationQ.text,
189
+ values: [key, now + windowMs, now]
190
+ });
191
+ return result.rows[0].count;
192
+ }
193
+ async reset(key) {
194
+ if (!this.ready)
195
+ await this.tablesReady;
196
+ await this.pool.query(this.resetHitsQ, [key]);
197
+ await this.pool.query(this.resetBansQ, [key]);
198
+ await this.pool.query(this.resetViolationsQ, [key]);
199
+ }
200
+ async cleanup() {
201
+ try {
202
+ const now = Date.now();
203
+ await this.pool.query(this.cleanupHitsQ, [now]);
204
+ await this.pool.query(this.cleanupBansQ, [now]);
205
+ await this.pool.query(this.cleanupViolationsQ, [now]);
206
+ } catch {}
207
+ }
208
+ shutdown() {
209
+ if (this.cleanupTimer) {
210
+ clearInterval(this.cleanupTimer);
211
+ this.cleanupTimer = null;
212
+ }
213
+ }
214
+ }
215
+ function postgresStore(options) {
216
+ return new PostgresStore(options);
217
+ }
218
+ export {
219
+ postgresStore
220
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../src/stores/redis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAA;AAG3E,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAqED,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,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,6 +1,56 @@
1
1
  // @bun
2
2
  // src/stores/redis.ts
3
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
+ `;
4
54
 
5
55
  class RedisStore {
6
56
  redis;
@@ -12,19 +62,41 @@ class RedisStore {
12
62
  this.prefix = options.keyPrefix ?? "hitlimit:";
13
63
  this.banPrefix = (options.keyPrefix ?? "hitlimit:") + "ban:";
14
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
+ });
15
73
  }
16
74
  async hit(key, windowMs, _limit) {
17
75
  const redisKey = this.prefix + key;
18
- const now = Date.now();
19
- const results = await this.redis.multi().incr(redisKey).pttl(redisKey).exec();
20
- const count = results[0][1];
21
- let ttl = results[1][1];
22
- if (ttl < 0) {
23
- await this.redis.pexpire(redisKey, windowMs);
24
- ttl = windowMs;
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 };
25
92
  }
26
- const resetAt = now + ttl;
27
- return { count, resetAt };
93
+ return {
94
+ count,
95
+ resetAt: Date.now() + ttl,
96
+ banned,
97
+ violations,
98
+ banExpiresAt: banned ? Date.now() + banDurationMs : 0
99
+ };
28
100
  }
29
101
  async isBanned(key) {
30
102
  const result = await this.redis.exists(this.banPrefix + key);
package/package.json CHANGED
@@ -1,12 +1,30 @@
1
1
  {
2
2
  "name": "@joint-ops/hitlimit-bun",
3
- "version": "1.1.2",
3
+ "version": "1.2.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",
7
7
  "email": "shayanhussain48@gmail.com",
8
8
  "url": "https://github.com/ShayanHussainSB"
9
9
  },
10
+ "contributors": [
11
+ {
12
+ "name": "tanv33",
13
+ "url": "https://github.com/tanv33"
14
+ },
15
+ {
16
+ "name": "builtbyali",
17
+ "url": "https://github.com/builtbyali"
18
+ },
19
+ {
20
+ "name": "MuhammadRehanRasool",
21
+ "url": "https://github.com/MuhammadRehanRasool"
22
+ },
23
+ {
24
+ "name": "sultandilaram",
25
+ "url": "https://github.com/sultandilaram"
26
+ }
27
+ ],
10
28
  "license": "MIT",
11
29
  "homepage": "https://hitlimit.jointops.dev/docs/bun",
12
30
  "repository": {
@@ -104,6 +122,11 @@
104
122
  "types": "./dist/stores/sqlite.d.ts",
105
123
  "bun": "./dist/stores/sqlite.js",
106
124
  "import": "./dist/stores/sqlite.js"
125
+ },
126
+ "./stores/postgres": {
127
+ "types": "./dist/stores/postgres.d.ts",
128
+ "bun": "./dist/stores/postgres.js",
129
+ "import": "./dist/stores/postgres.js"
107
130
  }
108
131
  },
109
132
  "files": [
@@ -111,18 +134,19 @@
111
134
  ],
112
135
  "sideEffects": false,
113
136
  "scripts": {
114
- "build": "bun build ./src/index.ts ./src/elysia.ts ./src/hono.ts ./src/stores/memory.ts ./src/stores/redis.ts ./src/stores/sqlite.ts --outdir=./dist --target=bun --external=elysia --external=hono --external=ioredis --external=@sinclair/typebox && tsc --emitDeclarationOnly",
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",
115
138
  "clean": "rm -rf dist",
116
139
  "test": "bun test",
117
140
  "test:watch": "bun test --watch"
118
141
  },
119
142
  "dependencies": {
120
- "@joint-ops/hitlimit-types": "1.1.2"
143
+ "@joint-ops/hitlimit-types": "1.2.0"
121
144
  },
122
145
  "peerDependencies": {
123
146
  "elysia": ">=1.0.0",
124
147
  "hono": ">=4.0.0",
125
- "ioredis": ">=5.0.0"
148
+ "ioredis": ">=5.0.0",
149
+ "pg": ">=8.0.0"
126
150
  },
127
151
  "peerDependenciesMeta": {
128
152
  "elysia": {
@@ -133,14 +157,19 @@
133
157
  },
134
158
  "ioredis": {
135
159
  "optional": true
160
+ },
161
+ "pg": {
162
+ "optional": true
136
163
  }
137
164
  },
138
165
  "devDependencies": {
139
166
  "@sinclair/typebox": "^0.34.48",
140
167
  "@types/bun": "latest",
168
+ "@types/pg": "^8.11.0",
141
169
  "elysia": "^1.0.0",
142
170
  "hono": "^4.11.9",
143
171
  "ioredis": "^5.3.0",
172
+ "pg": "^8.13.0",
144
173
  "typescript": "^5.3.0"
145
174
  }
146
175
  }