@carno.js/core 1.1.0 → 1.1.2
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/LICENSE +21 -21
- package/README.md +188 -188
- package/dist/Carno.js +46 -26
- package/dist/Carno.mjs +46 -26
- package/dist/bun/index.js +4 -4
- package/dist/bun/index.js.map +29 -29
- package/package.json +2 -2
- package/src/Carno.ts +718 -673
- package/src/DefaultRoutes.ts +34 -34
- package/src/cache/CacheDriver.ts +50 -50
- package/src/cache/CacheService.ts +139 -139
- package/src/cache/MemoryDriver.ts +104 -104
- package/src/cache/RedisDriver.ts +116 -116
- package/src/compiler/JITCompiler.ts +167 -167
- package/src/container/Container.ts +168 -168
- package/src/context/Context.ts +130 -130
- package/src/cors/CorsHandler.ts +145 -145
- package/src/decorators/Controller.ts +63 -63
- package/src/decorators/Inject.ts +16 -16
- package/src/decorators/Middleware.ts +22 -22
- package/src/decorators/Service.ts +18 -18
- package/src/decorators/methods.ts +58 -58
- package/src/decorators/params.ts +47 -47
- package/src/events/Lifecycle.ts +97 -97
- package/src/exceptions/HttpException.ts +99 -99
- package/src/index.ts +95 -95
- package/src/metadata.ts +46 -46
- package/src/middleware/CarnoMiddleware.ts +14 -14
- package/src/router/RadixRouter.ts +225 -225
- package/src/testing/TestHarness.ts +185 -185
- package/src/utils/Metadata.ts +43 -43
- package/src/utils/parseQuery.ts +161 -161
- package/src/validation/ValibotAdapter.ts +95 -95
- package/src/validation/ValidatorAdapter.ts +69 -69
- package/src/validation/ZodAdapter.ts +102 -102
- package/dist/Carno.d.js +0 -14
- package/dist/Carno.d.mjs +0 -1
- package/dist/DefaultRoutes.d.js +0 -13
- package/dist/DefaultRoutes.d.mjs +0 -0
- package/dist/cache/CacheDriver.d.js +0 -13
- package/dist/cache/CacheDriver.d.mjs +0 -0
- package/dist/cache/CacheService.d.js +0 -13
- package/dist/cache/CacheService.d.mjs +0 -0
- package/dist/cache/MemoryDriver.d.js +0 -13
- package/dist/cache/MemoryDriver.d.mjs +0 -0
- package/dist/cache/RedisDriver.d.js +0 -13
- package/dist/cache/RedisDriver.d.mjs +0 -0
- package/dist/compiler/JITCompiler.d.js +0 -13
- package/dist/compiler/JITCompiler.d.mjs +0 -0
- package/dist/container/Container.d.js +0 -13
- package/dist/container/Container.d.mjs +0 -0
- package/dist/context/Context.d.js +0 -13
- package/dist/context/Context.d.mjs +0 -0
- package/dist/cors/CorsHandler.d.js +0 -13
- package/dist/cors/CorsHandler.d.mjs +0 -0
- package/dist/decorators/Controller.d.js +0 -13
- package/dist/decorators/Controller.d.mjs +0 -0
- package/dist/decorators/Inject.d.js +0 -13
- package/dist/decorators/Inject.d.mjs +0 -0
- package/dist/decorators/Middleware.d.js +0 -13
- package/dist/decorators/Middleware.d.mjs +0 -0
- package/dist/decorators/Service.d.js +0 -13
- package/dist/decorators/Service.d.mjs +0 -0
- package/dist/decorators/methods.d.js +0 -13
- package/dist/decorators/methods.d.mjs +0 -0
- package/dist/decorators/params.d.js +0 -13
- package/dist/decorators/params.d.mjs +0 -0
- package/dist/events/Lifecycle.d.js +0 -13
- package/dist/events/Lifecycle.d.mjs +0 -0
- package/dist/exceptions/HttpException.d.js +0 -13
- package/dist/exceptions/HttpException.d.mjs +0 -0
- package/dist/index.d.js +0 -130
- package/dist/index.d.mjs +0 -78
- package/dist/metadata.d.js +0 -13
- package/dist/metadata.d.mjs +0 -0
- package/dist/middleware/CarnoMiddleware.d.js +0 -13
- package/dist/middleware/CarnoMiddleware.d.mjs +0 -0
- package/dist/router/RadixRouter.d.js +0 -13
- package/dist/router/RadixRouter.d.mjs +0 -0
- package/dist/testing/TestHarness.d.js +0 -13
- package/dist/testing/TestHarness.d.mjs +0 -0
- package/dist/utils/Metadata.d.js +0 -13
- package/dist/utils/Metadata.d.mjs +0 -0
- package/dist/utils/parseQuery.d.js +0 -13
- package/dist/utils/parseQuery.d.mjs +0 -0
- package/dist/validation/ValibotAdapter.d.js +0 -13
- package/dist/validation/ValibotAdapter.d.mjs +0 -0
- package/dist/validation/ValidatorAdapter.d.js +0 -13
- package/dist/validation/ValidatorAdapter.d.mjs +0 -0
- package/dist/validation/ZodAdapter.d.js +0 -13
- package/dist/validation/ZodAdapter.d.mjs +0 -0
- package/src/Carno.d.ts +0 -135
- package/src/DefaultRoutes.d.ts +0 -19
- package/src/cache/CacheDriver.d.ts +0 -43
- package/src/cache/CacheService.d.ts +0 -89
- package/src/cache/MemoryDriver.d.ts +0 -32
- package/src/cache/RedisDriver.d.ts +0 -34
- package/src/compiler/JITCompiler.d.ts +0 -36
- package/src/container/Container.d.ts +0 -38
- package/src/context/Context.d.ts +0 -36
- package/src/cors/CorsHandler.d.ts +0 -47
- package/src/decorators/Controller.d.ts +0 -13
- package/src/decorators/Inject.d.ts +0 -6
- package/src/decorators/Middleware.d.ts +0 -5
- package/src/decorators/Service.d.ts +0 -9
- package/src/decorators/methods.d.ts +0 -7
- package/src/decorators/params.d.ts +0 -13
- package/src/events/Lifecycle.d.ts +0 -54
- package/src/exceptions/HttpException.d.ts +0 -43
- package/src/index.d.ts +0 -42
- package/src/metadata.d.ts +0 -41
- package/src/middleware/CarnoMiddleware.d.ts +0 -12
- package/src/router/RadixRouter.d.ts +0 -19
- package/src/testing/TestHarness.d.ts +0 -71
- package/src/utils/Metadata.d.ts +0 -20
- package/src/utils/parseQuery.d.ts +0 -23
- package/src/validation/ValibotAdapter.d.ts +0 -30
- package/src/validation/ValidatorAdapter.d.ts +0 -54
- package/src/validation/ZodAdapter.d.ts +0 -35
|
@@ -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
|
+
}
|
package/src/context/Context.ts
CHANGED
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Request Context for Turbo.
|
|
3
|
-
*
|
|
4
|
-
* Lazy initialization for maximum performance:
|
|
5
|
-
* - Query parsed only when accessed
|
|
6
|
-
* - Body parsed only when needed
|
|
7
|
-
* - Minimal allocations in hot path
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { parseQueryFromURL } from '../utils/parseQuery';
|
|
11
|
-
|
|
12
|
-
const EMPTY_PARAMS: Record<string, string> = Object.freeze({}) as Record<string, string>;
|
|
13
|
-
|
|
14
|
-
export class Context {
|
|
15
|
-
readonly req: Request;
|
|
16
|
-
params: Record<string, string>;
|
|
17
|
-
locals: Record<string, any> = {};
|
|
18
|
-
|
|
19
|
-
// Lazy fields - only allocated when accessed
|
|
20
|
-
private _query: Record<string, string> | null = null;
|
|
21
|
-
private _body: any;
|
|
22
|
-
private _bodyParsed = false;
|
|
23
|
-
private _url: URL | null = null;
|
|
24
|
-
private _status = 0;
|
|
25
|
-
|
|
26
|
-
constructor(req: Request, params: Record<string, string> = EMPTY_PARAMS) {
|
|
27
|
-
this.req = req;
|
|
28
|
-
this.params = params;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
get status(): number {
|
|
32
|
-
return this._status || 200;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
set status(value: number) {
|
|
36
|
-
this._status = value;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
get url(): URL {
|
|
40
|
-
if (!this._url) {
|
|
41
|
-
this._url = new URL(this.req.url);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return this._url;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
get query(): Record<string, string> {
|
|
48
|
-
if (!this._query) {
|
|
49
|
-
this._query = parseQueryFromURL(this.req.url);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return this._query;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
get body(): any {
|
|
56
|
-
return this._body;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async parseBody(): Promise<any> {
|
|
60
|
-
if (this._bodyParsed) {
|
|
61
|
-
return this._body;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
this._bodyParsed = true;
|
|
65
|
-
const contentType = this.req.headers.get('content-type') || '';
|
|
66
|
-
|
|
67
|
-
if (contentType.includes('application/json')) {
|
|
68
|
-
this._body = await this.req.json();
|
|
69
|
-
} else if (contentType.includes('form')) {
|
|
70
|
-
const formData = await this.req.formData();
|
|
71
|
-
this._body = Object.fromEntries(formData);
|
|
72
|
-
} else if (contentType.includes('text')) {
|
|
73
|
-
this._body = await this.req.text();
|
|
74
|
-
} else {
|
|
75
|
-
this._body = await this.req.arrayBuffer();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return this._body;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
get method(): string {
|
|
82
|
-
return this.req.method;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
get headers(): Headers {
|
|
86
|
-
return this.req.headers;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
get path(): string {
|
|
90
|
-
return this.url.pathname;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
json(data: any, status?: number): Response {
|
|
94
|
-
if (status) this.status = status;
|
|
95
|
-
|
|
96
|
-
return Response.json(data, { status: this.status });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
text(data: string, status?: number): Response {
|
|
100
|
-
if (status) this.status = status;
|
|
101
|
-
|
|
102
|
-
return new Response(data, {
|
|
103
|
-
status: this.status,
|
|
104
|
-
headers: { 'Content-Type': 'text/plain' }
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
html(data: string, status?: number): Response {
|
|
109
|
-
if (status) this.status = status;
|
|
110
|
-
|
|
111
|
-
return new Response(data, {
|
|
112
|
-
status: this.status,
|
|
113
|
-
headers: { 'Content-Type': 'text/html' }
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
redirect(url: string, status: number = 302): Response {
|
|
118
|
-
return Response.redirect(url, status);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Creates a Context from a job (for queue processing).
|
|
123
|
-
*/
|
|
124
|
-
static createFromJob(job: any): Context {
|
|
125
|
-
const fakeRequest = new Request('http://localhost/job');
|
|
126
|
-
const ctx = new Context(fakeRequest);
|
|
127
|
-
ctx.locals.job = job;
|
|
128
|
-
return ctx;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Request Context for Turbo.
|
|
3
|
+
*
|
|
4
|
+
* Lazy initialization for maximum performance:
|
|
5
|
+
* - Query parsed only when accessed
|
|
6
|
+
* - Body parsed only when needed
|
|
7
|
+
* - Minimal allocations in hot path
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { parseQueryFromURL } from '../utils/parseQuery';
|
|
11
|
+
|
|
12
|
+
const EMPTY_PARAMS: Record<string, string> = Object.freeze({}) as Record<string, string>;
|
|
13
|
+
|
|
14
|
+
export class Context {
|
|
15
|
+
readonly req: Request;
|
|
16
|
+
params: Record<string, string>;
|
|
17
|
+
locals: Record<string, any> = {};
|
|
18
|
+
|
|
19
|
+
// Lazy fields - only allocated when accessed
|
|
20
|
+
private _query: Record<string, string> | null = null;
|
|
21
|
+
private _body: any;
|
|
22
|
+
private _bodyParsed = false;
|
|
23
|
+
private _url: URL | null = null;
|
|
24
|
+
private _status = 0;
|
|
25
|
+
|
|
26
|
+
constructor(req: Request, params: Record<string, string> = EMPTY_PARAMS) {
|
|
27
|
+
this.req = req;
|
|
28
|
+
this.params = params;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get status(): number {
|
|
32
|
+
return this._status || 200;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
set status(value: number) {
|
|
36
|
+
this._status = value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get url(): URL {
|
|
40
|
+
if (!this._url) {
|
|
41
|
+
this._url = new URL(this.req.url);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return this._url;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get query(): Record<string, string> {
|
|
48
|
+
if (!this._query) {
|
|
49
|
+
this._query = parseQueryFromURL(this.req.url);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return this._query;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get body(): any {
|
|
56
|
+
return this._body;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async parseBody(): Promise<any> {
|
|
60
|
+
if (this._bodyParsed) {
|
|
61
|
+
return this._body;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this._bodyParsed = true;
|
|
65
|
+
const contentType = this.req.headers.get('content-type') || '';
|
|
66
|
+
|
|
67
|
+
if (contentType.includes('application/json')) {
|
|
68
|
+
this._body = await this.req.json();
|
|
69
|
+
} else if (contentType.includes('form')) {
|
|
70
|
+
const formData = await this.req.formData();
|
|
71
|
+
this._body = Object.fromEntries(formData);
|
|
72
|
+
} else if (contentType.includes('text')) {
|
|
73
|
+
this._body = await this.req.text();
|
|
74
|
+
} else {
|
|
75
|
+
this._body = await this.req.arrayBuffer();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return this._body;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get method(): string {
|
|
82
|
+
return this.req.method;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get headers(): Headers {
|
|
86
|
+
return this.req.headers;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get path(): string {
|
|
90
|
+
return this.url.pathname;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
json(data: any, status?: number): Response {
|
|
94
|
+
if (status) this.status = status;
|
|
95
|
+
|
|
96
|
+
return Response.json(data, { status: this.status });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
text(data: string, status?: number): Response {
|
|
100
|
+
if (status) this.status = status;
|
|
101
|
+
|
|
102
|
+
return new Response(data, {
|
|
103
|
+
status: this.status,
|
|
104
|
+
headers: { 'Content-Type': 'text/plain' }
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
html(data: string, status?: number): Response {
|
|
109
|
+
if (status) this.status = status;
|
|
110
|
+
|
|
111
|
+
return new Response(data, {
|
|
112
|
+
status: this.status,
|
|
113
|
+
headers: { 'Content-Type': 'text/html' }
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
redirect(url: string, status: number = 302): Response {
|
|
118
|
+
return Response.redirect(url, status);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Creates a Context from a job (for queue processing).
|
|
123
|
+
*/
|
|
124
|
+
static createFromJob(job: any): Context {
|
|
125
|
+
const fakeRequest = new Request('http://localhost/job');
|
|
126
|
+
const ctx = new Context(fakeRequest);
|
|
127
|
+
ctx.locals.job = job;
|
|
128
|
+
return ctx;
|
|
129
|
+
}
|
|
130
|
+
}
|