@carno.js/core 1.1.1 → 1.2.0

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 (124) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +188 -188
  3. package/dist/Carno.js +45 -26
  4. package/dist/Carno.mjs +45 -26
  5. package/dist/bun/index.js +4 -4
  6. package/dist/bun/index.js.map +30 -29
  7. package/dist/compression/CompressionMiddleware.js +110 -0
  8. package/dist/compression/CompressionMiddleware.mjs +90 -0
  9. package/dist/index.js +3 -1
  10. package/dist/index.mjs +2 -0
  11. package/package.json +2 -2
  12. package/src/Carno.ts +728 -673
  13. package/src/DefaultRoutes.ts +34 -34
  14. package/src/cache/CacheDriver.ts +50 -50
  15. package/src/cache/CacheService.ts +139 -139
  16. package/src/cache/MemoryDriver.ts +104 -104
  17. package/src/cache/RedisDriver.ts +116 -116
  18. package/src/compiler/JITCompiler.ts +167 -167
  19. package/src/compression/CompressionMiddleware.ts +221 -0
  20. package/src/container/Container.ts +168 -168
  21. package/src/context/Context.ts +130 -130
  22. package/src/cors/CorsHandler.ts +145 -145
  23. package/src/decorators/Controller.ts +63 -63
  24. package/src/decorators/Inject.ts +16 -16
  25. package/src/decorators/Middleware.ts +22 -22
  26. package/src/decorators/Service.ts +18 -18
  27. package/src/decorators/methods.ts +58 -58
  28. package/src/decorators/params.ts +47 -47
  29. package/src/events/Lifecycle.ts +97 -97
  30. package/src/exceptions/HttpException.ts +99 -99
  31. package/src/index.ts +99 -95
  32. package/src/metadata.ts +46 -46
  33. package/src/middleware/CarnoMiddleware.ts +20 -14
  34. package/src/router/RadixRouter.ts +225 -225
  35. package/src/testing/TestHarness.ts +185 -185
  36. package/src/utils/Metadata.ts +43 -43
  37. package/src/utils/parseQuery.ts +161 -161
  38. package/src/validation/ValibotAdapter.ts +95 -95
  39. package/src/validation/ValidatorAdapter.ts +69 -69
  40. package/src/validation/ZodAdapter.ts +102 -102
  41. package/dist/Carno.d.js +0 -14
  42. package/dist/Carno.d.mjs +0 -1
  43. package/dist/DefaultRoutes.d.js +0 -13
  44. package/dist/DefaultRoutes.d.mjs +0 -0
  45. package/dist/cache/CacheDriver.d.js +0 -13
  46. package/dist/cache/CacheDriver.d.mjs +0 -0
  47. package/dist/cache/CacheService.d.js +0 -13
  48. package/dist/cache/CacheService.d.mjs +0 -0
  49. package/dist/cache/MemoryDriver.d.js +0 -13
  50. package/dist/cache/MemoryDriver.d.mjs +0 -0
  51. package/dist/cache/RedisDriver.d.js +0 -13
  52. package/dist/cache/RedisDriver.d.mjs +0 -0
  53. package/dist/compiler/JITCompiler.d.js +0 -13
  54. package/dist/compiler/JITCompiler.d.mjs +0 -0
  55. package/dist/container/Container.d.js +0 -13
  56. package/dist/container/Container.d.mjs +0 -0
  57. package/dist/context/Context.d.js +0 -13
  58. package/dist/context/Context.d.mjs +0 -0
  59. package/dist/cors/CorsHandler.d.js +0 -13
  60. package/dist/cors/CorsHandler.d.mjs +0 -0
  61. package/dist/decorators/Controller.d.js +0 -13
  62. package/dist/decorators/Controller.d.mjs +0 -0
  63. package/dist/decorators/Inject.d.js +0 -13
  64. package/dist/decorators/Inject.d.mjs +0 -0
  65. package/dist/decorators/Middleware.d.js +0 -13
  66. package/dist/decorators/Middleware.d.mjs +0 -0
  67. package/dist/decorators/Service.d.js +0 -13
  68. package/dist/decorators/Service.d.mjs +0 -0
  69. package/dist/decorators/methods.d.js +0 -13
  70. package/dist/decorators/methods.d.mjs +0 -0
  71. package/dist/decorators/params.d.js +0 -13
  72. package/dist/decorators/params.d.mjs +0 -0
  73. package/dist/events/Lifecycle.d.js +0 -13
  74. package/dist/events/Lifecycle.d.mjs +0 -0
  75. package/dist/exceptions/HttpException.d.js +0 -13
  76. package/dist/exceptions/HttpException.d.mjs +0 -0
  77. package/dist/index.d.js +0 -130
  78. package/dist/index.d.mjs +0 -78
  79. package/dist/metadata.d.js +0 -13
  80. package/dist/metadata.d.mjs +0 -0
  81. package/dist/middleware/CarnoMiddleware.d.js +0 -13
  82. package/dist/middleware/CarnoMiddleware.d.mjs +0 -0
  83. package/dist/router/RadixRouter.d.js +0 -13
  84. package/dist/router/RadixRouter.d.mjs +0 -0
  85. package/dist/testing/TestHarness.d.js +0 -13
  86. package/dist/testing/TestHarness.d.mjs +0 -0
  87. package/dist/utils/Metadata.d.js +0 -13
  88. package/dist/utils/Metadata.d.mjs +0 -0
  89. package/dist/utils/parseQuery.d.js +0 -13
  90. package/dist/utils/parseQuery.d.mjs +0 -0
  91. package/dist/validation/ValibotAdapter.d.js +0 -13
  92. package/dist/validation/ValibotAdapter.d.mjs +0 -0
  93. package/dist/validation/ValidatorAdapter.d.js +0 -13
  94. package/dist/validation/ValidatorAdapter.d.mjs +0 -0
  95. package/dist/validation/ZodAdapter.d.js +0 -13
  96. package/dist/validation/ZodAdapter.d.mjs +0 -0
  97. package/src/Carno.d.ts +0 -135
  98. package/src/DefaultRoutes.d.ts +0 -19
  99. package/src/cache/CacheDriver.d.ts +0 -43
  100. package/src/cache/CacheService.d.ts +0 -89
  101. package/src/cache/MemoryDriver.d.ts +0 -32
  102. package/src/cache/RedisDriver.d.ts +0 -34
  103. package/src/compiler/JITCompiler.d.ts +0 -36
  104. package/src/container/Container.d.ts +0 -38
  105. package/src/context/Context.d.ts +0 -36
  106. package/src/cors/CorsHandler.d.ts +0 -47
  107. package/src/decorators/Controller.d.ts +0 -13
  108. package/src/decorators/Inject.d.ts +0 -6
  109. package/src/decorators/Middleware.d.ts +0 -5
  110. package/src/decorators/Service.d.ts +0 -9
  111. package/src/decorators/methods.d.ts +0 -7
  112. package/src/decorators/params.d.ts +0 -13
  113. package/src/events/Lifecycle.d.ts +0 -54
  114. package/src/exceptions/HttpException.d.ts +0 -43
  115. package/src/index.d.ts +0 -42
  116. package/src/metadata.d.ts +0 -41
  117. package/src/middleware/CarnoMiddleware.d.ts +0 -12
  118. package/src/router/RadixRouter.d.ts +0 -19
  119. package/src/testing/TestHarness.d.ts +0 -71
  120. package/src/utils/Metadata.d.ts +0 -20
  121. package/src/utils/parseQuery.d.ts +0 -23
  122. package/src/validation/ValibotAdapter.d.ts +0 -30
  123. package/src/validation/ValidatorAdapter.d.ts +0 -54
  124. package/src/validation/ZodAdapter.d.ts +0 -35
