@stacksjs/bun-router 0.0.16 → 0.0.18

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.
Files changed (176) hide show
  1. package/dist/auth.d.ts +22 -8
  2. package/dist/chunk-1ahs68ys.js +18 -0
  3. package/dist/{chunk-0145g1me.js → chunk-cgptvjdf.js} +147 -75
  4. package/dist/chunk-g3ybefhg.js +1923 -0
  5. package/dist/cli.js +3 -2
  6. package/dist/container/container.d.ts +1 -0
  7. package/dist/container/contextual-binding.d.ts +0 -1
  8. package/dist/container/decorators.d.ts +2 -5
  9. package/dist/container/index.d.ts +16 -0
  10. package/dist/container/index.js +322 -0
  11. package/dist/container/service-provider.d.ts +3 -3
  12. package/dist/development/performance-profiler.d.ts +3 -3
  13. package/dist/development/route-debugger.d.ts +1 -1
  14. package/dist/development/route-inspector.d.ts +2 -2
  15. package/dist/file-serving/static-files.d.ts +16 -0
  16. package/dist/index.d.ts +17 -1
  17. package/dist/index.js +1159 -1380
  18. package/dist/middleware/cors.d.ts +9 -0
  19. package/dist/middleware/csrf.d.ts +7 -0
  20. package/dist/middleware/ddos_protection.d.ts +16 -1
  21. package/dist/middleware/rate_limit.d.ts +7 -1
  22. package/dist/middleware/request_tracer.d.ts +15 -9
  23. package/dist/middleware/response_cache.d.ts +2 -1
  24. package/dist/observability/integration.d.ts +1 -1
  25. package/dist/request/context.d.ts +12 -1
  26. package/dist/request/macros.d.ts +40 -2
  27. package/dist/response/macros.d.ts +4 -4
  28. package/dist/router/fluent-routing.d.ts +50 -13
  29. package/dist/router/handler-resolver.d.ts +8 -0
  30. package/dist/router/index.d.ts +9 -7
  31. package/dist/router/route-compiler.d.ts +8 -10
  32. package/dist/router/route-trie.d.ts +7 -1
  33. package/dist/router/router.d.ts +13 -34
  34. package/dist/router/validation-integration.d.ts +10 -4
  35. package/dist/testing/performance-testing.d.ts +2 -2
  36. package/dist/testing/test-client.d.ts +1 -1
  37. package/dist/testing/websocket-testing.d.ts +1 -1
  38. package/dist/types/middleware-types.d.ts +1 -1
  39. package/dist/types/route-inference.d.ts +9 -2
  40. package/dist/types.d.ts +62 -9
  41. package/dist/utils.d.ts +1 -1
  42. package/package.json +12 -3
  43. package/dist/router/fluent-router.d.ts +0 -315
  44. package/src/auth.ts +0 -469
  45. package/src/cache/lru-cache.ts +0 -457
  46. package/src/cache/middleware-memoization.ts +0 -531
  47. package/src/cache/route-cache-warmer.ts +0 -486
  48. package/src/cache/sqlite-cache.ts +0 -783
  49. package/src/cache/streaming-cache.ts +0 -572
  50. package/src/cli/colors.ts +0 -29
  51. package/src/cli/index.ts +0 -287
  52. package/src/cli/middleware.ts +0 -291
  53. package/src/cli/openapi.ts +0 -407
  54. package/src/cli/router.ts +0 -188
  55. package/src/cli/routes.ts +0 -265
  56. package/src/cli/utils.ts +0 -531
  57. package/src/cli.ts +0 -5
  58. package/src/config.ts +0 -322
  59. package/src/container/container.ts +0 -740
  60. package/src/container/contextual-binding.ts +0 -603
  61. package/src/container/decorators.ts +0 -359
  62. package/src/container/service-provider.ts +0 -596
  63. package/src/development/hot-reload.ts +0 -673
  64. package/src/development/index.ts +0 -499
  65. package/src/development/performance-profiler.ts +0 -717
  66. package/src/development/route-debugger.ts +0 -527
  67. package/src/development/route-inspector.ts +0 -749
  68. package/src/development/typescript-utilities.ts +0 -682
  69. package/src/docs.ts +0 -397
  70. package/src/errors/circuit-breaker.ts +0 -732
  71. package/src/errors/error-handler.ts +0 -569
  72. package/src/errors/error-reporting.ts +0 -672
  73. package/src/errors/exceptions.ts +0 -536
  74. package/src/errors/graceful-degradation.ts +0 -621
  75. package/src/errors/index.ts +0 -21
  76. package/src/errors/router-errors.ts +0 -632
  77. package/src/file-serving/static-files.ts +0 -581
  78. package/src/index.ts +0 -14
  79. package/src/middleware/auth.ts +0 -220
  80. package/src/middleware/content_security_policy.ts +0 -215
  81. package/src/middleware/cors.ts +0 -74
  82. package/src/middleware/csrf.ts +0 -118
  83. package/src/middleware/ddos_protection.ts +0 -255
  84. package/src/middleware/file_security.ts +0 -191
  85. package/src/middleware/file_upload.ts +0 -275
  86. package/src/middleware/helmet.ts +0 -268
  87. package/src/middleware/index.ts +0 -117
  88. package/src/middleware/input_validation.ts +0 -449
  89. package/src/middleware/json_body.ts +0 -37
  90. package/src/middleware/performance_alerting.ts +0 -538
  91. package/src/middleware/performance_dashboard.ts +0 -661
  92. package/src/middleware/performance_monitor.ts +0 -943
  93. package/src/middleware/pipeline.ts +0 -489
  94. package/src/middleware/rate_limit.ts +0 -245
  95. package/src/middleware/request_id.ts +0 -36
  96. package/src/middleware/request_signing.ts +0 -636
  97. package/src/middleware/request_tracer.ts +0 -636
  98. package/src/middleware/response_cache.ts +0 -743
  99. package/src/middleware/security.ts +0 -482
  100. package/src/middleware/security_suite.ts +0 -257
  101. package/src/middleware/session.ts +0 -91
  102. package/src/model-binding/index.ts +0 -18
  103. package/src/model-binding/model-middleware.ts +0 -425
  104. package/src/model-binding/model-registry.ts +0 -550
  105. package/src/model-binding.ts +0 -370
  106. package/src/model-resolver-factory.ts +0 -106
  107. package/src/observability/correlation.ts +0 -691
  108. package/src/observability/health-checks.ts +0 -729
  109. package/src/observability/index.ts +0 -184
  110. package/src/observability/integration.ts +0 -548
  111. package/src/observability/metrics.ts +0 -753
  112. package/src/observability/tracing.ts +0 -638
  113. package/src/optimization/bun-utilities.ts +0 -778
  114. package/src/query-builder-integration.ts +0 -137
  115. package/src/request/context.ts +0 -62
  116. package/src/request/enhanced-request.ts +0 -857
  117. package/src/request/macros.ts +0 -688
  118. package/src/response/macros.ts +0 -665
  119. package/src/response/response-factory.ts +0 -596
  120. package/src/router/api-routes.ts +0 -243
  121. package/src/router/file-based-routing.ts +0 -690
  122. package/src/router/file-streaming.ts +0 -383
  123. package/src/router/fluent-router.ts +0 -927
  124. package/src/router/fluent-routing.ts +0 -797
  125. package/src/router/group-organization.ts +0 -213
  126. package/src/router/handler-resolver.ts +0 -291
  127. package/src/router/http-methods.ts +0 -377
  128. package/src/router/index.ts +0 -187
  129. package/src/router/middleware-groups.ts +0 -222
  130. package/src/router/middleware-integration.ts +0 -399
  131. package/src/router/middleware.ts +0 -231
  132. package/src/router/model-binding.ts +0 -215
  133. package/src/router/optimized-route-matching.ts +0 -253
  134. package/src/router/route-building.ts +0 -221
  135. package/src/router/route-compiler.ts +0 -702
  136. package/src/router/route-matching.ts +0 -349
  137. package/src/router/route-trie.ts +0 -464
  138. package/src/router/router.ts +0 -1604
  139. package/src/router/server.ts +0 -462
  140. package/src/router/validation-integration.ts +0 -443
  141. package/src/router/view-rendering.ts +0 -233
  142. package/src/router/websocket.ts +0 -100
  143. package/src/routing/route-caching.ts +0 -402
  144. package/src/routing/route-throttling.ts +0 -469
  145. package/src/routing/subdomain-routing.ts +0 -492
  146. package/src/session/database-store.ts +0 -109
  147. package/src/session/file-store.ts +0 -148
  148. package/src/session/index.ts +0 -244
  149. package/src/session/memory-store.ts +0 -88
  150. package/src/session/redis-store.ts +0 -93
  151. package/src/streaming/index.ts +0 -17
  152. package/src/streaming/sse-handler.ts +0 -482
  153. package/src/streaming/stream-handler.ts +0 -552
  154. package/src/testing/auth-testing.ts +0 -446
  155. package/src/testing/file-upload-testing.ts +0 -543
  156. package/src/testing/index.ts +0 -10
  157. package/src/testing/middleware-testing.ts +0 -322
  158. package/src/testing/model-binding-testing.ts +0 -645
  159. package/src/testing/performance-testing.ts +0 -738
  160. package/src/testing/test-client.ts +0 -310
  161. package/src/testing/test-request.ts +0 -315
  162. package/src/testing/test-response.ts +0 -332
  163. package/src/testing/types.ts +0 -224
  164. package/src/testing/websocket-testing.ts +0 -590
  165. package/src/types/controller-types.ts +0 -385
  166. package/src/types/core.ts +0 -683
  167. package/src/types/middleware-types.ts +0 -419
  168. package/src/types/request-response-augmentation.ts +0 -489
  169. package/src/types/route-inference.ts +0 -357
  170. package/src/types.ts +0 -2067
  171. package/src/url.ts +0 -126
  172. package/src/utils/index.ts +0 -1
  173. package/src/utils/query-preservation.ts +0 -201
  174. package/src/utils.ts +0 -392
  175. package/src/validation/validator.ts +0 -685
  176. package/src/websocket/clustering.ts +0 -762
