@stacksjs/bun-router 0.0.16 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +22 -8
- package/dist/chunk-1ahs68ys.js +18 -0
- package/dist/{chunk-0145g1me.js → chunk-cgptvjdf.js} +147 -75
- package/dist/chunk-g3ybefhg.js +1923 -0
- package/dist/cli.js +3 -2
- package/dist/container/container.d.ts +1 -0
- package/dist/container/contextual-binding.d.ts +0 -1
- package/dist/container/decorators.d.ts +2 -5
- package/dist/container/index.d.ts +16 -0
- package/dist/container/index.js +322 -0
- package/dist/file-serving/static-files.d.ts +16 -0
- package/dist/index.d.ts +17 -1
- package/dist/index.js +1159 -1380
- package/dist/middleware/cors.d.ts +9 -0
- package/dist/middleware/csrf.d.ts +7 -0
- package/dist/middleware/ddos_protection.d.ts +16 -1
- package/dist/middleware/rate_limit.d.ts +7 -1
- package/dist/middleware/request_tracer.d.ts +15 -9
- package/dist/middleware/response_cache.d.ts +2 -1
- package/dist/request/context.d.ts +12 -1
- package/dist/request/macros.d.ts +40 -2
- package/dist/response/macros.d.ts +3 -3
- package/dist/router/fluent-routing.d.ts +50 -13
- package/dist/router/handler-resolver.d.ts +8 -0
- package/dist/router/index.d.ts +2 -0
- package/dist/router/route-compiler.d.ts +8 -10
- package/dist/router/route-trie.d.ts +7 -1
- package/dist/router/router.d.ts +13 -2
- package/dist/router/validation-integration.d.ts +10 -4
- package/dist/types/middleware-types.d.ts +1 -1
- package/dist/types/route-inference.d.ts +9 -2
- package/dist/types.d.ts +62 -9
- package/dist/utils.d.ts +1 -1
- package/package.json +11 -1
- package/src/auth.ts +32 -12
- package/src/container/container.ts +58 -12
- package/src/container/contextual-binding.ts +4 -1
- package/src/container/decorators.ts +16 -7
- package/src/container/index.ts +49 -0
- package/src/errors/circuit-breaker.ts +3 -2
- package/src/errors/graceful-degradation.ts +2 -0
- package/src/file-serving/static-files.ts +144 -7
- package/src/index.ts +53 -1
- package/src/middleware/cors.ts +76 -8
- package/src/middleware/csrf.ts +70 -22
- package/src/middleware/ddos_protection.ts +27 -3
- package/src/middleware/input_validation.ts +10 -6
- package/src/middleware/performance_alerting.ts +2 -0
- package/src/middleware/performance_monitor.ts +2 -0
- package/src/middleware/rate_limit.ts +7 -1
- package/src/middleware/request_signing.ts +2 -0
- package/src/middleware/request_tracer.ts +34 -17
- package/src/middleware/response_cache.ts +48 -13
- package/src/middleware/session.ts +3 -3
- package/src/observability/metrics.ts +1 -1
- package/src/request/context.ts +50 -3
- package/src/request/enhanced-request.ts +20 -1
- package/src/request/macros.ts +183 -49
- package/src/response/macros.ts +3 -3
- package/src/router/file-based-routing.ts +17 -10
- package/src/router/fluent-routing.ts +117 -48
- package/src/router/group-organization.ts +17 -1
- package/src/router/handler-resolver.ts +36 -0
- package/src/router/http-methods.ts +33 -3
- package/src/router/index.ts +6 -0
- package/src/router/middleware.ts +3 -0
- package/src/router/optimized-route-matching.ts +58 -9
- package/src/router/route-compiler.ts +62 -73
- package/src/router/route-matching.ts +19 -0
- package/src/router/route-trie.ts +34 -48
- package/src/router/router.ts +72 -60
- package/src/router/server.ts +361 -139
- package/src/router/validation-integration.ts +11 -5
- package/src/testing/auth-testing.ts +6 -4
- package/src/types/middleware-types.ts +7 -4
- package/src/types/route-inference.ts +29 -14
- package/src/types.ts +63 -7
- package/src/url.ts +7 -2
- package/src/utils.ts +9 -6
- package/src/validation/validator.ts +7 -4
- package/src/websocket/clustering.ts +8 -1
- package/dist/router/fluent-router.d.ts +0 -315
- package/src/router/fluent-router.ts +0 -927
|
@@ -149,6 +149,8 @@ export class InMemoryNonceStore implements NonceStore {
|
|
|
149
149
|
|
|
150
150
|
constructor(cleanupIntervalMs: number = 60000) {
|
|
151
151
|
this.cleanupInterval = setInterval(() => this.cleanup(), cleanupIntervalMs)
|
|
152
|
+
// Background housekeeping must not keep the process alive
|
|
153
|
+
this.cleanupInterval.unref?.()
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
has(nonce: string): boolean {
|
|
@@ -9,18 +9,30 @@ export interface TraceSpan {
|
|
|
9
9
|
startTime: number
|
|
10
10
|
endTime?: number
|
|
11
11
|
duration?: number
|
|
12
|
-
tags: Record<string,
|
|
12
|
+
tags: Record<string, unknown>
|
|
13
13
|
logs: Array<{
|
|
14
14
|
timestamp: number
|
|
15
15
|
level: 'debug' | 'info' | 'warn' | 'error'
|
|
16
16
|
message: string
|
|
17
|
-
fields?: Record<string,
|
|
17
|
+
fields?: Record<string, unknown>
|
|
18
18
|
}>
|
|
19
19
|
status: 'ok' | 'error' | 'timeout'
|
|
20
20
|
error?: string
|
|
21
21
|
stackTrace?: string[]
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Destination for exported spans. `endpoint`/`headers` configure the
|
|
26
|
+
* HTTP-based exporters (jaeger/zipkin/otlp); `customExporter` receives
|
|
27
|
+
* the spans directly.
|
|
28
|
+
*/
|
|
29
|
+
export interface TraceExporterConfig {
|
|
30
|
+
type: 'console' | 'jaeger' | 'zipkin' | 'otlp' | 'custom'
|
|
31
|
+
endpoint?: string
|
|
32
|
+
headers?: Record<string, string>
|
|
33
|
+
customExporter?: (spans: TraceSpan[]) => Promise<void>
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
export interface TracingOptions {
|
|
25
37
|
enabled?: boolean
|
|
26
38
|
sampleRate?: number
|
|
@@ -30,12 +42,7 @@ export interface TracingOptions {
|
|
|
30
42
|
includeRequestBody?: boolean
|
|
31
43
|
includeResponseBody?: boolean
|
|
32
44
|
maxBodySize?: number
|
|
33
|
-
exporters?:
|
|
34
|
-
type: 'console' | 'jaeger' | 'zipkin' | 'otlp' | 'custom'
|
|
35
|
-
endpoint?: string
|
|
36
|
-
headers?: Record<string, string>
|
|
37
|
-
customExporter?: (spans: TraceSpan[]) => Promise<void>
|
|
38
|
-
}>
|
|
45
|
+
exporters?: TraceExporterConfig[]
|
|
39
46
|
propagation?: {
|
|
40
47
|
enabled?: boolean
|
|
41
48
|
headers?: string[]
|
|
@@ -83,6 +90,8 @@ export default class RequestTracer {
|
|
|
83
90
|
console.warn('Error exporting spans:', error)
|
|
84
91
|
}
|
|
85
92
|
}, 100) // Export every 100ms for faster testing
|
|
93
|
+
// Background export must not keep the process alive
|
|
94
|
+
this.exportInterval.unref?.()
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
private shouldSample(): boolean {
|
|
@@ -120,7 +129,7 @@ export default class RequestTracer {
|
|
|
120
129
|
return { traceId, parentSpanId }
|
|
121
130
|
}
|
|
122
131
|
|
|
123
|
-
private async captureRequestBody(req: EnhancedRequest): Promise<
|
|
132
|
+
private async captureRequestBody(req: EnhancedRequest): Promise<unknown> {
|
|
124
133
|
if (!this.options.includeRequestBody)
|
|
125
134
|
return undefined
|
|
126
135
|
|
|
@@ -157,7 +166,7 @@ export default class RequestTracer {
|
|
|
157
166
|
return undefined
|
|
158
167
|
}
|
|
159
168
|
|
|
160
|
-
private async captureResponseBody(response: Response): Promise<
|
|
169
|
+
private async captureResponseBody(response: Response): Promise<unknown> {
|
|
161
170
|
if (!this.options.includeResponseBody)
|
|
162
171
|
return undefined
|
|
163
172
|
|
|
@@ -203,7 +212,7 @@ export default class RequestTracer {
|
|
|
203
212
|
}
|
|
204
213
|
}
|
|
205
214
|
|
|
206
|
-
private addSpanTag(spanId: string, key: string, value:
|
|
215
|
+
private addSpanTag(spanId: string, key: string, value: unknown): void {
|
|
207
216
|
const span = this.spans.get(spanId)
|
|
208
217
|
if (span) {
|
|
209
218
|
span.tags[key] = value
|
|
@@ -247,15 +256,23 @@ export default class RequestTracer {
|
|
|
247
256
|
}
|
|
248
257
|
|
|
249
258
|
private async exportSpans(): Promise<void> {
|
|
250
|
-
//
|
|
251
|
-
|
|
259
|
+
// Drop orphaned spans: anything unfinished for over a minute will
|
|
260
|
+
// never be completed (its request died mid-flight) and would pin the
|
|
261
|
+
// map forever
|
|
262
|
+
if (this.spans.size > 0) {
|
|
263
|
+
const cutoff = performance.now() - 60_000
|
|
264
|
+
for (const [spanId, span] of this.spans) {
|
|
265
|
+
if (span.endTime === undefined && span.startTime < cutoff) {
|
|
266
|
+
this.spans.delete(spanId)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
252
270
|
|
|
253
271
|
if (this.exportQueue.length === 0) {
|
|
254
272
|
// Also check if there are any finished spans that need to be exported
|
|
255
273
|
// This ensures spans are exported even if they weren't properly moved to the queue
|
|
256
274
|
const finishedSpans = Array.from(this.spans.values()).filter(span => span.endTime !== undefined)
|
|
257
275
|
if (finishedSpans.length > 0) {
|
|
258
|
-
console.warn(`[RequestTracer] Found ${finishedSpans.length} finished spans to export`)
|
|
259
276
|
// Move finished spans to export queue
|
|
260
277
|
this.exportQueue.push(...finishedSpans)
|
|
261
278
|
// Remove them from the spans map
|
|
@@ -323,7 +340,7 @@ export default class RequestTracer {
|
|
|
323
340
|
}
|
|
324
341
|
}
|
|
325
342
|
|
|
326
|
-
private async exportToJaeger(spans: TraceSpan[], exporter:
|
|
343
|
+
private async exportToJaeger(spans: TraceSpan[], exporter: TraceExporterConfig): Promise<void> {
|
|
327
344
|
if (!exporter.endpoint)
|
|
328
345
|
return
|
|
329
346
|
|
|
@@ -366,7 +383,7 @@ export default class RequestTracer {
|
|
|
366
383
|
})
|
|
367
384
|
}
|
|
368
385
|
|
|
369
|
-
private async exportToZipkin(spans: TraceSpan[], exporter:
|
|
386
|
+
private async exportToZipkin(spans: TraceSpan[], exporter: TraceExporterConfig): Promise<void> {
|
|
370
387
|
if (!exporter.endpoint)
|
|
371
388
|
return
|
|
372
389
|
|
|
@@ -590,7 +607,7 @@ export default class RequestTracer {
|
|
|
590
607
|
return childSpanId
|
|
591
608
|
}
|
|
592
609
|
|
|
593
|
-
addTag(spanId: string, key: string, value:
|
|
610
|
+
addTag(spanId: string, key: string, value: unknown): void {
|
|
594
611
|
this.addSpanTag(spanId, key, value)
|
|
595
612
|
}
|
|
596
613
|
|
|
@@ -9,7 +9,9 @@ export interface CacheEntry {
|
|
|
9
9
|
status: number
|
|
10
10
|
statusText: string
|
|
11
11
|
headers: Record<string, string>
|
|
12
|
-
|
|
12
|
+
// In-memory entries hold the raw bytes; entries round-tripped through
|
|
13
|
+
// the JSON file cache come back as a plain number array
|
|
14
|
+
body: Uint8Array | number[]
|
|
13
15
|
contentType: string
|
|
14
16
|
}
|
|
15
17
|
createdAt: number
|
|
@@ -73,6 +75,7 @@ export class ResponseCache implements Middleware {
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
private cleanupInterval?: ReturnType<typeof setInterval>
|
|
78
|
+
private compiledInvalidationPatterns: RegExp[] = []
|
|
76
79
|
|
|
77
80
|
constructor(options: ResponseCacheOptions = {}) {
|
|
78
81
|
this.options = {
|
|
@@ -121,6 +124,20 @@ export class ResponseCache implements Middleware {
|
|
|
121
124
|
},
|
|
122
125
|
} as Required<ResponseCacheOptions>
|
|
123
126
|
|
|
127
|
+
// Compile invalidation patterns once — matching per request with
|
|
128
|
+
// `new RegExp(pattern)` recompiled the regex on every invalidation check
|
|
129
|
+
this.compiledInvalidationPatterns = (this.options.invalidation.patterns ?? [])
|
|
130
|
+
.map((pattern) => {
|
|
131
|
+
try {
|
|
132
|
+
return new RegExp(pattern)
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
console.warn(`[ResponseCache] Invalid invalidation pattern: ${pattern}`)
|
|
136
|
+
return null
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
.filter((re): re is RegExp => re !== null)
|
|
140
|
+
|
|
124
141
|
this.initializeStorage()
|
|
125
142
|
this.startCleanupInterval()
|
|
126
143
|
}
|
|
@@ -142,6 +159,8 @@ export class ResponseCache implements Middleware {
|
|
|
142
159
|
this.cleanupInterval = setInterval(() => {
|
|
143
160
|
void this.cleanup()
|
|
144
161
|
}, 60000) // Cleanup every minute
|
|
162
|
+
// Background housekeeping must not keep the process alive
|
|
163
|
+
this.cleanupInterval.unref?.()
|
|
145
164
|
}
|
|
146
165
|
|
|
147
166
|
public async cleanup(): Promise<void> {
|
|
@@ -370,7 +389,12 @@ export class ResponseCache implements Middleware {
|
|
|
370
389
|
|
|
371
390
|
try {
|
|
372
391
|
const filePath = join(this.options.storage.directory, `${key}.cache`)
|
|
373
|
-
|
|
392
|
+
// Uint8Array doesn't JSON-serialize as an array — convert at the
|
|
393
|
+
// file boundary only (memory entries keep the compact byte view)
|
|
394
|
+
const serializable = entry.response.body instanceof Uint8Array
|
|
395
|
+
? { ...entry, response: { ...entry.response, body: Array.from(entry.response.body) } }
|
|
396
|
+
: entry
|
|
397
|
+
const content = JSON.stringify(serializable)
|
|
374
398
|
await Bun.write(filePath, content)
|
|
375
399
|
}
|
|
376
400
|
catch (error) {
|
|
@@ -574,9 +598,11 @@ export class ResponseCache implements Middleware {
|
|
|
574
598
|
}
|
|
575
599
|
|
|
576
600
|
private async createCacheEntry(req: EnhancedRequest, res: Response): Promise<{ entry: CacheEntry, response: Response }> {
|
|
577
|
-
//
|
|
578
|
-
|
|
579
|
-
|
|
601
|
+
// Read the body once and rebuild the outgoing response from the same
|
|
602
|
+
// bytes — cheaper than clone(), which tees the stream and buffers it
|
|
603
|
+
// twice. Bytes are kept as a Uint8Array (the old number[] conversion
|
|
604
|
+
// boxed every byte into a JS number array element).
|
|
605
|
+
const body = await res.arrayBuffer()
|
|
580
606
|
const bodyBytes = new Uint8Array(body)
|
|
581
607
|
const ttl = this.getTTL(req)
|
|
582
608
|
const now = Date.now()
|
|
@@ -587,7 +613,7 @@ export class ResponseCache implements Middleware {
|
|
|
587
613
|
status: res.status,
|
|
588
614
|
statusText: res.statusText,
|
|
589
615
|
headers: Object.fromEntries(res.headers.entries()),
|
|
590
|
-
body:
|
|
616
|
+
body: bodyBytes,
|
|
591
617
|
contentType: res.headers.get('Content-Type') || 'text/plain',
|
|
592
618
|
},
|
|
593
619
|
createdAt: now,
|
|
@@ -595,21 +621,31 @@ export class ResponseCache implements Middleware {
|
|
|
595
621
|
size: bodyBytes.length,
|
|
596
622
|
}
|
|
597
623
|
|
|
624
|
+
const outHeaders = new Headers(res.headers)
|
|
625
|
+
|
|
598
626
|
// Generate ETag if enabled
|
|
599
627
|
if (this.options.etag && this.options.etag.enabled) {
|
|
600
628
|
entry.etag = this.generateETag(bodyBytes)
|
|
601
629
|
if (entry.etag) {
|
|
602
630
|
entry.response.headers.ETag = entry.etag
|
|
603
|
-
|
|
631
|
+
outHeaders.set('ETag', entry.etag)
|
|
604
632
|
}
|
|
605
633
|
}
|
|
606
634
|
|
|
607
635
|
// Set Last-Modified
|
|
608
636
|
entry.lastModified = new Date(now).toUTCString()
|
|
609
637
|
entry.response.headers['Last-Modified'] = entry.lastModified
|
|
610
|
-
|
|
638
|
+
outHeaders.set('Last-Modified', entry.lastModified)
|
|
611
639
|
|
|
612
|
-
|
|
640
|
+
const outBody = res.status === 204 || res.status === 304 ? null : bodyBytes
|
|
641
|
+
return {
|
|
642
|
+
entry,
|
|
643
|
+
response: new Response(outBody, {
|
|
644
|
+
status: res.status,
|
|
645
|
+
statusText: res.statusText,
|
|
646
|
+
headers: outHeaders,
|
|
647
|
+
}),
|
|
648
|
+
}
|
|
613
649
|
}
|
|
614
650
|
|
|
615
651
|
private createResponseFromEntry(entry: CacheEntry): Response {
|
|
@@ -645,13 +681,12 @@ export class ResponseCache implements Middleware {
|
|
|
645
681
|
|
|
646
682
|
async invalidateCache(req?: EnhancedRequest): Promise<void> {
|
|
647
683
|
if (req) {
|
|
648
|
-
|
|
649
|
-
if (patterns && patterns.length > 0) {
|
|
684
|
+
if (this.compiledInvalidationPatterns.length > 0) {
|
|
650
685
|
// Invalidate specific patterns
|
|
651
686
|
const url = new URL(req.url)
|
|
652
687
|
|
|
653
|
-
for (const pattern of
|
|
654
|
-
if (url.pathname
|
|
688
|
+
for (const pattern of this.compiledInvalidationPatterns) {
|
|
689
|
+
if (pattern.test(url.pathname)) {
|
|
655
690
|
await this.clearCache()
|
|
656
691
|
return
|
|
657
692
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EnhancedRequest, NextFunction } from '../types'
|
|
1
|
+
import type { CookieOptions, EnhancedRequest, NextFunction } from '../types'
|
|
2
2
|
import type { SessionStore, SessionData } from '../types/core'
|
|
3
3
|
import { config } from '../config'
|
|
4
4
|
import { MemorySessionStore } from '../session/memory-store'
|
|
@@ -49,7 +49,7 @@ export default class Session {
|
|
|
49
49
|
|
|
50
50
|
// Create session manager and attach to request
|
|
51
51
|
const manager = new SessionManager(sessionId, sessionData, this.store)
|
|
52
|
-
req.session = manager
|
|
52
|
+
req.session = manager
|
|
53
53
|
|
|
54
54
|
// Continue to next middleware/handler
|
|
55
55
|
const response = await next()
|
|
@@ -58,7 +58,7 @@ export default class Session {
|
|
|
58
58
|
await manager.save()
|
|
59
59
|
|
|
60
60
|
// Set session cookie on the response
|
|
61
|
-
const cookieOptions:
|
|
61
|
+
const cookieOptions: CookieOptions = {
|
|
62
62
|
maxAge: sessionConfig?.cookie?.maxAge || 86400000,
|
|
63
63
|
path: sessionConfig?.cookie?.path || '/',
|
|
64
64
|
}
|
|
@@ -520,7 +520,7 @@ export class MetricsRegistry {
|
|
|
520
520
|
if (this.config.collectInterval && this.config.collectInterval > 0) {
|
|
521
521
|
setInterval(() => {
|
|
522
522
|
this.collectSystemMetrics()
|
|
523
|
-
}, this.config.collectInterval)
|
|
523
|
+
}, this.config.collectInterval).unref?.()
|
|
524
524
|
}
|
|
525
525
|
}
|
|
526
526
|
|
package/src/request/context.ts
CHANGED
|
@@ -17,12 +17,45 @@ interface RequestContextStore {
|
|
|
17
17
|
|
|
18
18
|
const storage: AsyncLocalStorage<RequestContextStore> = new AsyncLocalStorage<RequestContextStore>()
|
|
19
19
|
|
|
20
|
+
// AsyncLocalStorage.run costs roughly a microsecond per request — a large
|
|
21
|
+
// share of total dispatch time — so it only runs once something actually
|
|
22
|
+
// uses the context API. Until then a plain module variable tracks the
|
|
23
|
+
// in-flight request: exact in the synchronous phase of a handler and for
|
|
24
|
+
// non-overlapping requests, which covers the call that flips
|
|
25
|
+
// `contextEnabled`. Every request that starts after the first use gets
|
|
26
|
+
// full ALS isolation. Apps that need guaranteed isolation from the very
|
|
27
|
+
// first request under concurrency can call `enableRequestContext()` at
|
|
28
|
+
// startup.
|
|
29
|
+
let contextEnabled = false
|
|
30
|
+
let syncCurrent: EnhancedRequest | null = null
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Force AsyncLocalStorage-backed request context on for all subsequent
|
|
34
|
+
* requests. Calling `request()`/`getCurrentRequest()` enables it
|
|
35
|
+
* automatically — use this when the very first request must already be
|
|
36
|
+
* fully isolated under concurrent load.
|
|
37
|
+
*/
|
|
38
|
+
export function enableRequestContext(): void {
|
|
39
|
+
contextEnabled = true
|
|
40
|
+
}
|
|
41
|
+
|
|
20
42
|
/**
|
|
21
|
-
* Run `fn` inside
|
|
43
|
+
* Run `fn` inside a request-context scope where `request()` and
|
|
22
44
|
* `getCurrentRequest()` resolve to `initial`. The Router calls this around
|
|
23
45
|
* each request automatically.
|
|
24
46
|
*/
|
|
25
47
|
export function runWithRequest<T>(initial: EnhancedRequest | Request, fn: () => T | Promise<T>): T | Promise<T> {
|
|
48
|
+
if (!contextEnabled) {
|
|
49
|
+
syncCurrent = initial as EnhancedRequest
|
|
50
|
+
const result = fn()
|
|
51
|
+
if (result instanceof Promise) {
|
|
52
|
+
return result.finally(() => {
|
|
53
|
+
syncCurrent = null
|
|
54
|
+
}) as Promise<T>
|
|
55
|
+
}
|
|
56
|
+
syncCurrent = null
|
|
57
|
+
return result
|
|
58
|
+
}
|
|
26
59
|
return storage.run({ current: initial as EnhancedRequest }, fn)
|
|
27
60
|
}
|
|
28
61
|
|
|
@@ -33,16 +66,30 @@ export function runWithRequest<T>(initial: EnhancedRequest | Request, fn: () =>
|
|
|
33
66
|
*/
|
|
34
67
|
export function setCurrentRequest(req: EnhancedRequest): void {
|
|
35
68
|
const store = storage.getStore()
|
|
36
|
-
if (store)
|
|
69
|
+
if (store) {
|
|
37
70
|
store.current = req
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
// Pre-enablement fallback: only refresh it while a request is in flight
|
|
74
|
+
if (syncCurrent !== null) {
|
|
75
|
+
syncCurrent = req
|
|
76
|
+
}
|
|
38
77
|
}
|
|
39
78
|
|
|
40
79
|
/**
|
|
41
80
|
* Returns the active request, or `undefined` when called outside a request
|
|
42
81
|
* scope. Prefer `request()` when the request is required.
|
|
82
|
+
*
|
|
83
|
+
* First use switches the router to AsyncLocalStorage-backed context for
|
|
84
|
+
* all subsequent requests; this call itself resolves via a synchronous
|
|
85
|
+
* fallback (see `enableRequestContext` for eager opt-in).
|
|
43
86
|
*/
|
|
44
87
|
export function getCurrentRequest(): EnhancedRequest | undefined {
|
|
45
|
-
|
|
88
|
+
if (!contextEnabled) {
|
|
89
|
+
contextEnabled = true
|
|
90
|
+
return syncCurrent ?? undefined
|
|
91
|
+
}
|
|
92
|
+
return storage.getStore()?.current ?? syncCurrent ?? undefined
|
|
46
93
|
}
|
|
47
94
|
|
|
48
95
|
/**
|
|
@@ -433,8 +433,21 @@ export function enhanceRequestWithMethods(request: EnhancedRequest): EnhancedReq
|
|
|
433
433
|
_oldInput?: RequestInput
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
-
//
|
|
436
|
+
// Merged input (query + JSON body + form body + route params).
|
|
437
|
+
// Memoized: many of the helpers below call this several times per
|
|
438
|
+
// request, and the merge result only changes when a body is parsed
|
|
439
|
+
// later — so the cache keys off the body object identities.
|
|
440
|
+
let cachedInput: RequestInput | null = null
|
|
441
|
+
let cachedJsonBody: unknown
|
|
442
|
+
let cachedFormBody: unknown
|
|
443
|
+
|
|
437
444
|
const getAllInput = (): RequestInput => {
|
|
445
|
+
if (cachedInput
|
|
446
|
+
&& cachedJsonBody === request.jsonBody
|
|
447
|
+
&& cachedFormBody === request.formBody) {
|
|
448
|
+
return cachedInput
|
|
449
|
+
}
|
|
450
|
+
|
|
438
451
|
const input: RequestInput = {}
|
|
439
452
|
|
|
440
453
|
// Query parameters
|
|
@@ -465,6 +478,9 @@ export function enhanceRequestWithMethods(request: EnhancedRequest): EnhancedReq
|
|
|
465
478
|
}
|
|
466
479
|
}
|
|
467
480
|
|
|
481
|
+
cachedInput = input
|
|
482
|
+
cachedJsonBody = request.jsonBody
|
|
483
|
+
cachedFormBody = request.formBody
|
|
468
484
|
return input
|
|
469
485
|
}
|
|
470
486
|
|
|
@@ -827,6 +843,9 @@ export function enhanceRequestWithMethods(request: EnhancedRequest): EnhancedReq
|
|
|
827
843
|
else {
|
|
828
844
|
(request as any).jsonBody = data
|
|
829
845
|
}
|
|
846
|
+
// In-place mutation doesn't change jsonBody's identity, so drop the
|
|
847
|
+
// memoized merge explicitly
|
|
848
|
+
cachedInput = null
|
|
830
849
|
}
|
|
831
850
|
|
|
832
851
|
/**
|