@stacksjs/bun-router 0.0.4 → 0.0.6

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 (272) hide show
  1. package/README.md +2 -2
  2. package/dist/auth.d.ts +134 -0
  3. package/dist/cache/lru-cache.d.ts +139 -0
  4. package/dist/cache/middleware-memoization.d.ts +151 -0
  5. package/dist/cache/route-cache-warmer.d.ts +161 -0
  6. package/dist/cache/sqlite-cache.d.ts +209 -0
  7. package/dist/cache/streaming-cache.d.ts +132 -0
  8. package/dist/chunk-jhsk5c5b.js +15758 -0
  9. package/dist/cli/colors.d.ts +15 -0
  10. package/dist/cli/index.d.ts +10 -0
  11. package/dist/cli/middleware.d.ts +31 -0
  12. package/dist/cli/openapi.d.ts +17 -0
  13. package/dist/cli/router.d.ts +15 -0
  14. package/dist/cli/routes.d.ts +29 -0
  15. package/dist/cli/utils.d.ts +74 -0
  16. package/dist/cli.d.ts +1 -1
  17. package/dist/cli.js +2 -2
  18. package/dist/config.d.ts +7 -0
  19. package/dist/container/container.d.ts +273 -0
  20. package/dist/container/contextual-binding.d.ts +240 -0
  21. package/dist/container/decorators.d.ts +141 -0
  22. package/dist/container/service-provider.d.ts +285 -0
  23. package/dist/development/hot-reload.d.ts +187 -0
  24. package/dist/development/index.d.ts +167 -0
  25. package/dist/development/performance-profiler.d.ts +217 -0
  26. package/dist/development/route-debugger.d.ts +154 -0
  27. package/dist/development/route-inspector.d.ts +211 -0
  28. package/dist/development/typescript-utilities.d.ts +209 -0
  29. package/dist/docs.d.ts +11 -0
  30. package/dist/errors/circuit-breaker.d.ts +195 -0
  31. package/dist/errors/error-handler.d.ts +88 -0
  32. package/dist/errors/error-reporting.d.ts +144 -0
  33. package/dist/errors/exceptions.d.ts +184 -0
  34. package/dist/errors/graceful-degradation.d.ts +154 -0
  35. package/dist/errors/index.d.ts +9 -0
  36. package/dist/errors/router-errors.d.ts +110 -0
  37. package/dist/file-serving/static-files.d.ts +143 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.js +320 -206
  40. package/dist/middleware/auth.d.ts +63 -0
  41. package/dist/middleware/content_security_policy.d.ts +53 -0
  42. package/dist/middleware/cors.d.ts +5 -0
  43. package/dist/middleware/csrf.d.ts +7 -0
  44. package/dist/middleware/ddos_protection.d.ts +39 -0
  45. package/dist/middleware/file_security.d.ts +26 -0
  46. package/dist/middleware/file_upload.d.ts +38 -0
  47. package/dist/middleware/helmet.d.ts +54 -0
  48. package/dist/middleware/index.d.ts +44 -0
  49. package/dist/middleware/input_validation.d.ts +45 -0
  50. package/dist/middleware/json_body.d.ts +4 -0
  51. package/dist/middleware/performance_alerting.d.ts +87 -0
  52. package/dist/middleware/performance_dashboard.d.ts +87 -0
  53. package/dist/middleware/performance_monitor.d.ts +209 -0
  54. package/dist/middleware/pipeline.d.ts +131 -0
  55. package/dist/middleware/rate_limit.d.ts +36 -0
  56. package/dist/middleware/request_id.d.ts +4 -0
  57. package/dist/middleware/request_signing.d.ts +153 -0
  58. package/dist/middleware/request_tracer.d.ts +72 -0
  59. package/dist/middleware/response_cache.d.ts +97 -0
  60. package/dist/middleware/security.d.ts +69 -0
  61. package/dist/middleware/security_suite.d.ts +46 -0
  62. package/dist/middleware/session.d.ts +23 -0
  63. package/dist/model-binding/index.d.ts +2 -0
  64. package/dist/model-binding/model-middleware.d.ts +118 -0
  65. package/dist/model-binding/model-registry.d.ts +164 -0
  66. package/dist/model-binding.d.ts +185 -0
  67. package/dist/model-resolver-factory.d.ts +31 -0
  68. package/dist/observability/correlation.d.ts +187 -0
  69. package/dist/observability/health-checks.d.ts +186 -0
  70. package/dist/observability/index.d.ts +60 -0
  71. package/dist/observability/integration.d.ts +147 -0
  72. package/dist/observability/metrics.d.ts +184 -0
  73. package/dist/observability/tracing.d.ts +187 -0
  74. package/dist/optimization/bun-utilities.d.ts +224 -0
  75. package/dist/query-builder-integration.d.ts +36 -0
  76. package/dist/request/context.d.ts +34 -0
  77. package/dist/request/enhanced-request.d.ts +213 -0
  78. package/dist/request/macros.d.ts +300 -0
  79. package/dist/response/macros.d.ts +245 -0
  80. package/dist/response/response-factory.d.ts +133 -0
  81. package/dist/router/api-routes.d.ts +5 -0
  82. package/dist/router/file-based-routing.d.ts +118 -0
  83. package/dist/router/file-streaming.d.ts +5 -0
  84. package/dist/router/fluent-router.d.ts +315 -0
  85. package/dist/router/fluent-routing.d.ts +271 -0
  86. package/dist/router/group-organization.d.ts +5 -0
  87. package/dist/router/handler-resolver.d.ts +24 -0
  88. package/dist/router/http-methods.d.ts +10 -0
  89. package/dist/router/index.d.ts +113 -0
  90. package/dist/router/middleware-groups.d.ts +94 -0
  91. package/dist/router/middleware-integration.d.ts +112 -0
  92. package/dist/router/middleware.d.ts +5 -0
  93. package/dist/router/model-binding.d.ts +5 -0
  94. package/dist/router/optimized-route-matching.d.ts +6 -0
  95. package/dist/router/route-building.d.ts +5 -0
  96. package/dist/router/route-compiler.d.ts +153 -0
  97. package/dist/router/route-matching.d.ts +5 -0
  98. package/dist/router/route-trie.d.ts +114 -0
  99. package/dist/router/router.d.ts +276 -0
  100. package/dist/router/server.d.ts +5 -0
  101. package/dist/router/validation-integration.d.ts +170 -0
  102. package/dist/router/view-rendering.d.ts +5 -0
  103. package/dist/router/websocket.d.ts +5 -0
  104. package/dist/routing/route-caching.d.ts +129 -0
  105. package/dist/routing/route-throttling.d.ts +150 -0
  106. package/dist/routing/subdomain-routing.d.ts +206 -0
  107. package/dist/session/database-store.d.ts +51 -0
  108. package/dist/session/file-store.d.ts +22 -0
  109. package/dist/session/index.d.ts +106 -0
  110. package/dist/session/memory-store.d.ts +21 -0
  111. package/dist/session/redis-store.d.ts +31 -0
  112. package/dist/streaming/index.d.ts +3 -0
  113. package/dist/streaming/sse-handler.d.ts +138 -0
  114. package/dist/streaming/stream-handler.d.ts +114 -0
  115. package/dist/testing/auth-testing.d.ts +156 -0
  116. package/dist/testing/file-upload-testing.d.ts +190 -0
  117. package/dist/testing/index.d.ts +10 -0
  118. package/dist/testing/middleware-testing.d.ts +138 -0
  119. package/dist/testing/model-binding-testing.d.ts +186 -0
  120. package/dist/testing/performance-testing.d.ts +235 -0
  121. package/dist/testing/test-client.d.ts +117 -0
  122. package/dist/testing/test-request.d.ts +85 -0
  123. package/dist/testing/test-response.d.ts +90 -0
  124. package/dist/testing/types.d.ts +207 -0
  125. package/dist/testing/websocket-testing.d.ts +227 -0
  126. package/dist/types/controller-types.d.ts +208 -0
  127. package/dist/types/core.d.ts +543 -0
  128. package/dist/types/middleware-types.d.ts +227 -0
  129. package/dist/types/request-response-augmentation.d.ts +261 -0
  130. package/dist/types/route-inference.d.ts +168 -0
  131. package/dist/types.d.ts +1776 -0
  132. package/dist/url.d.ts +56 -0
  133. package/dist/utils/index.d.ts +1 -0
  134. package/dist/utils/query-preservation.d.ts +48 -0
  135. package/dist/utils.d.ts +69 -0
  136. package/dist/validation/validator.d.ts +140 -0
  137. package/dist/websocket/clustering.d.ts +185 -0
  138. package/package.json +18 -4
  139. package/src/auth.ts +469 -0
  140. package/src/cache/lru-cache.ts +457 -0
  141. package/src/cache/middleware-memoization.ts +531 -0
  142. package/src/cache/route-cache-warmer.ts +486 -0
  143. package/src/cache/sqlite-cache.ts +783 -0
  144. package/src/cache/streaming-cache.ts +572 -0
  145. package/src/cli/colors.ts +29 -0
  146. package/src/cli/index.ts +287 -0
  147. package/src/cli/middleware.ts +291 -0
  148. package/src/cli/openapi.ts +407 -0
  149. package/src/cli/router.ts +188 -0
  150. package/src/cli/routes.ts +265 -0
  151. package/src/cli/utils.ts +531 -0
  152. package/src/cli.ts +5 -0
  153. package/src/config.ts +322 -0
  154. package/src/container/container.ts +740 -0
  155. package/src/container/contextual-binding.ts +603 -0
  156. package/src/container/decorators.ts +359 -0
  157. package/src/container/service-provider.ts +596 -0
  158. package/src/development/hot-reload.ts +673 -0
  159. package/src/development/index.ts +499 -0
  160. package/src/development/performance-profiler.ts +717 -0
  161. package/src/development/route-debugger.ts +527 -0
  162. package/src/development/route-inspector.ts +749 -0
  163. package/src/development/typescript-utilities.ts +678 -0
  164. package/src/docs.ts +397 -0
  165. package/src/errors/circuit-breaker.ts +732 -0
  166. package/src/errors/error-handler.ts +569 -0
  167. package/src/errors/error-reporting.ts +672 -0
  168. package/src/errors/exceptions.ts +536 -0
  169. package/src/errors/graceful-degradation.ts +621 -0
  170. package/src/errors/index.ts +21 -0
  171. package/src/errors/router-errors.ts +632 -0
  172. package/src/file-serving/static-files.ts +581 -0
  173. package/src/index.ts +14 -0
  174. package/src/middleware/auth.ts +219 -0
  175. package/src/middleware/content_security_policy.ts +215 -0
  176. package/src/middleware/cors.ts +74 -0
  177. package/src/middleware/csrf.ts +108 -0
  178. package/src/middleware/ddos_protection.ts +255 -0
  179. package/src/middleware/file_security.ts +191 -0
  180. package/src/middleware/file_upload.ts +264 -0
  181. package/src/middleware/helmet.ts +268 -0
  182. package/src/middleware/index.ts +117 -0
  183. package/src/middleware/input_validation.ts +449 -0
  184. package/src/middleware/json_body.ts +37 -0
  185. package/src/middleware/performance_alerting.ts +538 -0
  186. package/src/middleware/performance_dashboard.ts +661 -0
  187. package/src/middleware/performance_monitor.ts +943 -0
  188. package/src/middleware/pipeline.ts +489 -0
  189. package/src/middleware/rate_limit.ts +245 -0
  190. package/src/middleware/request_id.ts +36 -0
  191. package/src/middleware/request_signing.ts +636 -0
  192. package/src/middleware/request_tracer.ts +636 -0
  193. package/src/middleware/response_cache.ts +743 -0
  194. package/src/middleware/security.ts +482 -0
  195. package/src/middleware/security_suite.ts +257 -0
  196. package/src/middleware/session.ts +91 -0
  197. package/src/model-binding/index.ts +18 -0
  198. package/src/model-binding/model-middleware.ts +425 -0
  199. package/src/model-binding/model-registry.ts +550 -0
  200. package/src/model-binding.ts +370 -0
  201. package/src/model-resolver-factory.ts +106 -0
  202. package/src/observability/correlation.ts +691 -0
  203. package/src/observability/health-checks.ts +729 -0
  204. package/src/observability/index.ts +184 -0
  205. package/src/observability/integration.ts +548 -0
  206. package/src/observability/metrics.ts +753 -0
  207. package/src/observability/tracing.ts +638 -0
  208. package/src/optimization/bun-utilities.ts +778 -0
  209. package/src/query-builder-integration.ts +137 -0
  210. package/src/request/context.ts +62 -0
  211. package/src/request/enhanced-request.ts +857 -0
  212. package/src/request/macros.ts +688 -0
  213. package/src/response/macros.ts +612 -0
  214. package/src/response/response-factory.ts +588 -0
  215. package/src/router/api-routes.ts +243 -0
  216. package/src/router/file-based-routing.ts +670 -0
  217. package/src/router/file-streaming.ts +372 -0
  218. package/src/router/fluent-router.ts +927 -0
  219. package/src/router/fluent-routing.ts +797 -0
  220. package/src/router/group-organization.ts +213 -0
  221. package/src/router/handler-resolver.ts +291 -0
  222. package/src/router/http-methods.ts +377 -0
  223. package/src/router/index.ts +185 -0
  224. package/src/router/middleware-groups.ts +222 -0
  225. package/src/router/middleware-integration.ts +399 -0
  226. package/src/router/middleware.ts +231 -0
  227. package/src/router/model-binding.ts +215 -0
  228. package/src/router/optimized-route-matching.ts +253 -0
  229. package/src/router/route-building.ts +212 -0
  230. package/src/router/route-compiler.ts +702 -0
  231. package/src/router/route-matching.ts +349 -0
  232. package/src/router/route-trie.ts +464 -0
  233. package/src/router/router.ts +1508 -0
  234. package/src/router/server.ts +406 -0
  235. package/src/router/validation-integration.ts +443 -0
  236. package/src/router/view-rendering.ts +233 -0
  237. package/src/router/websocket.ts +100 -0
  238. package/src/routing/route-caching.ts +402 -0
  239. package/src/routing/route-throttling.ts +469 -0
  240. package/src/routing/subdomain-routing.ts +492 -0
  241. package/src/session/database-store.ts +109 -0
  242. package/src/session/file-store.ts +148 -0
  243. package/src/session/index.ts +244 -0
  244. package/src/session/memory-store.ts +88 -0
  245. package/src/session/redis-store.ts +93 -0
  246. package/src/streaming/index.ts +17 -0
  247. package/src/streaming/sse-handler.ts +482 -0
  248. package/src/streaming/stream-handler.ts +552 -0
  249. package/src/testing/auth-testing.ts +446 -0
  250. package/src/testing/file-upload-testing.ts +543 -0
  251. package/src/testing/index.ts +10 -0
  252. package/src/testing/middleware-testing.ts +322 -0
  253. package/src/testing/model-binding-testing.ts +645 -0
  254. package/src/testing/performance-testing.ts +738 -0
  255. package/src/testing/test-client.ts +310 -0
  256. package/src/testing/test-request.ts +315 -0
  257. package/src/testing/test-response.ts +332 -0
  258. package/src/testing/types.ts +224 -0
  259. package/src/testing/websocket-testing.ts +590 -0
  260. package/src/types/controller-types.ts +385 -0
  261. package/src/types/core.ts +683 -0
  262. package/src/types/middleware-types.ts +419 -0
  263. package/src/types/request-response-augmentation.ts +489 -0
  264. package/src/types/route-inference.ts +357 -0
  265. package/src/types.ts +2049 -0
  266. package/src/url.ts +126 -0
  267. package/src/utils/index.ts +1 -0
  268. package/src/utils/query-preservation.ts +201 -0
  269. package/src/utils.ts +327 -0
  270. package/src/validation/validator.ts +685 -0
  271. package/src/websocket/clustering.ts +762 -0
  272. package/dist/chunk-cjdekj87.js +0 -25886
@@ -0,0 +1,743 @@
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
+ }