@@ -0,0 +1,221 @@
1
+ import type { Context } from '../context/Context';
2
+ import type { CarnoMiddleware, CarnoClosure } from '../middleware/CarnoMiddleware';
3
+ import { brotliCompressSync, constants } from 'zlib';
4
+
5
+ type GzipLevel = -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
6
+ type BrotliQuality = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11;
7
+
8
+ /**
9
+ * Compression configuration.
10
+ */
11
+ export interface CompressionConfig {
12
+ /**
13
+ * Minimum response size in bytes to trigger compression.
14
+ * Responses smaller than this are sent uncompressed.
15
+ * @default 1024
16
+ */
17
+ threshold?: number;
18
+
19
+ /**
20
+ * Preferred encoding order. Client's Accept-Encoding is matched against this list.
21
+ * @default ['br', 'gzip']
22
+ */
23
+ encodings?: ('br' | 'gzip' | 'deflate')[];
24
+
25
+ /**
26
+ * Content-Type patterns that should be compressed.
27
+ * Matched using string includes (case-insensitive).
28
+ * @default ['text/', 'application/json', 'application/javascript', 'application/xml', 'image/svg+xml']
29
+ */
30
+ compressibleTypes?: string[];
31
+
32
+ /**
33
+ * Brotli compression quality (0-11). Higher = better compression, slower.
34
+ * @default 4
35
+ */
36
+ brotliQuality?: BrotliQuality;
37
+
38
+ /**
39
+ * Gzip compression level (-1 to 9). Higher = better compression, slower.
40
+ * @default 6
41
+ */
42
+ gzipLevel?: GzipLevel;
43
+ }
44
+
45
+ const DEFAULT_THRESHOLD = 1024;
46
+ const DEFAULT_ENCODINGS: ('br' | 'gzip' | 'deflate')[] = ['br', 'gzip'];
47
+ const DEFAULT_COMPRESSIBLE_TYPES = [
48
+ 'text/',
49
+ 'application/json',
50
+ 'application/javascript',
51
+ 'application/xml',
52
+ 'application/xhtml+xml',
53
+ 'image/svg+xml',
54
+ ];
55
+ const DEFAULT_BROTLI_QUALITY: BrotliQuality = 4;
56
+ const DEFAULT_GZIP_LEVEL: GzipLevel = 6;
57
+
58
+ type CompressFn = (data: Uint8Array<ArrayBuffer>) => Uint8Array<ArrayBuffer>;
59
+
60
+ /**
61
+ * Compression middleware for Carno.
62
+ *
63
+ * Supports **gzip**, **brotli** and **deflate** using Bun's native APIs
64
+ * (zero external dependencies).
65
+ *
66
+ * Does NOT touch the hot path — runs entirely as a middleware in the
67
+ * onion chain. Routes without middleware remain untouched.
68
+ *
69
+ * All configuration is resolved once at construction time (startup)
70
+ * so the per-request overhead is minimal: one header check +
71
+ * one compression call for eligible responses.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * import { Carno, CompressionMiddleware } from '@carno.js/core';
76
+ *
77
+ * const app = new Carno()
78
+ * .middlewares([new CompressionMiddleware()])
79
+ * .controllers([MyController]);
80
+ *
81
+ * app.listen(3000);
82
+ * ```
83
+ *
84
+ * @example Custom configuration:
85
+ * ```ts
86
+ * new CompressionMiddleware({
87
+ * threshold: 512,
88
+ * encodings: ['gzip'],
89
+ * gzipLevel: 9,
90
+ * })
91
+ * ```
92
+ */
93
+ export class CompressionMiddleware implements CarnoMiddleware {
94
+ private readonly threshold: number;
95
+ private readonly compressibleTypes: string[];
96
+ private readonly compressors: Map<string, CompressFn>;
97
+ private readonly encodingOrder: string[];
98
+
99
+ constructor(config?: CompressionConfig) {
100
+ this.threshold = config?.threshold ?? DEFAULT_THRESHOLD;
101
+ this.compressibleTypes = (config?.compressibleTypes ?? DEFAULT_COMPRESSIBLE_TYPES)
102
+ .map(t => t.toLowerCase());
103
+
104
+ const encodings = config?.encodings ?? DEFAULT_ENCODINGS;
105
+ this.encodingOrder = encodings;
106
+
107
+ const brotliQuality = config?.brotliQuality ?? DEFAULT_BROTLI_QUALITY;
108
+ const gzipLevel = config?.gzipLevel ?? DEFAULT_GZIP_LEVEL;
109
+
110
+ // Pre-build compressor functions at construction time (startup only)
111
+ this.compressors = new Map();
112
+
113
+ for (const enc of encodings) {
114
+ switch (enc) {
115
+ case 'br':
116
+ this.compressors.set('br', (data: Uint8Array<ArrayBuffer>) => {
117
+ const buf = brotliCompressSync(data, {
118
+ params: { [constants.BROTLI_PARAM_QUALITY]: brotliQuality },
119
+ });
120
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
121
+ });
122
+ break;
123
+ case 'gzip':
124
+ this.compressors.set('gzip', (data: Uint8Array<ArrayBuffer>) =>
125
+ Bun.gzipSync(data, { level: gzipLevel }),
126
+ );
127
+ break;
128
+ case 'deflate':
129
+ this.compressors.set('deflate', (data: Uint8Array<ArrayBuffer>) =>
130
+ Bun.deflateSync(data),
131
+ );
132
+ break;
133
+ }
134
+ }
135
+ }
136
+
137
+ async handle(ctx: Context, next: CarnoClosure): Promise<Response | void> {
138
+ const response = await next();
139
+
140
+ // Fast-exit: no Accept-Encoding header
141
+ const acceptEncoding = ctx.req.headers.get('accept-encoding');
142
+ if (!acceptEncoding) {
143
+ return response;
144
+ }
145
+
146
+ // Already encoded — skip
147
+ if (response.headers.get('content-encoding')) {
148
+ return response;
149
+ }
150
+
151
+ // Non-compressible content type — skip
152
+ const contentType = response.headers.get('content-type');
153
+ if (!contentType || !this.isCompressible(contentType)) {
154
+ return response;
155
+ }
156
+
157
+ // Negotiate encoding
158
+ const encoding = this.negotiateEncoding(acceptEncoding);
159
+ if (!encoding) {
160
+ return response;
161
+ }
162
+
163
+ // Read body bytes (consumes the response body)
164
+ const buffer = await response.arrayBuffer();
165
+
166
+ // Below threshold — reconstruct original response
167
+ if (buffer.byteLength < this.threshold) {
168
+ return new Response(buffer, {
169
+ status: response.status,
170
+ statusText: response.statusText,
171
+ headers: response.headers,
172
+ });
173
+ }
174
+
175
+ const compressor = this.compressors.get(encoding)!;
176
+ const bodyBytes = new Uint8Array(buffer);
177
+ const compressed = compressor(bodyBytes);
178
+
179
+ // If compressed is not smaller, return original uncompressed
180
+ if (compressed.byteLength >= buffer.byteLength) {
181
+ return new Response(buffer, {
182
+ status: response.status,
183
+ statusText: response.statusText,
184
+ headers: response.headers,
185
+ });
186
+ }
187
+
188
+ const headers = new Headers(response.headers);
189
+ headers.set('Content-Encoding', encoding);
190
+ headers.set('Content-Length', String(compressed.byteLength));
191
+ headers.set('Vary', this.buildVaryHeader(headers.get('Vary')));
192
+
193
+ return new Response(compressed, {
194
+ status: response.status,
195
+ statusText: response.statusText,
196
+ headers,
197
+ });
198
+ }
199
+
200
+ private isCompressible(contentType: string): boolean {
201
+ const lower = contentType.toLowerCase();
202
+ for (const pattern of this.compressibleTypes) {
203
+ if (lower.includes(pattern)) return true;
204
+ }
205
+ return false;
206
+ }
207
+
208
+ private negotiateEncoding(acceptEncoding: string): string | null {
209
+ const lower = acceptEncoding.toLowerCase();
210
+ for (const encoding of this.encodingOrder) {
211
+ if (lower.includes(encoding)) return encoding;
212
+ }
213
+ return null;
214
+ }
215
+
216
+ private buildVaryHeader(existing: string | null): string {
217
+ if (!existing) return 'Accept-Encoding';
218
+ if (existing.toLowerCase().includes('accept-encoding')) return existing;
219
+ return `${existing}, Accept-Encoding`;
220
+ }
221
+ }
@@ -1,168 +1,168 @@
1
- /**
2
- * Lightweight DI Container for Turbo.
3
- *
4
- * Features:
5
- * - Constructor injection via reflect-metadata
6
- * - Singleton scope by default
7
- * - Request scope support
8
- * - Lazy instantiation
9
- */
10
-
11
- export type Token<T = any> = new (...args: any[]) => T;
12
-
13
- export enum Scope {
14
- SINGLETON = 'singleton', // Always the same instance
15
- REQUEST = 'request', // New instance per request
16
- INSTANCE = 'instance' // New instance per dependency injection
17
- }
18
-
19
- export interface ProviderConfig<T = any> {
20
- token: Token<T>;
21
- useClass?: Token<T>;
22
- useValue?: T;
23
- scope?: Scope;
24
- }
25
-
26
- interface ProviderEntry {
27
- config: ProviderConfig;
28
- instance: any | null;
29
- }
30
-
31
- export class Container {
32
- private configs = new Map<Token, ProviderConfig>();
33
- private instances = new Map<Token, any>();
34
- private resolving = new Set<Token>();
35
-
36
- register<T>(config: ProviderConfig<T> | Token<T>): this {
37
- const normalized = this.normalizeConfig(config);
38
-
39
- this.configs.set(normalized.token, normalized);
40
-
41
- if (normalized.useValue !== undefined) {
42
- this.instances.set(normalized.token, normalized.useValue);
43
- }
44
-
45
- return this;
46
- }
47
-
48
- get<T>(token: Token<T>): T {
49
- const cached = this.instances.get(token);
50
-
51
- if (cached !== undefined) {
52
- return cached;
53
- }
54
-
55
- const res = this.resolveInternal(token);
56
- return res.instance;
57
- }
58
-
59
- has(token: Token): boolean {
60
- return this.configs.has(token);
61
- }
62
-
63
- /**
64
- * Resolves a token to return instance and its effective scope.
65
- */
66
- private resolveInternal<T>(token: Token<T>, requestLocals?: Map<Token, any>): { instance: T, scope: Scope } {
67
- // 1. Check Request Cache
68
- if (requestLocals?.has(token)) {
69
- return { instance: requestLocals.get(token), scope: Scope.REQUEST };
70
- }
71
-
72
- // 2. Check Singleton Cache
73
- const cached = this.instances.get(token);
74
- if (cached !== undefined) {
75
- return { instance: cached, scope: Scope.SINGLETON };
76
- }
77
-
78
- const config = this.configs.get(token);
79
-
80
- if (!config) {
81
- throw new Error(`Provider not found: ${token.name}`);
82
- }
83
-
84
- // 3. Create Instance with Scope Bubbling
85
- const creation = this.createInstance(config, requestLocals);
86
-
87
- // 4. Cache based on Effective Scope
88
- if (creation.scope === Scope.SINGLETON) {
89
- this.instances.set(token, creation.instance);
90
- } else if (creation.scope === Scope.REQUEST && requestLocals) {
91
- requestLocals.set(token, creation.instance);
92
- }
93
- // INSTANCE scope is never cached
94
-
95
- return creation;
96
- }
97
-
98
- private createInstance(config: ProviderConfig, requestLocals?: Map<Token, any>): { instance: any, scope: Scope } {
99
- const target = config.useClass ?? config.token;
100
-
101
- if (this.resolving.has(target)) {
102
- throw new Error(`Circular dependency detected: ${target.name}`);
103
- }
104
-
105
- this.resolving.add(target);
106
-
107
- try {
108
- const depsToken = this.getDependencies(target);
109
-
110
- if (depsToken.length === 0) {
111
- // No deps: Scope is as configured
112
- return { instance: new target(), scope: config.scope || Scope.SINGLETON };
113
- }
114
-
115
- const args: any[] = [];
116
- let effectiveScope = config.scope || Scope.SINGLETON;
117
-
118
- for (const depToken of depsToken) {
119
- const depResult = this.resolveInternal(depToken, requestLocals);
120
- args.push(depResult.instance);
121
-
122
- // Scope Bubbling Logic:
123
- // If a dependency is REQUEST scoped, the parent MUST become REQUEST scoped (if it was Singleton)
124
- // to avoid holding a stale reference to a request-bound instance.
125
- if (depResult.scope === Scope.REQUEST && effectiveScope === Scope.SINGLETON) {
126
- effectiveScope = Scope.REQUEST;
127
- }
128
-
129
- // Note: INSTANCE scope dependencies do not force bubbling because they are transient and safe to hold (usually),
130
- // unless semantic logic dictates otherwise. For now, strictly bubbling REQUEST scope.
131
- }
132
-
133
- return { instance: new target(...args), scope: effectiveScope };
134
- } finally {
135
- this.resolving.delete(target);
136
- }
137
- }
138
-
139
- private getDependencies(target: Token): Token[] {
140
- const types = Reflect.getMetadata('design:paramtypes', target) || [];
141
- return types.filter((t: any) => t && typeof t === 'function' && !this.isPrimitive(t));
142
- }
143
-
144
- private isPrimitive(type: any): boolean {
145
- return type === String || type === Number || type === Boolean || type === Object || type === Array || type === Symbol;
146
- }
147
-
148
- private normalizeConfig<T>(config: ProviderConfig<T> | Token<T>): ProviderConfig<T> {
149
- if (typeof config === 'function') {
150
- return {
151
- token: config,
152
- useClass: config,
153
- scope: Scope.SINGLETON
154
- };
155
- }
156
-
157
- return {
158
- ...config,
159
- useClass: config.useClass ?? config.token,
160
- scope: config.scope ?? Scope.SINGLETON
161
- };
162
- }
163
-
164
- clear(): void {
165
- this.configs.clear();
166
- this.instances.clear();
167
- }
168
- }
1
+ /**
2
+ * Lightweight DI Container for Turbo.
3
+ *
4
+ * Features:
5
+ * - Constructor injection via reflect-metadata
6
+ * - Singleton scope by default
7
+ * - Request scope support
8
+ * - Lazy instantiation
9
+ */
10
+
11
+ export type Token<T = any> = new (...args: any[]) => T;
12
+
13
+ export enum Scope {
14
+ SINGLETON = 'singleton', // Always the same instance
15
+ REQUEST = 'request', // New instance per request
16
+ INSTANCE = 'instance' // New instance per dependency injection
17
+ }
18
+
19
+ export interface ProviderConfig<T = any> {
20
+ token: Token<T>;
21
+ useClass?: Token<T>;
22
+ useValue?: T;
23
+ scope?: Scope;
24
+ }
25
+
26
+ interface ProviderEntry {
27
+ config: ProviderConfig;
28
+ instance: any | null;
29
+ }
30
+
31
+ export class Container {
32
+ private configs = new Map<Token, ProviderConfig>();
33
+ private instances = new Map<Token, any>();
34
+ private resolving = new Set<Token>();
35
+
36
+ register<T>(config: ProviderConfig<T> | Token<T>): this {
37
+ const normalized = this.normalizeConfig(config);
38
+
39
+ this.configs.set(normalized.token, normalized);
40
+
41
+ if (normalized.useValue !== undefined) {
42
+ this.instances.set(normalized.token, normalized.useValue);
43
+ }
44
+
45
+ return this;
46
+ }
47
+
48
+ get<T>(token: Token<T>): T {
49
+ const cached = this.instances.get(token);
50
+
51
+ if (cached !== undefined) {
52
+ return cached;
53
+ }
54
+
55
+ const res = this.resolveInternal(token);
56
+ return res.instance;
57
+ }
58
+
59
+ has(token: Token): boolean {
60
+ return this.configs.has(token);
61
+ }
62
+
63
+ /**
64
+ * Resolves a token to return instance and its effective scope.
65
+ */
66
+ private resolveInternal<T>(token: Token<T>, requestLocals?: Map<Token, any>): { instance: T, scope: Scope } {
67
+ // 1. Check Request Cache
68
+ if (requestLocals?.has(token)) {
69
+ return { instance: requestLocals.get(token), scope: Scope.REQUEST };
70
+ }
71
+
72
+ // 2. Check Singleton Cache
73
+ const cached = this.instances.get(token);
74
+ if (cached !== undefined) {
75
+ return { instance: cached, scope: Scope.SINGLETON };
76
+ }
77
+
78
+ const config = this.configs.get(token);
79
+
80
+ if (!config) {
81
+ throw new Error(`Provider not found: ${token.name}`);
82
+ }
83
+
84
+ // 3. Create Instance with Scope Bubbling
85
+ const creation = this.createInstance(config, requestLocals);
86
+
87
+ // 4. Cache based on Effective Scope
88
+ if (creation.scope === Scope.SINGLETON) {
89
+ this.instances.set(token, creation.instance);
90
+ } else if (creation.scope === Scope.REQUEST && requestLocals) {
91
+ requestLocals.set(token, creation.instance);
92
+ }
93
+ // INSTANCE scope is never cached
94
+
95
+ return creation;
96
+ }
97
+
98
+ private createInstance(config: ProviderConfig, requestLocals?: Map<Token, any>): { instance: any, scope: Scope } {
99
+ const target = config.useClass ?? config.token;
100
+
101
+ if (this.resolving.has(target)) {
102
+ throw new Error(`Circular dependency detected: ${target.name}`);
103
+ }
104
+
105
+ this.resolving.add(target);
106
+
107
+ try {
108
+ const depsToken = this.getDependencies(target);
109
+
110
+ if (depsToken.length === 0) {
111
+ // No deps: Scope is as configured
112
+ return { instance: new target(), scope: config.scope || Scope.SINGLETON };
113
+ }
114
+
115
+ const args: any[] = [];
116
+ let effectiveScope = config.scope || Scope.SINGLETON;
117
+
118
+ for (const depToken of depsToken) {
119
+ const depResult = this.resolveInternal(depToken, requestLocals);
120
+ args.push(depResult.instance);
121
+
122
+ // Scope Bubbling Logic:
123
+ // If a dependency is REQUEST scoped, the parent MUST become REQUEST scoped (if it was Singleton)
124
+ // to avoid holding a stale reference to a request-bound instance.
125
+ if (depResult.scope === Scope.REQUEST && effectiveScope === Scope.SINGLETON) {
126
+ effectiveScope = Scope.REQUEST;
127
+ }
128
+
129
+ // Note: INSTANCE scope dependencies do not force bubbling because they are transient and safe to hold (usually),
130
+ // unless semantic logic dictates otherwise. For now, strictly bubbling REQUEST scope.
131
+ }
132
+
133
+ return { instance: new target(...args), scope: effectiveScope };
134
+ } finally {
135
+ this.resolving.delete(target);
136
+ }
137
+ }
138
+
139
+ private getDependencies(target: Token): Token[] {
140
+ const types = Reflect.getMetadata('design:paramtypes', target) || [];
141
+ return types.filter((t: any) => t && typeof t === 'function' && !this.isPrimitive(t));
142
+ }
143
+
144
+ private isPrimitive(type: any): boolean {
145
+ return type === String || type === Number || type === Boolean || type === Object || type === Array || type === Symbol;
146
+ }
147
+
148
+ private normalizeConfig<T>(config: ProviderConfig<T> | Token<T>): ProviderConfig<T> {
149
+ if (typeof config === 'function') {
150
+ return {
151
+ token: config,
152
+ useClass: config,
153
+ scope: Scope.SINGLETON
154
+ };
155
+ }
156
+
157
+ return {
158
+ ...config,
159
+ useClass: config.useClass ?? config.token,
160
+ scope: config.scope ?? Scope.SINGLETON
161
+ };
162
+ }
163
+
164
+ clear(): void {
165
+ this.configs.clear();
166
+ this.instances.clear();
167
+ }
168
+ }