@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
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Server } from 'bun';
|
|
2
2
|
import type { Router } from './router/router';
|
|
3
|
+
import type { SessionManager } from './session/index';
|
|
3
4
|
import type { QueryPreservationConfig } from './utils/query-preservation';
|
|
4
5
|
import type { AuthContext, AuthenticatedUser, BoundModels, CacheAdapter, CacheStats, CookieToDelete, CookieToSet, ErrorMetadata, FallbackError, FileInfo, FlashMessages, FormBody, InputValidationSchemas, InputValue, JsonBodyData, JwtHeader, JwtPayload, MetricEntry, MiddlewareParams, OAuth2Flows, OAuth2Profile, RequestContext, RequestInput, RouteMetadata, SanitizationOptions, SanitizationRule, SessionData, SessionSerializer, SessionStore, SSEConnection, StaticResponseBody, TemplateHelper, TemplateHelpers, TraceAttributes, TraceSpan, TypedServerWebSocket, User, ValidatedBody, ValidatedData, ValidationSchema, WebSocketData } from './types/core';
|
|
5
6
|
export type { QueryPreservationConfig } from './utils/query-preservation';
|
|
@@ -702,6 +703,12 @@ export interface EnhancedRequest extends Request, Omit<RequestMacroMethods, 'ip'
|
|
|
702
703
|
* Parsed JSON body (if Content-Type is application/json)
|
|
703
704
|
*/
|
|
704
705
|
jsonBody?: Record<string, unknown>;
|
|
706
|
+
/**
|
|
707
|
+
* Raw unparsed request body cache, populated on first `rawBody()` read (or up
|
|
708
|
+
* front by a framework body parser). Kept so webhook signature checks get the
|
|
709
|
+
* exact bytes the client sent.
|
|
710
|
+
*/
|
|
711
|
+
_rawBody?: string;
|
|
705
712
|
/**
|
|
706
713
|
* Form body data (if Content-Type is multipart/form-data or application/x-www-form-urlencoded)
|
|
707
714
|
*/
|
|
@@ -711,9 +718,11 @@ export interface EnhancedRequest extends Request, Omit<RequestMacroMethods, 'ip'
|
|
|
711
718
|
*/
|
|
712
719
|
files?: UploadedFile[];
|
|
713
720
|
/**
|
|
714
|
-
* Session
|
|
721
|
+
* Session for the current request. The built-in session middleware
|
|
722
|
+
* attaches a `SessionManager` (with `get`/`set`/`flash`/… methods);
|
|
723
|
+
* testing utilities may attach plain `SessionData`.
|
|
715
724
|
*/
|
|
716
|
-
session?: SessionData;
|
|
725
|
+
session?: SessionManager | SessionData;
|
|
717
726
|
/**
|
|
718
727
|
* User data (if authentication middleware is used)
|
|
719
728
|
*/
|
|
@@ -936,6 +945,13 @@ export interface Route {
|
|
|
936
945
|
pattern?: {
|
|
937
946
|
exec: (url: URL) => PatternMatchResult | null;
|
|
938
947
|
};
|
|
948
|
+
/**
|
|
949
|
+
* Set to `false` (via `router.withoutNativeDispatch()`) to keep this
|
|
950
|
+
* route on the fetch-handler matcher even when the server runs with
|
|
951
|
+
* `nativeRoutes: true` — e.g. when it must respect registration-order
|
|
952
|
+
* precedence against an overlapping pattern.
|
|
953
|
+
*/
|
|
954
|
+
nativeDispatch?: boolean;
|
|
939
955
|
}
|
|
940
956
|
/**
|
|
941
957
|
* WebSocket handler configuration
|
|
@@ -967,6 +983,22 @@ export interface ServerOptions<T extends WebSocketData = WebSocketData> extends
|
|
|
967
983
|
* These bypass the fetch handler entirely for zero-allocation serving (~15% faster).
|
|
968
984
|
*/
|
|
969
985
|
static?: Record<string, Response>;
|
|
986
|
+
/**
|
|
987
|
+
* Opt-in: register compatible routes with `Bun.serve`'s native router
|
|
988
|
+
* (`routes`), skipping the fetch handler's URL parsing and route
|
|
989
|
+
* matching for them. Compatible means: standard HTTP method, no
|
|
990
|
+
* domain scoping, no `where()`/inline constraints, no optional
|
|
991
|
+
* params, and `{param}`s spanning whole segments. Everything else —
|
|
992
|
+
* incompatible routes, 404/405 handling, HEAD fallback, the generic
|
|
993
|
+
* OPTIONS preflight — still flows through the fetch handler with
|
|
994
|
+
* identical semantics.
|
|
995
|
+
*
|
|
996
|
+
* Caveat: Bun matches by specificity (exact > param > wildcard).
|
|
997
|
+
* Apps that rely on registration order between same-shape overlapping
|
|
998
|
+
* patterns should leave this off. Routes registered after `serve()`
|
|
999
|
+
* only join the native table after `reload()`.
|
|
1000
|
+
*/
|
|
1001
|
+
nativeRoutes?: boolean;
|
|
970
1002
|
/**
|
|
971
1003
|
* Development mode options for Bun's dev server.
|
|
972
1004
|
* When true, enables HMR and development features.
|
|
@@ -1085,11 +1117,30 @@ export type MiddlewareWithParams<T extends string = string> = T | `${T}:${string
|
|
|
1085
1117
|
*/
|
|
1086
1118
|
export type RouteParam<T extends string> = T extends `{${infer P}}` ? P : never;
|
|
1087
1119
|
/**
|
|
1088
|
-
*
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1120
|
+
* Strip an inline constraint from a parameter token:
|
|
1121
|
+
* `id:[0-9]+` → `id`, `slug` → `slug`
|
|
1122
|
+
*/
|
|
1123
|
+
export type ExtractParamName<T extends string> = T extends `${infer Name}:${string}` ? Name : T;
|
|
1124
|
+
/**
|
|
1125
|
+
* Extract parameters from a route path into a narrowly-typed object.
|
|
1126
|
+
*
|
|
1127
|
+
* Handles the full parameter grammar:
|
|
1128
|
+
* - `{id}` → `{ id: string }`
|
|
1129
|
+
* - `{id?}` (optional) → `{ id?: string }`
|
|
1130
|
+
* - `{id:[0-9]+}` (inline constraint) → `{ id: string }`
|
|
1131
|
+
* - trailing `*` wildcard → `{ wildcard: string }`
|
|
1132
|
+
*
|
|
1133
|
+
* @example
|
|
1134
|
+
* type P = ExtractRouteParams<'/users/{userId}/posts/{slug?}'>
|
|
1135
|
+
* // ^? { userId: string } & { slug?: string }
|
|
1136
|
+
*/
|
|
1137
|
+
export type ExtractRouteParams<T extends string> = T extends `${string}{${infer Param}}${infer Rest}` ? (Param extends `${infer Inner}?` ? {
|
|
1138
|
+
[K in ExtractParamName<Inner>]?: string;
|
|
1139
|
+
} : {
|
|
1140
|
+
[K in ExtractParamName<Param>]: string;
|
|
1141
|
+
}) & ExtractRouteParams<Rest> : T extends `${string}*${string}` ? {
|
|
1142
|
+
wildcard: string;
|
|
1143
|
+
} : object;
|
|
1093
1144
|
/**
|
|
1094
1145
|
* Route method constraints
|
|
1095
1146
|
*/
|
|
@@ -1520,8 +1571,8 @@ export interface StreamingOptions {
|
|
|
1520
1571
|
/**
|
|
1521
1572
|
* SSE (Server-Sent Events) data structure
|
|
1522
1573
|
*/
|
|
1523
|
-
export interface SSEData {
|
|
1524
|
-
data:
|
|
1574
|
+
export interface SSEData<T = unknown> {
|
|
1575
|
+
data: T;
|
|
1525
1576
|
event?: string;
|
|
1526
1577
|
id?: string;
|
|
1527
1578
|
retry?: number;
|
|
@@ -1701,6 +1752,8 @@ export interface RequestMacroMethods {
|
|
|
1701
1752
|
username: string;
|
|
1702
1753
|
password: string;
|
|
1703
1754
|
} | null;
|
|
1755
|
+
/** Raw unparsed request body as a string (cached). Needed for webhook signature verification (Stripe/GitHub/Slack), where a re-serialized jsonBody won't match the HMAC. */
|
|
1756
|
+
rawBody: () => Promise<string>;
|
|
1704
1757
|
hasHeader: (name: string) => boolean;
|
|
1705
1758
|
header: (name: string, defaultValue?: string) => string | null;
|
|
1706
1759
|
allHeaders: () => Record<string, string>;
|
package/dist/utils.d.ts
CHANGED
|
@@ -66,4 +66,4 @@ export declare function resolveViewPath(viewName: string, viewsPath: string, ext
|
|
|
66
66
|
* @param template The HTML template string
|
|
67
67
|
* @param data The data to inject into the template
|
|
68
68
|
*/
|
|
69
|
-
export declare function processHtmlTemplate(template: string, data: Record<string,
|
|
69
|
+
export declare function processHtmlTemplate(template: string, data: Record<string, unknown>): string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/bun-router",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.17",
|
|
5
5
|
"description": "A fast, type-safe router for Bun.",
|
|
6
6
|
"author": "Chris Breuer <chris@stacksjs.org>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
29
29
|
"import": "./dist/index.js"
|
|
30
30
|
},
|
|
31
|
+
"./container": {
|
|
32
|
+
"bun": "./dist/container/index.js",
|
|
33
|
+
"types": "./dist/container/index.d.ts",
|
|
34
|
+
"import": "./dist/container/index.js"
|
|
35
|
+
},
|
|
31
36
|
"./*": {
|
|
32
37
|
"bun": "./dist/*",
|
|
33
38
|
"import": "./dist/*"
|
|
@@ -40,6 +45,11 @@
|
|
|
40
45
|
"types": "./dist/index.d.ts",
|
|
41
46
|
"import": "./dist/index.js"
|
|
42
47
|
},
|
|
48
|
+
"./container": {
|
|
49
|
+
"bun": "./dist/container/index.js",
|
|
50
|
+
"types": "./dist/container/index.d.ts",
|
|
51
|
+
"import": "./dist/container/index.js"
|
|
52
|
+
},
|
|
43
53
|
"./*": {
|
|
44
54
|
"bun": "./dist/*",
|
|
45
55
|
"import": "./dist/*"
|
package/src/auth.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { EnhancedRequest, JwtPayload } from './types'
|
|
1
|
+
import type { EnhancedRequest, JwtHeader, JwtPayload } from './types'
|
|
2
|
+
import { timingSafeEqual } from 'node:crypto'
|
|
2
3
|
|
|
3
4
|
// Types
|
|
4
5
|
export interface JwtVerifyOptions {
|
|
@@ -22,10 +23,25 @@ export interface JwtSignOptions {
|
|
|
22
23
|
keyid?: string
|
|
23
24
|
jwtid?: string
|
|
24
25
|
noTimestamp?: boolean
|
|
25
|
-
header?:
|
|
26
|
+
header?: Partial<JwtHeader>
|
|
26
27
|
encoding?: string
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Token endpoint response for the OAuth2 authorization-code flow
|
|
32
|
+
* (RFC 6749 §5.1). Providers may include extra fields — they're preserved
|
|
33
|
+
* under the index signature.
|
|
34
|
+
*/
|
|
35
|
+
export interface OAuth2TokenResponse {
|
|
36
|
+
access_token: string
|
|
37
|
+
token_type: string
|
|
38
|
+
expires_in?: number
|
|
39
|
+
refresh_token?: string
|
|
40
|
+
scope?: string
|
|
41
|
+
id_token?: string
|
|
42
|
+
[key: string]: string | number | undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
export interface ApiKeyOptions {
|
|
30
46
|
source?: 'header' | 'query' | 'cookie'
|
|
31
47
|
keyName?: string
|
|
@@ -74,7 +90,7 @@ export class JWT {
|
|
|
74
90
|
/**
|
|
75
91
|
* Sign payload into a JWT token
|
|
76
92
|
*/
|
|
77
|
-
sign(payload:
|
|
93
|
+
sign(payload: JwtPayload, options: JwtSignOptions = {}): string {
|
|
78
94
|
// Default header
|
|
79
95
|
const header = {
|
|
80
96
|
alg: options.algorithm || 'HS256',
|
|
@@ -132,7 +148,7 @@ export class JWT {
|
|
|
132
148
|
/**
|
|
133
149
|
* Verify a JWT token and return the decoded payload
|
|
134
150
|
*/
|
|
135
|
-
verify(token: string, options: JwtVerifyOptions = {}):
|
|
151
|
+
verify(token: string, options: JwtVerifyOptions = {}): JwtPayload | null {
|
|
136
152
|
try {
|
|
137
153
|
const parts = token.split('.')
|
|
138
154
|
|
|
@@ -143,8 +159,8 @@ export class JWT {
|
|
|
143
159
|
const [encodedHeader, encodedPayload, signature] = parts
|
|
144
160
|
|
|
145
161
|
// Decode header and payload
|
|
146
|
-
const header = JSON.parse(base64UrlDecode(encodedHeader))
|
|
147
|
-
const payload = JSON.parse(base64UrlDecode(encodedPayload))
|
|
162
|
+
const header = JSON.parse(base64UrlDecode(encodedHeader)) as JwtHeader
|
|
163
|
+
const payload = JSON.parse(base64UrlDecode(encodedPayload)) as JwtPayload
|
|
148
164
|
|
|
149
165
|
// Verify algorithm if specified
|
|
150
166
|
if (options.algorithms && !options.algorithms.includes(header.alg)) {
|
|
@@ -156,7 +172,11 @@ export class JWT {
|
|
|
156
172
|
const data = `${encodedHeader}.${encodedPayload}`
|
|
157
173
|
const expectedSignature = this.createSignature(data, this.secret, header.alg)
|
|
158
174
|
|
|
159
|
-
|
|
175
|
+
// Constant-time comparison — `!==` leaks how much of the signature
|
|
176
|
+
// matched through response timing
|
|
177
|
+
const sigBuf = Buffer.from(signature)
|
|
178
|
+
const expectedBuf = Buffer.from(expectedSignature)
|
|
179
|
+
if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {
|
|
160
180
|
throw new Error('Invalid signature')
|
|
161
181
|
}
|
|
162
182
|
|
|
@@ -200,7 +220,7 @@ export class JWT {
|
|
|
200
220
|
/**
|
|
201
221
|
* Decode a JWT token without verifying the signature
|
|
202
222
|
*/
|
|
203
|
-
decode(token: string): { header:
|
|
223
|
+
decode(token: string): { header: JwtHeader, payload: JwtPayload } | null {
|
|
204
224
|
try {
|
|
205
225
|
const parts = token.split('.')
|
|
206
226
|
|
|
@@ -402,7 +422,7 @@ export class OAuth2Helper {
|
|
|
402
422
|
/**
|
|
403
423
|
* Exchange authorization code for access token
|
|
404
424
|
*/
|
|
405
|
-
async exchangeCodeForToken(code: string): Promise<
|
|
425
|
+
async exchangeCodeForToken(code: string): Promise<OAuth2TokenResponse> {
|
|
406
426
|
const params = new URLSearchParams({
|
|
407
427
|
grant_type: 'authorization_code',
|
|
408
428
|
code,
|
|
@@ -423,13 +443,13 @@ export class OAuth2Helper {
|
|
|
423
443
|
throw new Error(`OAuth token exchange failed: ${response.status} ${response.statusText}`)
|
|
424
444
|
}
|
|
425
445
|
|
|
426
|
-
return await response.json() as
|
|
446
|
+
return await response.json() as OAuth2TokenResponse
|
|
427
447
|
}
|
|
428
448
|
|
|
429
449
|
/**
|
|
430
450
|
* Refresh an access token using a refresh token
|
|
431
451
|
*/
|
|
432
|
-
async refreshToken(refreshToken: string): Promise<
|
|
452
|
+
async refreshToken(refreshToken: string): Promise<OAuth2TokenResponse> {
|
|
433
453
|
const params = new URLSearchParams({
|
|
434
454
|
grant_type: 'refresh_token',
|
|
435
455
|
refresh_token: refreshToken,
|
|
@@ -449,7 +469,7 @@ export class OAuth2Helper {
|
|
|
449
469
|
throw new Error(`OAuth token refresh failed: ${response.status} ${response.statusText}`)
|
|
450
470
|
}
|
|
451
471
|
|
|
452
|
-
return await response.json() as
|
|
472
|
+
return await response.json() as OAuth2TokenResponse
|
|
453
473
|
}
|
|
454
474
|
}
|
|
455
475
|
|
|
@@ -86,6 +86,9 @@ export interface InjectMetadata {
|
|
|
86
86
|
*/
|
|
87
87
|
export class Container {
|
|
88
88
|
private bindings = new Map<Token, Binding>()
|
|
89
|
+
// Tag → tokens index, maintained on register/unbind so tagged lookups
|
|
90
|
+
// are O(matches) instead of scanning every binding
|
|
91
|
+
private tagIndex = new Map<string, Set<Token>>()
|
|
89
92
|
private instances = new Map<Token, any>()
|
|
90
93
|
private scopedInstances = new Map<string, Map<Token, any>>()
|
|
91
94
|
private interceptors = new Map<Token, ((..._args: any[]) => any)[]>()
|
|
@@ -115,7 +118,26 @@ export class Container {
|
|
|
115
118
|
* Register a binding
|
|
116
119
|
*/
|
|
117
120
|
register<T>(binding: Binding<T>): this {
|
|
121
|
+
// Re-registering a token replaces its tag index entries
|
|
122
|
+
const existing = this.bindings.get(binding.token)
|
|
123
|
+
if (existing?.metadata?.tags) {
|
|
124
|
+
for (const tag of existing.metadata.tags) {
|
|
125
|
+
this.tagIndex.get(tag)?.delete(binding.token)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
118
129
|
this.bindings.set(binding.token, binding)
|
|
130
|
+
|
|
131
|
+
if (binding.metadata?.tags) {
|
|
132
|
+
for (const tag of binding.metadata.tags) {
|
|
133
|
+
let tokens = this.tagIndex.get(tag)
|
|
134
|
+
if (!tokens) {
|
|
135
|
+
tokens = new Set()
|
|
136
|
+
this.tagIndex.set(tag, tokens)
|
|
137
|
+
}
|
|
138
|
+
tokens.add(binding.token)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
119
141
|
return this
|
|
120
142
|
}
|
|
121
143
|
|
|
@@ -177,31 +199,48 @@ export class Container {
|
|
|
177
199
|
* Resolve all instances of a token (useful for arrays of services)
|
|
178
200
|
*/
|
|
179
201
|
resolveAll<T>(token: Token): T[] {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
)
|
|
202
|
+
// Direct binding plus anything tagged with the token (when it's a
|
|
203
|
+
// string), deduplicated by token
|
|
204
|
+
const tokens = new Set<Token>()
|
|
205
|
+
if (this.bindings.has(token)) {
|
|
206
|
+
tokens.add(token)
|
|
207
|
+
}
|
|
208
|
+
if (typeof token === 'string') {
|
|
209
|
+
for (const tagged of this.tagIndex.get(token) ?? []) {
|
|
210
|
+
tokens.add(tagged)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
183
213
|
|
|
184
|
-
return
|
|
214
|
+
return Array.from(tokens, t => this.resolve<T>(t))
|
|
185
215
|
}
|
|
186
216
|
|
|
187
217
|
/**
|
|
188
218
|
* Resolve with tags
|
|
189
219
|
*/
|
|
190
220
|
resolveTagged<T>(tag: string): T[] {
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return
|
|
221
|
+
const tokens = this.tagIndex.get(tag)
|
|
222
|
+
if (!tokens || tokens.size === 0) {
|
|
223
|
+
return []
|
|
224
|
+
}
|
|
225
|
+
return Array.from(tokens, token => this.resolve<T>(token))
|
|
196
226
|
}
|
|
197
227
|
|
|
198
228
|
/**
|
|
199
229
|
* Get all bindings with a specific tag
|
|
200
230
|
*/
|
|
201
231
|
getTaggedBindings<T>(tag: string): Binding<T>[] {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
232
|
+
const tokens = this.tagIndex.get(tag)
|
|
233
|
+
if (!tokens || tokens.size === 0) {
|
|
234
|
+
return []
|
|
235
|
+
}
|
|
236
|
+
const bindings: Binding<T>[] = []
|
|
237
|
+
for (const token of tokens) {
|
|
238
|
+
const binding = this.bindings.get(token)
|
|
239
|
+
if (binding) {
|
|
240
|
+
bindings.push(binding as Binding<T>)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return bindings
|
|
205
244
|
}
|
|
206
245
|
|
|
207
246
|
/**
|
|
@@ -241,6 +280,12 @@ export class Container {
|
|
|
241
280
|
* Unbind a token
|
|
242
281
|
*/
|
|
243
282
|
unbind(token: Token): this {
|
|
283
|
+
const binding = this.bindings.get(token)
|
|
284
|
+
if (binding?.metadata?.tags) {
|
|
285
|
+
for (const tag of binding.metadata.tags) {
|
|
286
|
+
this.tagIndex.get(tag)?.delete(token)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
244
289
|
this.bindings.delete(token)
|
|
245
290
|
this.instances.delete(token)
|
|
246
291
|
return this
|
|
@@ -277,6 +322,7 @@ export class Container {
|
|
|
277
322
|
*/
|
|
278
323
|
clear(): this {
|
|
279
324
|
this.bindings.clear()
|
|
325
|
+
this.tagIndex.clear()
|
|
280
326
|
this.instances.clear()
|
|
281
327
|
this.scopedInstances.clear()
|
|
282
328
|
return this
|
|
@@ -600,4 +600,7 @@ export class BindingConditions {
|
|
|
600
600
|
}
|
|
601
601
|
}
|
|
602
602
|
|
|
603
|
-
|
|
603
|
+
// NOTE: this module used to also alias `ContextualContainer` as
|
|
604
|
+
// `DIContainer`, colliding with container.ts's `Container as DIContainer`
|
|
605
|
+
// alias — two different classes under one public name. The base
|
|
606
|
+
// container keeps the alias; use `ContextualContainer` explicitly here.
|
|
@@ -4,13 +4,22 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ResolutionContext } from './container'
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
import {
|
|
8
|
+
Container,
|
|
9
|
+
INJECT_METADATA_KEY,
|
|
10
|
+
INJECTABLE_METADATA_KEY,
|
|
11
|
+
OPTIONAL_METADATA_KEY,
|
|
12
|
+
TAGGED_METADATA_KEY,
|
|
13
|
+
} from './container'
|
|
14
|
+
|
|
15
|
+
// The injection metadata keys MUST be the container's own symbols — this
|
|
16
|
+
// module previously declared its own `Symbol('inject')` etc., so metadata
|
|
17
|
+
// written by `@Inject` was invisible to `Container.getConstructorDependencies`
|
|
18
|
+
// (which reads with the container's symbols) and constructor injection
|
|
19
|
+
// silently resolved nothing. Re-exported for backward compatibility.
|
|
20
|
+
export { INJECT_METADATA_KEY, INJECTABLE_METADATA_KEY, OPTIONAL_METADATA_KEY, TAGGED_METADATA_KEY }
|
|
21
|
+
|
|
22
|
+
// Decorator-specific metadata keys
|
|
14
23
|
export const CONTROLLER_METADATA_KEY: unique symbol = Symbol('controller')
|
|
15
24
|
export const ROUTE_METADATA_KEY: unique symbol = Symbol('route')
|
|
16
25
|
export const MIDDLEWARE_METADATA_KEY: unique symbol = Symbol('middleware')
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-injection container — public entry point.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { Container, createContainer, Inject, Injectable } from '@stacksjs/bun-router/container'
|
|
6
|
+
* ```
|
|
7
|
+
*
|
|
8
|
+
* The core `Container` (and a few companions) are also re-exported from the
|
|
9
|
+
* package root; the decorators live only here because their names (`Get`,
|
|
10
|
+
* `Post`, `Controller`, …) would collide with the router's exports.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export * from './container'
|
|
14
|
+
|
|
15
|
+
// Decorators. The duplicate `InjectableMetadata`/`InjectMetadata` interface
|
|
16
|
+
// declarations stay out of the barrel — the canonical ones come from
|
|
17
|
+
// `./container` above.
|
|
18
|
+
export {
|
|
19
|
+
Body,
|
|
20
|
+
Controller,
|
|
21
|
+
CONTROLLER_METADATA_KEY,
|
|
22
|
+
Cookie,
|
|
23
|
+
decoratorContainer,
|
|
24
|
+
DecoratorContainer,
|
|
25
|
+
Delete,
|
|
26
|
+
Get,
|
|
27
|
+
Head,
|
|
28
|
+
Header,
|
|
29
|
+
Inject,
|
|
30
|
+
Injectable,
|
|
31
|
+
InjectParam,
|
|
32
|
+
MetadataReader,
|
|
33
|
+
MIDDLEWARE_METADATA_KEY,
|
|
34
|
+
Optional,
|
|
35
|
+
Options,
|
|
36
|
+
Param,
|
|
37
|
+
PARAM_METADATA_KEY,
|
|
38
|
+
Patch,
|
|
39
|
+
Post,
|
|
40
|
+
Put,
|
|
41
|
+
Query,
|
|
42
|
+
ROUTE_METADATA_KEY,
|
|
43
|
+
Tagged,
|
|
44
|
+
UseMiddleware,
|
|
45
|
+
} from './decorators'
|
|
46
|
+
export type { ControllerMetadata, ParamMetadata, RouteMetadata } from './decorators'
|
|
47
|
+
|
|
48
|
+
export * from './contextual-binding'
|
|
49
|
+
export * from './service-provider'
|
|
@@ -87,8 +87,9 @@ export class CircuitBreaker {
|
|
|
87
87
|
consecutiveSuccesses: 0,
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
// Clean up old requests periodically
|
|
91
|
-
|
|
90
|
+
// Clean up old requests periodically (unref'd so the timer never
|
|
91
|
+
// keeps the process alive)
|
|
92
|
+
setInterval(() => this.cleanupOldRequests(), this.config.monitoringPeriod / 4).unref?.()
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
/**
|
|
@@ -119,6 +119,8 @@ export class ServiceHealthMonitor {
|
|
|
119
119
|
() => this.performHealthCheck(serviceName, healthConfig),
|
|
120
120
|
healthConfig.interval,
|
|
121
121
|
)
|
|
122
|
+
// Health checks must not keep the process alive on shutdown
|
|
123
|
+
interval.unref?.()
|
|
122
124
|
this.healthCheckIntervals.set(serviceName, interval)
|
|
123
125
|
}
|
|
124
126
|
}
|