@stacksjs/bun-router 0.0.17 → 0.0.19

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 (153) hide show
  1. package/dist/chunk-2mx7fq49.js +4 -0
  2. package/dist/chunk-a8p83068.js +103 -0
  3. package/dist/{chunk-cgptvjdf.js → chunk-y3xq6wq6.js} +1 -1
  4. package/dist/cli.js +4 -3
  5. package/dist/container/index.js +1149 -211
  6. package/dist/container/service-provider.d.ts +3 -3
  7. package/dist/development/performance-profiler.d.ts +3 -3
  8. package/dist/development/route-debugger.d.ts +1 -1
  9. package/dist/development/route-inspector.d.ts +2 -2
  10. package/dist/index.js +369 -2209
  11. package/dist/observability/integration.d.ts +1 -1
  12. package/dist/response/macros.d.ts +1 -1
  13. package/dist/router/index.d.ts +7 -7
  14. package/dist/router/router.d.ts +0 -32
  15. package/dist/testing/performance-testing.d.ts +2 -2
  16. package/dist/testing/test-client.d.ts +1 -1
  17. package/dist/testing/websocket-testing.d.ts +1 -1
  18. package/package.json +7 -7
  19. package/dist/chunk-1ahs68ys.js +0 -18
  20. package/dist/chunk-g3ybefhg.js +0 -1923
  21. package/src/auth.ts +0 -489
  22. package/src/cache/lru-cache.ts +0 -457
  23. package/src/cache/middleware-memoization.ts +0 -531
  24. package/src/cache/route-cache-warmer.ts +0 -486
  25. package/src/cache/sqlite-cache.ts +0 -783
  26. package/src/cache/streaming-cache.ts +0 -572
  27. package/src/cli/colors.ts +0 -29
  28. package/src/cli/index.ts +0 -287
  29. package/src/cli/middleware.ts +0 -291
  30. package/src/cli/openapi.ts +0 -407
  31. package/src/cli/router.ts +0 -188
  32. package/src/cli/routes.ts +0 -265
  33. package/src/cli/utils.ts +0 -531
  34. package/src/cli.ts +0 -5
  35. package/src/config.ts +0 -322
  36. package/src/container/container.ts +0 -786
  37. package/src/container/contextual-binding.ts +0 -606
  38. package/src/container/decorators.ts +0 -368
  39. package/src/container/index.ts +0 -49
  40. package/src/container/service-provider.ts +0 -596
  41. package/src/development/hot-reload.ts +0 -673
  42. package/src/development/index.ts +0 -499
  43. package/src/development/performance-profiler.ts +0 -717
  44. package/src/development/route-debugger.ts +0 -527
  45. package/src/development/route-inspector.ts +0 -749
  46. package/src/development/typescript-utilities.ts +0 -682
  47. package/src/docs.ts +0 -397
  48. package/src/errors/circuit-breaker.ts +0 -733
  49. package/src/errors/error-handler.ts +0 -569
  50. package/src/errors/error-reporting.ts +0 -672
  51. package/src/errors/exceptions.ts +0 -536
  52. package/src/errors/graceful-degradation.ts +0 -623
  53. package/src/errors/index.ts +0 -21
  54. package/src/errors/router-errors.ts +0 -632
  55. package/src/file-serving/static-files.ts +0 -718
  56. package/src/index.ts +0 -66
  57. package/src/middleware/auth.ts +0 -220
  58. package/src/middleware/content_security_policy.ts +0 -215
  59. package/src/middleware/cors.ts +0 -142
  60. package/src/middleware/csrf.ts +0 -166
  61. package/src/middleware/ddos_protection.ts +0 -279
  62. package/src/middleware/file_security.ts +0 -191
  63. package/src/middleware/file_upload.ts +0 -275
  64. package/src/middleware/helmet.ts +0 -268
  65. package/src/middleware/index.ts +0 -117
  66. package/src/middleware/input_validation.ts +0 -453
  67. package/src/middleware/json_body.ts +0 -37
  68. package/src/middleware/performance_alerting.ts +0 -540
  69. package/src/middleware/performance_dashboard.ts +0 -661
  70. package/src/middleware/performance_monitor.ts +0 -945
  71. package/src/middleware/pipeline.ts +0 -489
  72. package/src/middleware/rate_limit.ts +0 -251
  73. package/src/middleware/request_id.ts +0 -36
  74. package/src/middleware/request_signing.ts +0 -638
  75. package/src/middleware/request_tracer.ts +0 -653
  76. package/src/middleware/response_cache.ts +0 -778
  77. package/src/middleware/security.ts +0 -482
  78. package/src/middleware/security_suite.ts +0 -257
  79. package/src/middleware/session.ts +0 -91
  80. package/src/model-binding/index.ts +0 -18
  81. package/src/model-binding/model-middleware.ts +0 -425
  82. package/src/model-binding/model-registry.ts +0 -550
  83. package/src/model-binding.ts +0 -370
  84. package/src/model-resolver-factory.ts +0 -106
  85. package/src/observability/correlation.ts +0 -691
  86. package/src/observability/health-checks.ts +0 -729
  87. package/src/observability/index.ts +0 -184
  88. package/src/observability/integration.ts +0 -548
  89. package/src/observability/metrics.ts +0 -753
  90. package/src/observability/tracing.ts +0 -638
  91. package/src/optimization/bun-utilities.ts +0 -778
  92. package/src/query-builder-integration.ts +0 -137
  93. package/src/request/context.ts +0 -109
  94. package/src/request/enhanced-request.ts +0 -876
  95. package/src/request/macros.ts +0 -822
  96. package/src/response/macros.ts +0 -665
  97. package/src/response/response-factory.ts +0 -596
  98. package/src/router/api-routes.ts +0 -243
  99. package/src/router/file-based-routing.ts +0 -697
  100. package/src/router/file-streaming.ts +0 -383
  101. package/src/router/fluent-routing.ts +0 -866
  102. package/src/router/group-organization.ts +0 -229
  103. package/src/router/handler-resolver.ts +0 -327
  104. package/src/router/http-methods.ts +0 -407
  105. package/src/router/index.ts +0 -193
  106. package/src/router/middleware-groups.ts +0 -222
  107. package/src/router/middleware-integration.ts +0 -399
  108. package/src/router/middleware.ts +0 -234
  109. package/src/router/model-binding.ts +0 -215
  110. package/src/router/optimized-route-matching.ts +0 -302
  111. package/src/router/route-building.ts +0 -221
  112. package/src/router/route-compiler.ts +0 -691
  113. package/src/router/route-matching.ts +0 -368
  114. package/src/router/route-trie.ts +0 -450
  115. package/src/router/router.ts +0 -1616
  116. package/src/router/server.ts +0 -684
  117. package/src/router/validation-integration.ts +0 -449
  118. package/src/router/view-rendering.ts +0 -233
  119. package/src/router/websocket.ts +0 -100
  120. package/src/routing/route-caching.ts +0 -402
  121. package/src/routing/route-throttling.ts +0 -469
  122. package/src/routing/subdomain-routing.ts +0 -492
  123. package/src/session/database-store.ts +0 -109
  124. package/src/session/file-store.ts +0 -148
  125. package/src/session/index.ts +0 -244
  126. package/src/session/memory-store.ts +0 -88
  127. package/src/session/redis-store.ts +0 -93
  128. package/src/streaming/index.ts +0 -17
  129. package/src/streaming/sse-handler.ts +0 -482
  130. package/src/streaming/stream-handler.ts +0 -552
  131. package/src/testing/auth-testing.ts +0 -448
  132. package/src/testing/file-upload-testing.ts +0 -543
  133. package/src/testing/index.ts +0 -10
  134. package/src/testing/middleware-testing.ts +0 -322
  135. package/src/testing/model-binding-testing.ts +0 -645
  136. package/src/testing/performance-testing.ts +0 -738
  137. package/src/testing/test-client.ts +0 -310
  138. package/src/testing/test-request.ts +0 -315
  139. package/src/testing/test-response.ts +0 -332
  140. package/src/testing/types.ts +0 -224
  141. package/src/testing/websocket-testing.ts +0 -590
  142. package/src/types/controller-types.ts +0 -385
  143. package/src/types/core.ts +0 -683
  144. package/src/types/middleware-types.ts +0 -422
  145. package/src/types/request-response-augmentation.ts +0 -489
  146. package/src/types/route-inference.ts +0 -372
  147. package/src/types.ts +0 -2123
  148. package/src/url.ts +0 -131
  149. package/src/utils/index.ts +0 -1
  150. package/src/utils/query-preservation.ts +0 -201
  151. package/src/utils.ts +0 -395
  152. package/src/validation/validator.ts +0 -688
  153. package/src/websocket/clustering.ts +0 -769