@@ -1,743 +0,0 @@
1
- import type { EnhancedRequest, Middleware, NextFunction } from '../types'
2
- import crypto from 'node:crypto'
3
- import { mkdir } from 'node:fs/promises'
4
- import { join } from 'node:path'
5
-
6
- export interface CacheEntry {
7
- key: string
8
- response: {
9
- status: number
10
- statusText: string
11
- headers: Record<string, string>
12
- body: number[] // Store as number array for JSON serialization
13
- contentType: string
14
- }
15
- createdAt: number
16
- expiresAt: number
17
- etag?: string
18
- lastModified?: string
19
- size: number
20
- }
21
-
22
- export interface ResponseCacheOptions {
23
- enabled?: boolean
24
- storage?: {
25
- type: 'memory' | 'file' | 'hybrid'
26
- directory?: string
27
- maxSize?: number
28
- maxEntries?: number
29
- }
30
- ttl?: {
31
- default: number
32
- routes?: Record<string, number>
33
- methods?: Record<string, number>
34
- }
35
- keyGenerator?: (req: EnhancedRequest) => string
36
- shouldCache?: (req: EnhancedRequest, res: Response) => boolean
37
- varyHeaders?: string[]
38
- compression?: {
39
- enabled: boolean
40
- threshold: number
41
- algorithms: ('gzip' | 'deflate' | 'br')[]
42
- }
43
- invalidation?: {
44
- patterns?: string[]
45
- methods?: string[]
46
- headers?: string[]
47
- }
48
- etag?: {
49
- enabled: boolean
50
- weak: boolean
51
- }
52
- staleWhileRevalidate?: {
53
- enabled: boolean
54
- maxAge: number
55
- }
56
- }
57
-
58
- interface MemoryCacheEntry extends CacheEntry {
59
- accessCount: number
60
- lastAccessed: number
61
- }
62
-
63
- export class ResponseCache implements Middleware {
64
- private options: Required<ResponseCacheOptions>
65
- private memoryCache = new Map<string, MemoryCacheEntry>()
66
- private cacheStats = {
67
- hits: 0,
68
- misses: 0,
69
- sets: 0,
70
- deletes: 0,
71
- size: 0,
72
- memoryUsage: 0,
73
- }
74
-
75
- private cleanupInterval?: ReturnType<typeof setInterval>
76
-
77
- constructor(options: ResponseCacheOptions = {}) {
78
- this.options = {
79
- enabled: options.enabled ?? true,
80
- storage: {
81
- type: options.storage?.type ?? 'hybrid',
82
- directory: options.storage?.directory ?? '.cache/responses',
83
- maxSize: options.storage?.maxSize ?? 100 * 1024 * 1024, // 100MB
84
- maxEntries: options.storage?.maxEntries ?? 10000,
85
- ...options.storage,
86
- },
87
- ttl: {
88
- default: options.ttl?.default ?? 300000, // 5 minutes
89
- routes: options.ttl?.routes ?? {},
90
- methods: options.ttl?.methods ?? {
91
- GET: 300000,
92
- HEAD: 300000,
93
- OPTIONS: 60000,
94
- },
95
- ...options.ttl,
96
- },
97
- keyGenerator: options.keyGenerator ?? this.defaultKeyGenerator.bind(this),
98
- shouldCache: options.shouldCache ?? this.defaultShouldCache.bind(this),
99
- varyHeaders: options.varyHeaders ?? ['Accept', 'Accept-Encoding', 'Authorization'],
100
- compression: {
101
- enabled: options.compression?.enabled ?? true,
102
- threshold: options.compression?.threshold ?? 1024,
103
- algorithms: options.compression?.algorithms ?? ['gzip', 'deflate'],
104
- ...options.compression,
105
- },
106
- invalidation: {
107
- patterns: options.invalidation?.patterns ?? [],
108
- methods: options.invalidation?.methods ?? ['POST', 'PUT', 'PATCH', 'DELETE'],
109
- headers: options.invalidation?.headers ?? ['Cache-Control'],
110
- ...options.invalidation,
111
- },
112
- etag: {
113
- enabled: options.etag?.enabled ?? true,
114
- weak: options.etag?.weak ?? true,
115
- ...options.etag,
116
- },
117
- staleWhileRevalidate: {
118
- enabled: options.staleWhileRevalidate?.enabled ?? false,
119
- maxAge: options.staleWhileRevalidate?.maxAge ?? 60000,
120
- ...options.staleWhileRevalidate,
121
- },
122
- } as Required<ResponseCacheOptions>
123
-
124
- this.initializeStorage()
125
- this.startCleanupInterval()
126
- }
127
-
128
- private async initializeStorage(): Promise<void> {
129
- if (this.options.storage.type === 'file' || this.options.storage.type === 'hybrid') {
130
- try {
131
- if (this.options.storage?.directory) {
132
- await mkdir(this.options.storage.directory, { recursive: true })
133
- }
134
- }
135
- catch (error) {
136
- console.warn(`[ResponseCache] Failed to create cache directory: ${error}`)
137
- }
138
- }
139
- }
140
-
141
- private startCleanupInterval(): void {
142
- this.cleanupInterval = setInterval(() => {
143
- void this.cleanup()
144
- }, 60000) // Cleanup every minute
145
- }
146
-
147
- public async cleanup(): Promise<void> {
148
- const now = Date.now()
149
-
150
- // Clean memory cache - check expiration properly
151
- const keysToDelete: string[] = []
152
- for (const [key, entry] of this.memoryCache.entries()) {
153
- if (entry.expiresAt <= now) {
154
- keysToDelete.push(key)
155
- }
156
- }
157
-
158
- // Delete expired keys
159
- for (const key of keysToDelete) {
160
- this.memoryCache.delete(key)
161
- this.cacheStats.deletes++
162
- }
163
-
164
- // Clean file cache if using file storage
165
- if (this.options.storage.type === 'file' || this.options.storage.type === 'hybrid') {
166
- await this.cleanupFileCache()
167
- }
168
-
169
- // Update cache stats after cleanup
170
- this.updateCacheStats()
171
- }
172
-
173
- private async cleanupFileCache(): Promise<void> {
174
- if (!this.options.storage.directory)
175
- return
176
-
177
- try {
178
- const { readdir } = await import('node:fs/promises')
179
- const files = await readdir(this.options.storage.directory)
180
-
181
- for (const file of files) {
182
- if (file.endsWith('.cache')) {
183
- const filePath = join(this.options.storage.directory, file)
184
- const bunFile = Bun.file(filePath)
185
-
186
- if (await bunFile.exists()) {
187
- try {
188
- const content = await bunFile.text()
189
- const entry: CacheEntry = JSON.parse(content)
190
-
191
- if (entry.expiresAt < Date.now()) {
192
- await bunFile.delete()
193
- this.cacheStats.deletes++
194
- }
195
- }
196
- catch {
197
- // Invalid cache file, delete it
198
- await bunFile.delete()
199
- }
200
- }
201
- }
202
- }
203
- }
204
- catch (error) {
205
- console.warn(`[ResponseCache] File cache cleanup failed: ${error}`)
206
- }
207
- }
208
-
209
- private defaultKeyGenerator(req: EnhancedRequest): string {
210
- const url = new URL(req.url)
211
- const varyParts: string[] = []
212
-
213
- // Include vary headers in key
214
- for (const header of this.options.varyHeaders) {
215
- const value = req.headers.get(header)
216
- if (value) {
217
- varyParts.push(`${header}:${value}`)
218
- }
219
- }
220
-
221
- const keyData = [
222
- req.method,
223
- url.pathname,
224
- url.search,
225
- ...varyParts,
226
- ].join('|')
227
-
228
- return crypto.createHash('sha256').update(keyData).digest('hex')
229
- }
230
-
231
- private defaultShouldCache(req: EnhancedRequest, res: Response): boolean {
232
- // Don't cache if disabled
233
- if (!this.options.enabled)
234
- return false
235
-
236
- // Don't cache non-GET/HEAD requests by default
237
- if (!['GET', 'HEAD'].includes(req.method))
238
- return false
239
-
240
- // Don't cache responses with Cache-Control: no-cache or no-store
241
- const cacheControl = res.headers.get('Cache-Control')
242
- if (cacheControl?.includes('no-cache') || cacheControl?.includes('no-store')) {
243
- return false
244
- }
245
-
246
- // Don't cache error responses (4xx, 5xx)
247
- if (res.status >= 400)
248
- return false
249
-
250
- // Don't cache responses with Set-Cookie header
251
- if (res.headers.get('Set-Cookie'))
252
- return false
253
-
254
- return true
255
- }
256
-
257
- private getTTL(req: EnhancedRequest): number {
258
- const method = req.method.toLowerCase()
259
- const url = new URL(req.url)
260
- const pathname = url.pathname
261
-
262
- // Check for route-specific TTL
263
- if (this.options.ttl?.routes) {
264
- for (const [pattern, ttl] of Object.entries(this.options.ttl.routes)) {
265
- if (pathname.includes(pattern)) {
266
- return ttl
267
- }
268
- }
269
- }
270
-
271
- // Check for method-specific TTL
272
- if (this.options.ttl?.methods?.[method]) {
273
- return this.options.ttl.methods[method]
274
- }
275
-
276
- // Return default TTL
277
- return this.options.ttl?.default || 300000 // 5 minutes default
278
- }
279
-
280
- private generateETag(content: string | Uint8Array): string {
281
- const hash = crypto.createHash('sha1')
282
- hash.update(content)
283
- const etag = hash.digest('hex').substring(0, 16)
284
- return this.options.etag.weak ? `W/"${etag}"` : `"${etag}"`
285
- }
286
-
287
- private async getFromMemoryCache(key: string): Promise<CacheEntry | null> {
288
- const entry = this.memoryCache.get(key)
289
- if (!entry)
290
- return null
291
-
292
- // Check if entry is expired
293
- const now = Date.now()
294
- if (entry.expiresAt <= now) {
295
- this.memoryCache.delete(key)
296
- this.cacheStats.deletes++
297
- return null
298
- }
299
-
300
- // Update access time for LRU
301
- entry.lastAccessed = Date.now()
302
- entry.accessCount++
303
-
304
- // Re-insert to update the entry in the map
305
- this.memoryCache.set(key, entry)
306
- return entry
307
- }
308
-
309
- private async getFromFileCache(key: string): Promise<CacheEntry | null> {
310
- if (!this.options.storage.directory)
311
- return null
312
-
313
- try {
314
- const filePath = join(this.options.storage.directory, `${key}.cache`)
315
- const file = Bun.file(filePath)
316
-
317
- if (!(await file.exists())) {
318
- return null
319
- }
320
-
321
- const data = await file.json() as CacheEntry
322
-
323
- // Check if entry is expired
324
- const now = Date.now()
325
- if (data.expiresAt <= now) {
326
- // Delete expired file
327
- try {
328
- const { unlink } = await import('node:fs/promises')
329
- await unlink(filePath)
330
- }
331
- catch (deleteError) {
332
- console.warn(`[ResponseCache] Error deleting expired file cache:`, deleteError)
333
- }
334
- return null
335
- }
336
-
337
- return data
338
- }
339
- catch (error) {
340
- console.warn(`[ResponseCache] Error reading file cache for key ${key}:`, error)
341
- return null
342
- }
343
- }
344
-
345
- private async setMemoryCache(key: string, entry: CacheEntry): Promise<void> {
346
- // If key already exists, don't add as new entry
347
- if (this.memoryCache.has(key)) {
348
- return
349
- }
350
-
351
- // Check memory limits and evict if necessary before adding new entry
352
- while (this.memoryCache.size >= (this.options.storage.maxEntries || 100)) {
353
- this.evictLRU()
354
- }
355
-
356
- const now = Date.now()
357
- const memoryEntry: MemoryCacheEntry = {
358
- ...entry,
359
- accessCount: 1,
360
- lastAccessed: now,
361
- }
362
-
363
- this.memoryCache.set(key, memoryEntry)
364
- this.updateCacheStats()
365
- }
366
-
367
- private async setFileCache(key: string, entry: CacheEntry): Promise<void> {
368
- if (!this.options.storage.directory)
369
- return
370
-
371
- try {
372
- const filePath = join(this.options.storage.directory, `${key}.cache`)
373
- const content = JSON.stringify(entry)
374
- await Bun.write(filePath, content)
375
- }
376
- catch (error) {
377
- console.warn(`[ResponseCache] Failed to write file cache: ${error}`)
378
- }
379
- }
380
-
381
- private evictLRU(): void {
382
- if (this.memoryCache.size === 0)
383
- return
384
-
385
- // Find the entry with the oldest lastAccessed time
386
- let oldestKey: string | null = null
387
- let oldestTime = Number.MAX_SAFE_INTEGER
388
-
389
- for (const [key, entry] of this.memoryCache.entries()) {
390
- if (entry.lastAccessed < oldestTime) {
391
- oldestTime = entry.lastAccessed
392
- oldestKey = key
393
- }
394
- }
395
-
396
- if (oldestKey) {
397
- this.memoryCache.delete(oldestKey)
398
- this.cacheStats.deletes++
399
- }
400
- }
401
-
402
- private updateCacheStats(): void {
403
- this.cacheStats.size = this.memoryCache.size
404
- this.cacheStats.memoryUsage = Array.from(this.memoryCache.values())
405
- .reduce((total, entry) => total + entry.size, 0)
406
- }
407
-
408
- private generateCacheKey(req: EnhancedRequest): string {
409
- return this.options.keyGenerator(req)
410
- }
411
-
412
- private async getCachedResponse(key: string): Promise<CacheEntry | null> {
413
- // Try memory cache first
414
- if (this.options.storage.type === 'memory' || this.options.storage.type === 'hybrid') {
415
- const memoryEntry = await this.getFromMemoryCache(key)
416
- if (memoryEntry) {
417
- return memoryEntry
418
- }
419
- }
420
-
421
- // Try file cache
422
- if (this.options.storage.type === 'file' || this.options.storage.type === 'hybrid') {
423
- const fileEntry = await this.getFromFileCache(key)
424
- if (fileEntry) {
425
- // Store in memory cache for faster access
426
- if (this.options.storage.type === 'hybrid') {
427
- await this.setMemoryCache(key, fileEntry)
428
- }
429
- return fileEntry
430
- }
431
- }
432
-
433
- return null
434
- }
435
-
436
- private async setCachedResponse(key: string, entry: CacheEntry): Promise<void> {
437
- // Only increment sets if this is a new entry
438
- const isNewEntry = !this.memoryCache.has(key)
439
-
440
- // Store in memory cache
441
- if (this.options.storage.type === 'memory' || this.options.storage.type === 'hybrid') {
442
- await this.setMemoryCache(key, entry)
443
- }
444
-
445
- // Store in file cache
446
- if (this.options.storage.type === 'file' || this.options.storage.type === 'hybrid') {
447
- await this.setFileCache(key, entry)
448
- }
449
-
450
- if (isNewEntry) {
451
- this.cacheStats.sets++
452
- }
453
- this.updateCacheStats()
454
- }
455
-
456
- private handleConditionalRequest(req: EnhancedRequest, entry: CacheEntry): Response | null {
457
- const ifNoneMatch = req.headers.get('If-None-Match')
458
- const ifModifiedSince = req.headers.get('If-Modified-Since')
459
-
460
- // Check ETag
461
- if (ifNoneMatch && entry.etag) {
462
- if (ifNoneMatch === entry.etag || ifNoneMatch === '*') {
463
- return new Response(null, { status: 304 })
464
- }
465
- }
466
-
467
- // Check Last-Modified
468
- if (ifModifiedSince && entry.lastModified) {
469
- const modifiedSince = new Date(ifModifiedSince)
470
- const lastModified = new Date(entry.lastModified)
471
-
472
- if (lastModified <= modifiedSince) {
473
- return new Response(null, { status: 304 })
474
- }
475
- }
476
-
477
- return null
478
- }
479
-
480
- async handle(req: EnhancedRequest, next: NextFunction): Promise<Response> {
481
- const cacheKey = this.generateCacheKey(req)
482
-
483
- // Check if this is an invalidating request
484
- if (this.options.invalidation.methods?.includes(req.method)) {
485
- const response = await next()
486
- if (response) {
487
- await this.invalidateCache(req)
488
- response.headers.set('X-Cache', 'MISS')
489
- return response
490
- }
491
- return new Response('Internal Server Error', { status: 500 })
492
- }
493
-
494
- // Try to get cached response
495
- let cachedEntry = await this.getCachedResponse(cacheKey)
496
-
497
- // Check if cached entry is expired
498
- if (cachedEntry && cachedEntry.expiresAt <= Date.now()) {
499
- // Remove expired entry
500
- await this.deleteCachedResponse(cacheKey)
501
- cachedEntry = null
502
- }
503
-
504
- if (cachedEntry) {
505
- // Track cache hit
506
- this.cacheStats.hits++
507
-
508
- // Handle conditional requests
509
- const conditionalResponse = this.handleConditionalRequest(req, cachedEntry)
510
- if (conditionalResponse) {
511
- conditionalResponse.headers.set('X-Cache', 'HIT')
512
- return conditionalResponse
513
- }
514
-
515
- // Return cached response
516
- const response = this.createResponseFromEntry(cachedEntry)
517
- if (response) {
518
- response.headers.set('X-Cache', 'HIT')
519
- return response
520
- }
521
- }
522
-
523
- // Track cache miss
524
- this.cacheStats.misses++
525
-
526
- // Cache miss - get response from next middleware
527
- const response = await next()
528
-
529
- if (!response) {
530
- return new Response('Internal Server Error', { status: 500 })
531
- }
532
-
533
- // Set X-Cache header for miss
534
- response.headers.set('X-Cache', 'MISS')
535
-
536
- // Check if response should be cached
537
- if (this.options.shouldCache(req, response)) {
538
- try {
539
- const { entry, response: updatedResponse } = await this.createCacheEntry(req, response)
540
- await this.setCachedResponse(cacheKey, entry)
541
- return updatedResponse
542
- }
543
- catch (error) {
544
- console.warn(`[ResponseCache] Failed to cache response: ${error}`)
545
- }
546
- }
547
-
548
- return response
549
- }
550
-
551
- private async deleteCachedResponse(key: string): Promise<void> {
552
- // Remove from memory cache
553
- if (this.memoryCache.has(key)) {
554
- this.memoryCache.delete(key)
555
- this.cacheStats.deletes++
556
- }
557
-
558
- // Remove from file cache
559
- if (this.options.storage.type === 'file' || this.options.storage.type === 'hybrid') {
560
- try {
561
- const filePath = join(this.options.storage.directory || '.cache/responses', `${key}.cache`)
562
- const bunFile = Bun.file(filePath)
563
- if (await bunFile.exists()) {
564
- await bunFile.delete()
565
- this.cacheStats.deletes++
566
- }
567
- }
568
- catch (error) {
569
- console.warn(`[ResponseCache] Failed to delete file cache: ${error}`)
570
- }
571
- }
572
-
573
- this.updateCacheStats()
574
- }
575
-
576
- private async createCacheEntry(req: EnhancedRequest, res: Response): Promise<{ entry: CacheEntry, response: Response }> {
577
- // Clone the response to avoid consuming the original stream
578
- const clonedRes = res.clone()
579
- const body = await clonedRes.arrayBuffer()
580
- const bodyBytes = new Uint8Array(body)
581
- const ttl = this.getTTL(req)
582
- const now = Date.now()
583
-
584
- const entry: CacheEntry = {
585
- key: this.generateCacheKey(req),
586
- response: {
587
- status: res.status,
588
- statusText: res.statusText,
589
- headers: Object.fromEntries(res.headers.entries()),
590
- body: Array.from(bodyBytes), // Convert to regular array for JSON serialization
591
- contentType: res.headers.get('Content-Type') || 'text/plain',
592
- },
593
- createdAt: now,
594
- expiresAt: now + ttl,
595
- size: bodyBytes.length,
596
- }
597
-
598
- // Generate ETag if enabled
599
- if (this.options.etag && this.options.etag.enabled) {
600
- entry.etag = this.generateETag(bodyBytes)
601
- if (entry.etag) {
602
- entry.response.headers.ETag = entry.etag
603
- res.headers.set('ETag', entry.etag)
604
- }
605
- }
606
-
607
- // Set Last-Modified
608
- entry.lastModified = new Date(now).toUTCString()
609
- entry.response.headers['Last-Modified'] = entry.lastModified
610
- res.headers.set('Last-Modified', entry.lastModified)
611
-
612
- return { entry, response: res }
613
- }
614
-
615
- private createResponseFromEntry(entry: CacheEntry): Response {
616
- const headers = new Headers(entry.response.headers)
617
-
618
- // Add cache headers
619
- headers.set('Age', Math.floor((Date.now() - entry.createdAt) / 1000).toString())
620
-
621
- // Convert body back to Uint8Array if it's an array
622
- const body = Array.isArray(entry.response.body)
623
- ? new Uint8Array(entry.response.body)
624
- : entry.response.body
625
-
626
- return new Response(body, {
627
- status: entry.response.status,
628
- statusText: entry.response.statusText,
629
- headers,
630
- })
631
- }
632
-
633
- private async backgroundRevalidate(cacheKey: string, req: EnhancedRequest, next: NextFunction): Promise<void> {
634
- try {
635
- const response = await next()
636
- if (response && this.options.shouldCache(req, response)) {
637
- const { entry } = await this.createCacheEntry(req, response)
638
- await this.setCachedResponse(cacheKey, entry)
639
- }
640
- }
641
- catch (error) {
642
- console.warn(`[ResponseCache] Background revalidation failed: ${error}`)
643
- }
644
- }
645
-
646
- async invalidateCache(req?: EnhancedRequest): Promise<void> {
647
- if (req) {
648
- const patterns = this.options.invalidation.patterns
649
- if (patterns && patterns.length > 0) {
650
- // Invalidate specific patterns
651
- const url = new URL(req.url)
652
-
653
- for (const pattern of patterns) {
654
- if (url.pathname.match(new RegExp(pattern))) {
655
- await this.clearCache()
656
- return
657
- }
658
- }
659
- }
660
- else {
661
- // No specific patterns, clear all cache for invalidating methods
662
- await this.clearCache()
663
- }
664
- }
665
- else {
666
- // Clear all cache
667
- await this.clearCache()
668
- }
669
- }
670
-
671
- async clearCache(): Promise<void> {
672
- // Get current cache size before clearing
673
- const currentSize = this.memoryCache.size
674
-
675
- // Clear memory cache
676
- this.memoryCache.clear()
677
- this.cacheStats.deletes += currentSize
678
-
679
- // Clear file cache if using file storage
680
- if (this.options.storage.type === 'file' || this.options.storage.type === 'hybrid') {
681
- try {
682
- const { readdir, unlink } = await import('node:fs/promises')
683
- const files = await readdir(this.options.storage.directory || '.cache/responses')
684
-
685
- for (const file of files) {
686
- if (file.endsWith('.cache')) {
687
- const filePath = `${this.options.storage.directory || '.cache/responses'}/${file}`
688
- await unlink(filePath)
689
- }
690
- }
691
- }
692
- catch (error) {
693
- console.warn(`[ResponseCache] Failed to clear file cache: ${error}`)
694
- }
695
- }
696
- this.updateCacheStats()
697
- }
698
-
699
- getStats(): { hits: number, misses: number, sets: number, deletes: number, size: number, memoryUsage: number, hitRate: number } {
700
- return {
701
- hits: this.cacheStats.hits,
702
- misses: this.cacheStats.misses,
703
- sets: this.cacheStats.sets,
704
- deletes: this.cacheStats.deletes,
705
- size: this.cacheStats.size,
706
- memoryUsage: this.cacheStats.memoryUsage,
707
- hitRate: this.cacheStats.hits / (this.cacheStats.hits + this.cacheStats.misses) || 0,
708
- }
709
- }
710
-
711
- destroy(): void {
712
- if (this.cleanupInterval) {
713
- clearInterval(this.cleanupInterval)
714
- }
715
- this.memoryCache.clear()
716
- }
717
- }
718
-
719
- // Factory functions
720
- export function responseCache(options?: ResponseCacheOptions): ResponseCache {
721
- return new ResponseCache(options)
722
- }
723
-
724
- export function memoryCache(options?: Omit<ResponseCacheOptions, 'storage'>): ResponseCache {
725
- return new ResponseCache({
726
- ...options,
727
- storage: { type: 'memory' },
728
- })
729
- }
730
-
731
- export function fileCache(directory?: string, options?: Omit<ResponseCacheOptions, 'storage'>): ResponseCache {
732
- return new ResponseCache({
733
- ...options,
734
- storage: { type: 'file', directory },
735
- })
736
- }
737
-
738
- export function hybridCache(options?: ResponseCacheOptions): ResponseCache {
739
- return new ResponseCache({
740
- ...options,
741
- storage: { type: 'hybrid' },
742
- })
743
- }