@stacksjs/bun-router 0.0.13 → 0.0.14

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