@@ -1,691 +0,0 @@
1
- import type { HTTPMethod, MatchResult, PatternMatchResult, Route } from '../types'
2
- import type { CompiledRoute } from './route-trie'
3
- import { RouteTrie } from './route-trie'
4
-
5
- /**
6
- * Route compilation options
7
- */
8
- export interface RouteCompilerOptions {
9
- enableCaching: boolean
10
- enableMethodGrouping: boolean
11
- enablePriorityOptimization: boolean
12
- cacheSize: number
13
- precompilePatterns: boolean
14
- /**
15
- * Record per-match timing via `performance.now()`. Off by default —
16
- * timing every request costs two clock reads per match on the hot path.
17
- * Match/hit/miss counters are always collected (they're just integers).
18
- */
19
- enableProfiling: boolean
20
- }
21
-
22
- /**
23
- * Route matching statistics for performance monitoring
24
- */
25
- export interface RouteMatchStats {
26
- totalMatches: number
27
- cacheHits: number
28
- cacheMisses: number
29
- averageMatchTime: number
30
- trieDepth: number
31
- methodDistribution: Record<string, number>
32
- }
33
-
34
- /**
35
- * High-performance route compiler with pre-compilation and optimization
36
- */
37
- export class RouteCompiler {
38
- private trie: RouteTrie
39
- // Bounded match cache with FIFO eviction (oldest key in Map iteration
40
- // order). Cache hits are pure reads — refreshing recency on every hit
41
- // (true LRU) costs a delete+set per request and buys almost nothing
42
- // for route tables, where the hot set is small and stable. The old
43
- // implementation kept a `cacheKeys` array whose indexOf/splice
44
- // bookkeeping cost O(cacheSize) on every request.
45
- private matchCache: Map<string, MatchResult | null> = new Map()
46
- // O(1) duplicate detection by `method:path` (the old approach scanned
47
- // every compiled route on each addRoute call — O(n²) registration).
48
- private routeKeys: Set<string> = new Set()
49
- private stats: RouteMatchStats = {
50
- totalMatches: 0,
51
- cacheHits: 0,
52
- cacheMisses: 0,
53
- averageMatchTime: 0,
54
- trieDepth: 0,
55
- methodDistribution: {},
56
- }
57
-
58
- private options: RouteCompilerOptions
59
-
60
- constructor(options: Partial<RouteCompilerOptions> = {}) {
61
- this.options = {
62
- enableCaching: true,
63
- enableMethodGrouping: true,
64
- enablePriorityOptimization: true,
65
- cacheSize: 1000,
66
- precompilePatterns: true,
67
- enableProfiling: false,
68
- ...options,
69
- }
70
-
71
- this.trie = new RouteTrie()
72
- }
73
-
74
- /**
75
- * Add a route to the compiler
76
- * @returns true if route was added, false if it's a duplicate
77
- */
78
- addRoute(route: Route): boolean {
79
- // Pre-compile the route pattern if enabled
80
- if (this.options.precompilePatterns) {
81
- this.precompileRoutePattern(route)
82
- }
83
-
84
- // Check for exact duplicates before adding — O(1) set lookup
85
- const routeKey = `${route.method}:${route.path}`
86
- if (this.routeKeys.has(routeKey)) {
87
- return false
88
- }
89
- this.routeKeys.add(routeKey)
90
-
91
- // Add to trie for fast matching
92
- this.trie.addRoute(route)
93
-
94
- // Clear cache when routes change
95
- if (this.options.enableCaching) {
96
- this.matchCache.clear()
97
- }
98
-
99
- // Update method distribution stats
100
- const method = route.method
101
- this.stats.methodDistribution[method] = (this.stats.methodDistribution[method] || 0) + 1
102
-
103
- return true
104
- }
105
-
106
- /**
107
- * Pre-compile route patterns for faster matching
108
- */
109
- // Pattern cache for memory optimization
110
- private patternCache: Map<string, { exec: (url: URL) => PatternMatchResult | null }> = new Map()
111
-
112
- /**
113
- * Extract parameter names from a route path
114
- */
115
- private extractParamNames(path: string): string[] {
116
- const paramNames: string[] = []
117
- const regex = /\{([^}:]+)(?::[^}]+)?\}/g
118
-
119
- // Using a safe approach to avoid lint error with assignment in while condition
120
- let tempMatch: RegExpExecArray | null
121
- // eslint-disable-next-line no-cond-assign
122
- while (tempMatch = regex.exec(path)) {
123
- paramNames.push(tempMatch[1].replace('?', ''))
124
- }
125
-
126
- return paramNames
127
- }
128
-
129
- /**
130
- * Pre-compile route patterns for faster matching
131
- */
132
- private precompileRoutePattern(route: Route): void {
133
- if (!route.pattern && route.path.includes('{')) {
134
- // Generate a cache key that includes constraints if present
135
- let cacheKey = route.path
136
- if (route.constraints && !Array.isArray(route.constraints)) {
137
- // Sort constraint keys for consistent cache key generation
138
- // Type assertion to handle the Record<string, string> case
139
- const constraintsRecord = route.constraints as Record<string, string>
140
- const constraintKeys = Object.keys(constraintsRecord).sort()
141
- const constraintString = constraintKeys
142
- .map(key => `${key}:${constraintsRecord[key]}`)
143
- .join('|')
144
- cacheKey = `${route.path}#${constraintString}`
145
- }
146
-
147
- // Check if we already have this pattern cached
148
- if (this.patternCache.has(cacheKey)) {
149
- route.pattern = this.patternCache.get(cacheKey)!
150
- return
151
- }
152
-
153
- // Convert Laravel-style parameters to URLPattern with constraints
154
- const pattern = this.convertToURLPattern(
155
- route.path,
156
- route.constraints && !Array.isArray(route.constraints) ? route.constraints : undefined,
157
- )
158
-
159
- // Create a URL pattern adapter that matches the expected interface
160
- const urlPatternAdapter = {
161
- exec: (url: URL): PatternMatchResult | null => {
162
- const pathname = url.pathname
163
- const result = pattern.exec(pathname)
164
- if (!result)
165
- return null
166
-
167
- return {
168
- pathname: {
169
- groups: result.groups || {},
170
- },
171
- }
172
- },
173
- }
174
-
175
- // Cache the compiled pattern
176
- this.patternCache.set(cacheKey, urlPatternAdapter)
177
-
178
- // Assign the pattern to the route
179
- route.pattern = urlPatternAdapter
180
- }
181
- }
182
-
183
- /**
184
- * Convert Laravel-style route to a simple regex pattern
185
- * @param path The route path with Laravel-style parameters
186
- * @param constraints Optional constraints for route parameters
187
- */
188
- private convertToURLPattern(
189
- path: string,
190
- constraints?: Record<string, string>,
191
- ): { exec: (pathname: string) => { groups?: Record<string, string> } | null } {
192
- // Pull params out as placeholder tokens so the static text can be
193
- // regex-escaped safely — without this, a literal dot in a path like
194
- // `/api/v1.0/users` would match any character.
195
- const paramTokens: string[] = []
196
- const withTokens = path.replace(/\{([^}]+)\}/g, (_m, inner: string) => {
197
- paramTokens.push(inner)
198
- return `\u0000${paramTokens.length - 1}\u0000`
199
- })
200
-
201
- let regexPattern = withTokens.replace(/[.+*?^${}()|[\]\\]/g, '\\$&')
202
-
203
- // Convert placeholder tokens to named capture groups. Optional params
204
- // (`{id?}`) make both the parameter and its leading slash optional.
205
- regexPattern = regexPattern.replace(/\/?\u0000(\d+)\u0000/g, (token, idx: string) => {
206
- const inner = paramTokens[Number(idx)]
207
- const isOptional = inner.endsWith('?')
208
- const core = isOptional ? inner.slice(0, -1) : inner
209
- const colonIndex = core.indexOf(':')
210
- const name = colonIndex === -1 ? core : core.slice(0, colonIndex)
211
- const inlinePattern = colonIndex === -1 ? undefined : core.slice(colonIndex + 1)
212
- const pattern = constraints?.[name] ?? inlinePattern ?? '[^/]+'
213
- const group = `(?<${name}>${pattern})`
214
- const leadingSlash = token.startsWith('/') ? '/' : ''
215
- return isOptional ? `(?:${leadingSlash}${group})?` : `${leadingSlash}${group}`
216
- })
217
-
218
- const regex = new RegExp(`^${regexPattern}$`)
219
-
220
- return {
221
- exec: (pathname: string) => {
222
- const match = pathname.match(regex)
223
- if (!match)
224
- return null
225
-
226
- return {
227
- groups: match.groups || {},
228
- }
229
- },
230
- }
231
- }
232
-
233
- /**
234
- * Match a request path and method to a route
235
- */
236
- match(path: string, method: HTTPMethod): MatchResult | null {
237
- const startTime = this.options.enableProfiling ? performance.now() : 0
238
- this.stats.totalMatches++
239
-
240
- // Check cache first if enabled
241
- if (this.options.enableCaching) {
242
- const cacheKey = `${method}:${path}`
243
- const cached = this.matchCache.get(cacheKey)
244
- if (cached !== undefined || this.matchCache.has(cacheKey)) {
245
- this.stats.cacheHits++
246
- if (startTime)
247
- this.updateMatchTime(startTime)
248
- // Hand each request its own params object — cached results are
249
- // shared, and handlers are allowed to mutate `req.params`.
250
- return cached ? { route: cached.route, params: { ...cached.params } } : null
251
- }
252
- this.stats.cacheMisses++
253
- }
254
-
255
- // Use trie for fast matching
256
- const trieMatch = this.trie.match(path, method)
257
- let result: MatchResult | null = null
258
-
259
- if (trieMatch) {
260
- result = {
261
- route: trieMatch.route,
262
- params: trieMatch.params,
263
- }
264
- }
265
-
266
- // Cache the result if enabled
267
- if (this.options.enableCaching) {
268
- const cacheKey = `${method}:${path}`
269
- this.addToCache(cacheKey, result)
270
- }
271
-
272
- if (startTime)
273
- this.updateMatchTime(startTime)
274
- return result ? { route: result.route, params: { ...result.params } } : null
275
- }
276
-
277
- /**
278
- * Add an entry to the cache with LRU eviction if needed
279
- */
280
- private addToCache(key: string, value: MatchResult | null): void {
281
- // If cache is full, evict the least recently used entry (oldest key
282
- // in Map iteration order)
283
- if (this.matchCache.size >= this.options.cacheSize && !this.matchCache.has(key)) {
284
- const lruKey = this.matchCache.keys().next().value
285
- if (lruKey !== undefined) {
286
- this.matchCache.delete(lruKey)
287
- }
288
- }
289
-
290
- this.matchCache.set(key, value)
291
- }
292
-
293
- /**
294
- * Update average match time statistics (only when profiling is enabled)
295
- */
296
- private updateMatchTime(startTime: number): void {
297
- const matchTime = performance.now() - startTime
298
- this.stats.averageMatchTime = (
299
- (this.stats.averageMatchTime * (this.stats.totalMatches - 1) + matchTime)
300
- / this.stats.totalMatches
301
- )
302
- }
303
-
304
- /**
305
- * Get all compiled routes sorted by priority
306
- */
307
- getCompiledRoutes(): CompiledRoute[] {
308
- const routes = this.trie.getAllRoutes()
309
-
310
- if (this.options.enablePriorityOptimization) {
311
- return routes.sort((a, b) => b.priority - a.priority)
312
- }
313
-
314
- return routes
315
- }
316
-
317
- /**
318
- * Get routes grouped by HTTP method
319
- */
320
- getRoutesByMethod(): Map<HTTPMethod, CompiledRoute[]> {
321
- const routesByMethod = new Map<HTTPMethod, CompiledRoute[]>()
322
-
323
- for (const compiled of this.trie.getAllRoutes()) {
324
- const method = compiled.route.method as HTTPMethod
325
- if (!routesByMethod.has(method)) {
326
- routesByMethod.set(method, [])
327
- }
328
- routesByMethod.get(method)!.push(compiled)
329
- }
330
-
331
- // Sort each method's routes by priority
332
- if (this.options.enablePriorityOptimization) {
333
- for (const [method, routes] of routesByMethod) {
334
- routes.sort((a, b) => b.priority - a.priority)
335
- routesByMethod.set(method, routes)
336
- }
337
- }
338
-
339
- return routesByMethod
340
- }
341
-
342
- /**
343
- * Warm up the cache with common routes
344
- * @param commonPaths Array of paths and methods to pre-cache
345
- * @param options Optional warming options
346
- * @param options.force If true, enables caching temporarily even if disabled in compiler options
347
- * @param options.precompile If true, performs a second match to ensure cache hits are recorded
348
- * @returns Statistics about the warming operation
349
- */
350
- warmCache(
351
- commonPaths: Array<{ path: string, method: HTTPMethod }>,
352
- options: { force?: boolean, precompile?: boolean } = {},
353
- ): { warmedPaths: number, cacheSize: number, hitRate: number } {
354
- if (!this.options.enableCaching && !options.force) {
355
- return { warmedPaths: 0, cacheSize: 0, hitRate: 0 }
356
- }
357
-
358
- // Enable caching temporarily if force is true
359
- const originalCachingState = this.options.enableCaching
360
- if (options.force && !this.options.enableCaching) {
361
- this.options.enableCaching = true
362
- }
363
-
364
- // Clear existing cache entries for these paths to ensure fresh warming
365
- if (options.precompile) {
366
- for (const { path, method } of commonPaths) {
367
- const cacheKey = `${method}:${path}`
368
- this.matchCache.delete(cacheKey)
369
- }
370
- }
371
-
372
- // Warm the cache by matching each path
373
- const initialCacheSize = this.matchCache.size
374
- const initialHits = this.stats.cacheHits
375
- const initialTotal = this.stats.totalMatches
376
-
377
- for (const { path, method } of commonPaths) {
378
- // First match will cache the result
379
- this.match(path, method)
380
-
381
- // Second match should be a cache hit
382
- if (options.precompile) {
383
- this.match(path, method)
384
- }
385
- }
386
-
387
- // Calculate statistics
388
- const newCacheEntries = this.matchCache.size - initialCacheSize
389
- const newHits = this.stats.cacheHits - initialHits
390
- const newTotal = this.stats.totalMatches - initialTotal
391
- const hitRate = newTotal > 0 ? newHits / newTotal : 0
392
-
393
- // Restore original caching state if needed
394
- if (options.force && !originalCachingState) {
395
- this.options.enableCaching = originalCachingState
396
- }
397
-
398
- return {
399
- warmedPaths: newCacheEntries,
400
- cacheSize: this.matchCache.size,
401
- hitRate,
402
- }
403
- }
404
-
405
- /**
406
- * Clear all caches and reset statistics
407
- */
408
- clear(): void {
409
- this.trie.clear()
410
- this.matchCache.clear()
411
- this.routeKeys.clear()
412
- this.stats = {
413
- totalMatches: 0,
414
- cacheHits: 0,
415
- cacheMisses: 0,
416
- averageMatchTime: 0,
417
- trieDepth: 0,
418
- methodDistribution: {},
419
- }
420
- }
421
-
422
- /**
423
- * Get performance statistics
424
- */
425
- getStats(): RouteMatchStats {
426
- const trieStats = this.trie.getStats()
427
- return {
428
- ...this.stats,
429
- trieDepth: trieStats.averageDepth,
430
- methodDistribution: trieStats.methodDistribution,
431
- }
432
- }
433
-
434
- /**
435
- * Get cache statistics
436
- */
437
- getCacheStats(): {
438
- size: number
439
- maxSize: number
440
- hitRate: number
441
- enabled: boolean
442
- } {
443
- const hitRate = this.stats.totalMatches > 0
444
- ? this.stats.cacheHits / this.stats.totalMatches
445
- : 0
446
-
447
- return {
448
- size: this.matchCache.size,
449
- maxSize: this.options.cacheSize,
450
- hitRate,
451
- enabled: this.options.enableCaching,
452
- }
453
- }
454
-
455
- /**
456
- * Optimize route order based on usage patterns
457
- */
458
- optimizeRouteOrder(_usageStats: Record<string, number>): void {
459
- if (!this.options.enablePriorityOptimization)
460
- return
461
-
462
- // This would reorder routes based on actual usage patterns
463
- // For now, we'll just clear the cache to force recompilation
464
- this.matchCache.clear()
465
- }
466
-
467
- /**
468
- * Get route conflicts (overlapping patterns)
469
- */
470
- getRouteConflicts(): Array<{
471
- routes: CompiledRoute[]
472
- conflictType: 'overlap' | 'duplicate' | 'ambiguous'
473
- severity: 'low' | 'medium' | 'high'
474
- }> {
475
- const conflicts: Array<{
476
- routes: CompiledRoute[]
477
- conflictType: 'overlap' | 'duplicate' | 'ambiguous'
478
- severity: 'low' | 'medium' | 'high'
479
- }> = []
480
-
481
- const routes = this.trie.getAllRoutes()
482
- const routesByMethod = new Map<string, CompiledRoute[]>()
483
-
484
- // Group routes by method
485
- for (const route of routes) {
486
- const method = route.route.method
487
- if (!routesByMethod.has(method)) {
488
- routesByMethod.set(method, [])
489
- }
490
- routesByMethod.get(method)!.push(route)
491
- }
492
-
493
- // Check for conflicts within each method
494
- for (const [_method, methodRoutes] of routesByMethod) {
495
- for (let i = 0; i < methodRoutes.length; i++) {
496
- for (let j = i + 1; j < methodRoutes.length; j++) {
497
- const route1 = methodRoutes[i]
498
- const route2 = methodRoutes[j]
499
-
500
- if (this.routesConflict(route1, route2)) {
501
- const severity = this.getConflictSeverity(route1, route2)
502
- const conflictType = this.getConflictType(route1, route2)
503
-
504
- conflicts.push({
505
- routes: [route1, route2],
506
- conflictType,
507
- severity,
508
- })
509
- }
510
- }
511
- }
512
- }
513
-
514
- return conflicts
515
- }
516
-
517
- /**
518
- * Check if two routes conflict
519
- */
520
- private routesConflict(route1: CompiledRoute, route2: CompiledRoute): boolean {
521
- // Exact path match
522
- if (route1.route.path === route2.route.path) {
523
- return true
524
- }
525
-
526
- // If routes have different methods, they don't conflict
527
- if (route1.route.method !== route2.route.method) {
528
- return false
529
- }
530
-
531
- // Check if patterns could match the same paths
532
- return this.patternsOverlap(route1.segments, route2.segments)
533
- }
534
-
535
- /**
536
- * Check if two route patterns overlap
537
- */
538
- private patternsOverlap(segments1: any[], segments2: any[]): boolean {
539
- // If segment lengths differ and neither has optional parameters or wildcards,
540
- // they can't overlap
541
- const hasOptionalOrWildcard1 = segments1.some(seg => seg.optional || seg.type === 'wildcard')
542
- const hasOptionalOrWildcard2 = segments2.some(seg => seg.optional || seg.type === 'wildcard')
543
-
544
- if (segments1.length !== segments2.length && !hasOptionalOrWildcard1 && !hasOptionalOrWildcard2) {
545
- return false
546
- }
547
-
548
- const maxLength = Math.max(segments1.length, segments2.length)
549
- let potentialOverlap = true
550
-
551
- for (let i = 0; i < maxLength; i++) {
552
- const seg1 = i < segments1.length ? segments1[i] : null
553
- const seg2 = i < segments2.length ? segments2[i] : null
554
-
555
- // If one segment is missing and the other isn't optional, they can't overlap
556
- if (!seg1 && seg2 && !seg2.optional) {
557
- return false
558
- }
559
- if (!seg2 && seg1 && !seg1.optional) {
560
- return false
561
- }
562
-
563
- // If both segments are missing, continue
564
- if (!seg1 || !seg2) {
565
- continue
566
- }
567
-
568
- // Static segments must match exactly
569
- if (seg1.type === 'static' && seg2.type === 'static') {
570
- if (seg1.value !== seg2.value) {
571
- return false
572
- }
573
- }
574
- // Check for parameter constraints
575
- else if (seg1.type === 'parameter' && seg2.type === 'parameter') {
576
- // If both have patterns/constraints, check if they're mutually exclusive
577
- if (seg1.pattern && seg2.pattern) {
578
- // Simple check: if patterns are different, assume they might overlap
579
- // A more sophisticated check would analyze the regex patterns
580
- if (seg1.pattern.toString() === seg2.pattern.toString()) {
581
- potentialOverlap = true
582
- }
583
- }
584
-
585
- // If parameter names are the same, they likely represent the same entity
586
- // This is a heuristic that helps identify conflicts
587
- if (seg1.paramName === seg2.paramName) {
588
- potentialOverlap = true
589
- }
590
- }
591
- // One static, one parameter
592
- else if (
593
- (seg1.type === 'static' && seg2.type === 'parameter')
594
- || (seg1.type === 'parameter' && seg2.type === 'static')
595
- ) {
596
- const staticSeg = seg1.type === 'static' ? seg1 : seg2
597
- const paramSeg = seg1.type === 'parameter' ? seg1 : seg2
598
-
599
- // If parameter has a pattern, check if static value matches the pattern
600
- if (paramSeg.pattern) {
601
- if (!paramSeg.pattern.test(staticSeg.value)) {
602
- return false
603
- }
604
- }
605
- }
606
- // Wildcard matches anything
607
- else if (seg1.type === 'wildcard' || seg2.type === 'wildcard') {
608
- return true
609
- }
610
- }
611
-
612
- return potentialOverlap
613
- }
614
-
615
- /**
616
- * Get conflict severity
617
- */
618
- private getConflictSeverity(route1: CompiledRoute, route2: CompiledRoute): 'low' | 'medium' | 'high' {
619
- if (route1.route.path === route2.route.path) {
620
- return 'high' // Exact duplicate
621
- }
622
-
623
- if (route1.staticScore === route2.staticScore) {
624
- return 'medium' // Same specificity
625
- }
626
-
627
- return 'low' // Different specificity, priority will resolve
628
- }
629
-
630
- /**
631
- * Get conflict type
632
- */
633
- private getConflictType(route1: CompiledRoute, route2: CompiledRoute): 'overlap' | 'duplicate' | 'ambiguous' {
634
- if (route1.route.path === route2.route.path) {
635
- return 'duplicate'
636
- }
637
-
638
- // Check for ambiguous parameter patterns
639
- const hasAmbiguousParams = this.hasAmbiguousParameters(route1, route2)
640
- if (hasAmbiguousParams) {
641
- return 'ambiguous'
642
- }
643
-
644
- // Check for similar priority
645
- if (Math.abs(route1.priority - route2.priority) < 10) {
646
- return 'ambiguous'
647
- }
648
-
649
- return 'overlap'
650
- }
651
-
652
- /**
653
- * Check if routes have ambiguous parameters
654
- */
655
- private hasAmbiguousParameters(route1: CompiledRoute, route2: CompiledRoute): boolean {
656
- // If routes have different segment counts, they're less likely to be ambiguous
657
- if (Math.abs(route1.segments.length - route2.segments.length) > 1) {
658
- return false
659
- }
660
-
661
- // Count parameter segments in each route
662
- const paramCount1 = route1.segments.filter(s => s.type === 'parameter').length
663
- const paramCount2 = route2.segments.filter(s => s.type === 'parameter').length
664
-
665
- // If both routes have parameters in the same positions, they might be ambiguous
666
- if (paramCount1 > 0 && paramCount2 > 0) {
667
- const minLength = Math.min(route1.segments.length, route2.segments.length)
668
- let samePositionParams = 0
669
-
670
- for (let i = 0; i < minLength; i++) {
671
- if (route1.segments[i].type === 'parameter' && route2.segments[i].type === 'parameter') {
672
- samePositionParams++
673
-
674
- // If parameters have different constraints, they might still be distinct
675
- const hasPattern1 = !!route1.segments[i].pattern
676
- const hasPattern2 = !!route2.segments[i].pattern
677
-
678
- if (hasPattern1 !== hasPattern2) {
679
- return true // One has constraint, one doesn't = ambiguous
680
- }
681
- }
682
- }
683
-
684
- // If more than half of parameters are in the same positions, likely ambiguous
685
- return samePositionParams > 0
686
- && samePositionParams >= Math.min(paramCount1, paramCount2) / 2
687
- }
688
-
689
- return false
690
- }
691
- }