@engjts/nexus 0.1.6 → 0.1.8
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/BENCHMARK_REPORT.md +343 -0
- package/dist/advanced/playground/generatePlaygroundHTML.d.ts.map +1 -1
- package/dist/advanced/playground/generatePlaygroundHTML.js +107 -0
- package/dist/advanced/playground/generatePlaygroundHTML.js.map +1 -1
- package/dist/advanced/playground/playground.d.ts +19 -0
- package/dist/advanced/playground/playground.d.ts.map +1 -1
- package/dist/advanced/playground/playground.js +70 -0
- package/dist/advanced/playground/playground.js.map +1 -1
- package/dist/advanced/playground/types.d.ts +20 -0
- package/dist/advanced/playground/types.d.ts.map +1 -1
- package/dist/cli/templates/generators.d.ts.map +1 -1
- package/dist/cli/templates/generators.js +16 -13
- package/dist/cli/templates/generators.js.map +1 -1
- package/dist/cli/templates/index.js +25 -25
- package/dist/core/application.d.ts +14 -0
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/application.js +173 -71
- package/dist/core/application.js.map +1 -1
- package/dist/core/context-pool.d.ts +2 -13
- package/dist/core/context-pool.d.ts.map +1 -1
- package/dist/core/context-pool.js +7 -45
- package/dist/core/context-pool.js.map +1 -1
- package/dist/core/context.d.ts +108 -5
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +449 -53
- package/dist/core/context.js.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +9 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/middleware.d.ts +6 -0
- package/dist/core/middleware.d.ts.map +1 -1
- package/dist/core/middleware.js +83 -84
- package/dist/core/middleware.js.map +1 -1
- package/dist/core/performance/fast-json.d.ts +149 -0
- package/dist/core/performance/fast-json.d.ts.map +1 -0
- package/dist/core/performance/fast-json.js +473 -0
- package/dist/core/performance/fast-json.js.map +1 -0
- package/dist/core/router/file-router.d.ts +20 -7
- package/dist/core/router/file-router.d.ts.map +1 -1
- package/dist/core/router/file-router.js +41 -13
- package/dist/core/router/file-router.js.map +1 -1
- package/dist/core/router/index.d.ts +6 -0
- package/dist/core/router/index.d.ts.map +1 -1
- package/dist/core/router/index.js +33 -6
- package/dist/core/router/index.js.map +1 -1
- package/dist/core/router/radix-tree.d.ts +4 -1
- package/dist/core/router/radix-tree.d.ts.map +1 -1
- package/dist/core/router/radix-tree.js +7 -3
- package/dist/core/router/radix-tree.js.map +1 -1
- package/dist/core/serializer.d.ts +251 -0
- package/dist/core/serializer.d.ts.map +1 -0
- package/dist/core/serializer.js +290 -0
- package/dist/core/serializer.js.map +1 -0
- package/dist/core/types.d.ts +39 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/src/advanced/playground/generatePlaygroundHTML.ts +107 -0
- package/src/advanced/playground/playground.ts +225 -145
- package/src/advanced/playground/types.ts +29 -0
- package/src/cli/templates/generators.ts +16 -13
- package/src/cli/templates/index.ts +25 -25
- package/src/core/application.ts +202 -84
- package/src/core/context-pool.ts +8 -56
- package/src/core/context.ts +497 -53
- package/src/core/index.ts +14 -0
- package/src/core/middleware.ts +99 -89
- package/src/core/router/file-router.ts +41 -12
- package/src/core/router/index.ts +213 -180
- package/src/core/router/radix-tree.ts +20 -4
- package/src/core/serializer.ts +397 -0
- package/src/core/types.ts +43 -1
- package/src/index.ts +17 -0
package/src/core/context.ts
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
7
|
-
import { parse as
|
|
8
|
-
import { parse as parseQueryString } from 'querystring';
|
|
7
|
+
import { parse as fastQueryParse } from 'fast-querystring';
|
|
9
8
|
import {
|
|
10
9
|
Context,
|
|
11
10
|
Headers,
|
|
@@ -16,6 +15,7 @@ import {
|
|
|
16
15
|
HTTPMethod
|
|
17
16
|
} from './types';
|
|
18
17
|
import { ContextStore, StoreConstructor, StoreRegistry, RequestStore, RequestStoreConstructor, RequestStoreRegistry } from './store';
|
|
18
|
+
import { SerializerFunction } from './serializer';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Cookie manager implementation
|
|
@@ -74,12 +74,25 @@ class CookieManager implements Cookies {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Pre-cached headers for common content types
|
|
79
|
+
* This avoids object creation on every response
|
|
80
|
+
*/
|
|
81
|
+
const CACHED_HEADERS = {
|
|
82
|
+
JSON: { 'Content-Type': 'application/json' } as Headers,
|
|
83
|
+
HTML: { 'Content-Type': 'text/html; charset=utf-8' } as Headers,
|
|
84
|
+
TEXT: { 'Content-Type': 'text/plain; charset=utf-8' } as Headers,
|
|
85
|
+
};
|
|
86
|
+
|
|
77
87
|
/**
|
|
78
88
|
* Response builder implementation
|
|
89
|
+
* Supports fast-json-stringify for optimized JSON serialization
|
|
79
90
|
*/
|
|
80
91
|
class ResponseBuilderImpl implements ResponseBuilder {
|
|
81
92
|
private _status: number = 200;
|
|
82
93
|
private _headers: Headers = {};
|
|
94
|
+
private _hasCustomHeaders: boolean = false;
|
|
95
|
+
private _serializers: Map<number | string, SerializerFunction> | null = null;
|
|
83
96
|
|
|
84
97
|
status(code: number): ResponseBuilder {
|
|
85
98
|
this._status = code;
|
|
@@ -88,27 +101,57 @@ class ResponseBuilderImpl implements ResponseBuilder {
|
|
|
88
101
|
|
|
89
102
|
header(name: string, value: string): ResponseBuilder {
|
|
90
103
|
this._headers[name] = value;
|
|
104
|
+
this._hasCustomHeaders = true;
|
|
91
105
|
return this;
|
|
92
106
|
}
|
|
93
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Set serializers for this response builder (called by router)
|
|
110
|
+
* @internal
|
|
111
|
+
*/
|
|
112
|
+
setSerializers(serializers: Map<number | string, SerializerFunction>): void {
|
|
113
|
+
this._serializers = serializers;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get the appropriate serializer for current status code
|
|
118
|
+
*/
|
|
119
|
+
private getSerializer(): SerializerFunction | null {
|
|
120
|
+
if (!this._serializers) return null;
|
|
121
|
+
|
|
122
|
+
// Try exact match first
|
|
123
|
+
const exactMatch = this._serializers.get(this._status);
|
|
124
|
+
if (exactMatch) return exactMatch;
|
|
125
|
+
|
|
126
|
+
// Try status code ranges (2xx, 4xx, 5xx)
|
|
127
|
+
const range = `${Math.floor(this._status / 100)}xx`;
|
|
128
|
+
const rangeMatch = this._serializers.get(range);
|
|
129
|
+
if (rangeMatch) return rangeMatch;
|
|
130
|
+
|
|
131
|
+
// Try default
|
|
132
|
+
return this._serializers.get('default') || null;
|
|
133
|
+
}
|
|
134
|
+
|
|
94
135
|
json<T>(data: T): Response {
|
|
136
|
+
// Try to use fast serializer first
|
|
137
|
+
const serializer = this.getSerializer();
|
|
138
|
+
const body = serializer ? serializer(data) : JSON.stringify(data);
|
|
139
|
+
|
|
95
140
|
return {
|
|
96
141
|
statusCode: this._status,
|
|
97
|
-
headers:
|
|
98
|
-
...this._headers,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
body: JSON.stringify(data)
|
|
142
|
+
headers: this._hasCustomHeaders
|
|
143
|
+
? { ...this._headers, 'Content-Type': 'application/json' }
|
|
144
|
+
: CACHED_HEADERS.JSON,
|
|
145
|
+
body
|
|
102
146
|
};
|
|
103
147
|
}
|
|
104
148
|
|
|
105
149
|
html(content: string): Response {
|
|
106
150
|
return {
|
|
107
151
|
statusCode: this._status,
|
|
108
|
-
headers:
|
|
109
|
-
...this._headers,
|
|
110
|
-
|
|
111
|
-
},
|
|
152
|
+
headers: this._hasCustomHeaders
|
|
153
|
+
? { ...this._headers, 'Content-Type': 'text/html; charset=utf-8' }
|
|
154
|
+
: CACHED_HEADERS.HTML,
|
|
112
155
|
body: content
|
|
113
156
|
};
|
|
114
157
|
}
|
|
@@ -116,10 +159,9 @@ class ResponseBuilderImpl implements ResponseBuilder {
|
|
|
116
159
|
text(content: string): Response {
|
|
117
160
|
return {
|
|
118
161
|
statusCode: this._status,
|
|
119
|
-
headers:
|
|
120
|
-
...this._headers,
|
|
121
|
-
|
|
122
|
-
},
|
|
162
|
+
headers: this._hasCustomHeaders
|
|
163
|
+
? { ...this._headers, 'Content-Type': 'text/plain; charset=utf-8' }
|
|
164
|
+
: CACHED_HEADERS.TEXT,
|
|
123
165
|
body: content
|
|
124
166
|
};
|
|
125
167
|
}
|
|
@@ -127,10 +169,9 @@ class ResponseBuilderImpl implements ResponseBuilder {
|
|
|
127
169
|
redirect(url: string, status: number = 302): Response {
|
|
128
170
|
return {
|
|
129
171
|
statusCode: status,
|
|
130
|
-
headers:
|
|
131
|
-
...this._headers,
|
|
132
|
-
'Location': url
|
|
133
|
-
},
|
|
172
|
+
headers: this._hasCustomHeaders
|
|
173
|
+
? { ...this._headers, 'Location': url }
|
|
174
|
+
: { 'Location': url },
|
|
134
175
|
body: ''
|
|
135
176
|
};
|
|
136
177
|
}
|
|
@@ -143,6 +184,37 @@ class ResponseBuilderImpl implements ResponseBuilder {
|
|
|
143
184
|
stream: readable
|
|
144
185
|
};
|
|
145
186
|
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Reset for reuse (object pooling)
|
|
190
|
+
*/
|
|
191
|
+
reset(): void {
|
|
192
|
+
this._status = 200;
|
|
193
|
+
this._headers = {};
|
|
194
|
+
this._hasCustomHeaders = false;
|
|
195
|
+
this._serializers = null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Reusable response builder pool
|
|
201
|
+
*/
|
|
202
|
+
const responseBuilderPool: ResponseBuilderImpl[] = [];
|
|
203
|
+
const RESPONSE_BUILDER_POOL_SIZE = 100;
|
|
204
|
+
|
|
205
|
+
function acquireResponseBuilder(): ResponseBuilderImpl {
|
|
206
|
+
if (responseBuilderPool.length > 0) {
|
|
207
|
+
const builder = responseBuilderPool.pop()!;
|
|
208
|
+
builder.reset();
|
|
209
|
+
return builder;
|
|
210
|
+
}
|
|
211
|
+
return new ResponseBuilderImpl();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function releaseResponseBuilder(builder: ResponseBuilderImpl): void {
|
|
215
|
+
if (responseBuilderPool.length < RESPONSE_BUILDER_POOL_SIZE) {
|
|
216
|
+
responseBuilderPool.push(builder);
|
|
217
|
+
}
|
|
146
218
|
}
|
|
147
219
|
|
|
148
220
|
/**
|
|
@@ -151,23 +223,30 @@ class ResponseBuilderImpl implements ResponseBuilder {
|
|
|
151
223
|
export class ContextImpl implements Context {
|
|
152
224
|
method: HTTPMethod;
|
|
153
225
|
path: string;
|
|
154
|
-
|
|
226
|
+
private _url: URL | null = null; // Lazy URL creation
|
|
227
|
+
private _host: string = 'localhost';
|
|
155
228
|
params: Record<string, string> = {};
|
|
156
|
-
|
|
157
|
-
|
|
229
|
+
private _query: Record<string, any> | null = null; // Lazy query parsing
|
|
230
|
+
private _queryString: string = ''; // Raw query string for lazy parsing
|
|
158
231
|
headers: Headers;
|
|
159
|
-
|
|
232
|
+
private _cookieHeader: string | undefined;
|
|
233
|
+
private _cookies: CookieManager | null = null; // Lazy cookie parsing
|
|
160
234
|
raw: { req: IncomingMessage; res: ServerResponse };
|
|
161
235
|
response: ResponseBuilder;
|
|
162
236
|
|
|
237
|
+
// Lazy body parsing - key optimization!
|
|
238
|
+
private _parsedBody: any = undefined;
|
|
239
|
+
private _bodyPromise: Promise<any> | null = null;
|
|
240
|
+
private _bodyParsed: boolean = false;
|
|
241
|
+
|
|
163
242
|
// Store registry reference (set by Application)
|
|
164
243
|
private _storeRegistry?: StoreRegistry;
|
|
165
244
|
|
|
166
|
-
// Request-scoped store registry
|
|
167
|
-
private _requestStoreRegistry: RequestStoreRegistry;
|
|
245
|
+
// Request-scoped store registry - now lazy!
|
|
246
|
+
private _requestStoreRegistry: RequestStoreRegistry | null = null;
|
|
168
247
|
|
|
169
|
-
// Request-scoped simple key-value storage
|
|
170
|
-
private _data: Map<string, any> =
|
|
248
|
+
// Request-scoped simple key-value storage - now lazy!
|
|
249
|
+
private _data: Map<string, any> | null = null;
|
|
171
250
|
|
|
172
251
|
// Debug mode
|
|
173
252
|
private _debug: boolean = false;
|
|
@@ -175,26 +254,344 @@ export class ContextImpl implements Context {
|
|
|
175
254
|
constructor(req: IncomingMessage, res: ServerResponse) {
|
|
176
255
|
this.raw = { req, res };
|
|
177
256
|
|
|
178
|
-
// Parse method
|
|
179
|
-
this.method = (req.method
|
|
257
|
+
// Parse method - use direct access, avoid optional chaining overhead
|
|
258
|
+
this.method = (req.method ? req.method.toUpperCase() : 'GET') as HTTPMethod;
|
|
180
259
|
|
|
181
|
-
//
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
260
|
+
// Fast URL parsing - just extract path, delay query parsing
|
|
261
|
+
const url = req.url || '/';
|
|
262
|
+
const queryIndex = url.indexOf('?');
|
|
263
|
+
|
|
264
|
+
if (queryIndex === -1) {
|
|
265
|
+
this.path = url;
|
|
266
|
+
this._queryString = '';
|
|
267
|
+
this._query = null; // Will be {} when accessed
|
|
268
|
+
} else {
|
|
269
|
+
this.path = url.substring(0, queryIndex);
|
|
270
|
+
// Store query string for lazy parsing
|
|
271
|
+
this._queryString = url.substring(queryIndex + 1);
|
|
272
|
+
this._query = null; // Parse lazily
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Store host for lazy URL creation
|
|
276
|
+
this._host = (req.headers.host as string) || 'localhost';
|
|
277
|
+
|
|
278
|
+
// URL is now lazy - don't create here!
|
|
279
|
+
this._url = null;
|
|
186
280
|
|
|
187
|
-
// Parse headers
|
|
281
|
+
// Parse headers (direct reference, no copy)
|
|
188
282
|
this.headers = req.headers as Headers;
|
|
189
283
|
|
|
190
|
-
//
|
|
191
|
-
this.
|
|
284
|
+
// Store cookie header for lazy parsing
|
|
285
|
+
this._cookieHeader = req.headers.cookie;
|
|
286
|
+
this._cookies = null;
|
|
287
|
+
|
|
288
|
+
// Get response builder from pool
|
|
289
|
+
this.response = acquireResponseBuilder();
|
|
290
|
+
}
|
|
192
291
|
|
|
193
|
-
|
|
194
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Lazy URL getter - only create URL object when accessed
|
|
294
|
+
* Most handlers don't need the full URL object
|
|
295
|
+
*/
|
|
296
|
+
get url(): URL {
|
|
297
|
+
if (!this._url) {
|
|
298
|
+
this._url = new URL(this.path, `http://${this._host}`);
|
|
299
|
+
}
|
|
300
|
+
return this._url;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
set url(value: URL) {
|
|
304
|
+
this._url = value;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Lazy query getter - only parse query string when accessed
|
|
309
|
+
* Most simple endpoints like /json don't need query parsing
|
|
310
|
+
* Inline fast-querystring for minimal overhead
|
|
311
|
+
*/
|
|
312
|
+
get query(): Record<string, any> {
|
|
313
|
+
if (this._query === null) {
|
|
314
|
+
// Inline fast-querystring call directly - no method call overhead
|
|
315
|
+
this._query = this._queryString
|
|
316
|
+
? fastQueryParse(this._queryString) as Record<string, any>
|
|
317
|
+
: {};
|
|
318
|
+
}
|
|
319
|
+
return this._query;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
set query(value: Record<string, any>) {
|
|
323
|
+
this._query = value;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Lazy cookies getter - only parse cookies when accessed
|
|
328
|
+
*/
|
|
329
|
+
get cookies(): Cookies {
|
|
330
|
+
if (!this._cookies) {
|
|
331
|
+
this._cookies = new CookieManager(this._cookieHeader);
|
|
332
|
+
}
|
|
333
|
+
return this._cookies;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
set cookies(value: Cookies) {
|
|
337
|
+
this._cookies = value as CookieManager;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Reinitialize context for pooling (avoids new object creation)
|
|
342
|
+
*/
|
|
343
|
+
reinitialize(req: IncomingMessage, res: ServerResponse): void {
|
|
344
|
+
this.raw = { req, res };
|
|
345
|
+
this.method = (req.method ? req.method.toUpperCase() : 'GET') as HTTPMethod;
|
|
346
|
+
|
|
347
|
+
// Fast URL parsing - delay query parsing
|
|
348
|
+
const url = req.url || '/';
|
|
349
|
+
const queryIndex = url.indexOf('?');
|
|
350
|
+
|
|
351
|
+
if (queryIndex === -1) {
|
|
352
|
+
this.path = url;
|
|
353
|
+
this._queryString = '';
|
|
354
|
+
this._query = null;
|
|
355
|
+
} else {
|
|
356
|
+
this.path = url.substring(0, queryIndex);
|
|
357
|
+
this._queryString = url.substring(queryIndex + 1);
|
|
358
|
+
this._query = null; // Parse lazily
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Lazy URL - don't create here
|
|
362
|
+
this._host = (req.headers.host as string) || 'localhost';
|
|
363
|
+
this._url = null;
|
|
364
|
+
|
|
365
|
+
this.headers = req.headers as Headers;
|
|
366
|
+
|
|
367
|
+
// Lazy cookies
|
|
368
|
+
this._cookieHeader = req.headers.cookie;
|
|
369
|
+
this._cookies = null;
|
|
370
|
+
|
|
371
|
+
// Reuse or get new response builder from pool
|
|
372
|
+
if (this.response && typeof (this.response as ResponseBuilderImpl).reset === 'function') {
|
|
373
|
+
(this.response as ResponseBuilderImpl).reset();
|
|
374
|
+
} else {
|
|
375
|
+
this.response = acquireResponseBuilder();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Reset body state
|
|
379
|
+
this._parsedBody = undefined;
|
|
380
|
+
this._bodyPromise = null;
|
|
381
|
+
this._bodyParsed = false;
|
|
382
|
+
|
|
383
|
+
// Reset params
|
|
384
|
+
this.params = {};
|
|
385
|
+
|
|
386
|
+
// Lazy data and store - just null them, create on access
|
|
387
|
+
if (this._data) {
|
|
388
|
+
this._data.clear();
|
|
389
|
+
}
|
|
390
|
+
this._requestStoreRegistry = null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Lazy body getter - parses body on first access
|
|
395
|
+
* This is the KEY optimization that fixes POST performance!
|
|
396
|
+
*/
|
|
397
|
+
get body(): any {
|
|
398
|
+
// If already parsed synchronously, return it
|
|
399
|
+
if (this._bodyParsed) {
|
|
400
|
+
return this._parsedBody;
|
|
401
|
+
}
|
|
195
402
|
|
|
196
|
-
//
|
|
197
|
-
|
|
403
|
+
// Return undefined if not parsed yet
|
|
404
|
+
// Use getBody() for async access
|
|
405
|
+
return this._parsedBody;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Set body directly (for backwards compatibility)
|
|
410
|
+
*/
|
|
411
|
+
set body(value: any) {
|
|
412
|
+
this._parsedBody = value;
|
|
413
|
+
this._bodyParsed = true;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Check if body is ready for sync access (no await needed)
|
|
418
|
+
*/
|
|
419
|
+
get isBodyReady(): boolean {
|
|
420
|
+
return this._bodyParsed;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Wait for body to be parsed
|
|
425
|
+
* Use this if you need to ensure body is available for sync access
|
|
426
|
+
* @example
|
|
427
|
+
* ```typescript
|
|
428
|
+
* app.post('/data', async (ctx) => {
|
|
429
|
+
* await ctx.waitForBody();
|
|
430
|
+
* console.log(ctx.body); // Now safe to access synchronously
|
|
431
|
+
* });
|
|
432
|
+
* ```
|
|
433
|
+
*/
|
|
434
|
+
async waitForBody(): Promise<any> {
|
|
435
|
+
return this.getBody();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Async body getter - use this in handlers for POST/PUT/PATCH
|
|
440
|
+
* @example
|
|
441
|
+
* ```typescript
|
|
442
|
+
* app.post('/data', async (ctx) => {
|
|
443
|
+
* const body = await ctx.getBody();
|
|
444
|
+
* return { received: body };
|
|
445
|
+
* });
|
|
446
|
+
* ```
|
|
447
|
+
*/
|
|
448
|
+
async getBody<T = any>(): Promise<T> {
|
|
449
|
+
// Already parsed
|
|
450
|
+
if (this._bodyParsed) {
|
|
451
|
+
return this._parsedBody as T;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Already parsing (dedup concurrent calls)
|
|
455
|
+
if (this._bodyPromise) {
|
|
456
|
+
return this._bodyPromise as Promise<T>;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Start parsing with optimized parser
|
|
460
|
+
this._bodyPromise = this.parseBodyOptimized();
|
|
461
|
+
this._parsedBody = await this._bodyPromise;
|
|
462
|
+
this._bodyParsed = true;
|
|
463
|
+
this._bodyPromise = null;
|
|
464
|
+
|
|
465
|
+
return this._parsedBody as T;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Ultra-optimized body parser inspired by Fastify's approach
|
|
470
|
+
* Key optimizations:
|
|
471
|
+
* 1. Pre-check content-type before reading data
|
|
472
|
+
* 2. Use direct string concatenation with setEncoding
|
|
473
|
+
* 3. Minimal closure allocation
|
|
474
|
+
* 4. Fast-path for JSON (most common case)
|
|
475
|
+
*/
|
|
476
|
+
private parseBodyOptimized(): Promise<any> {
|
|
477
|
+
const req = this.raw.req;
|
|
478
|
+
const contentType = req.headers['content-type'];
|
|
479
|
+
|
|
480
|
+
// Fast path: determine parser type once, before data collection
|
|
481
|
+
const isJSON = contentType ? contentType.charCodeAt(0) === 97 && contentType.startsWith('application/json') : false;
|
|
482
|
+
const isForm = !isJSON && contentType ? contentType.includes('x-www-form-urlencoded') : false;
|
|
483
|
+
|
|
484
|
+
return new Promise((resolve, reject) => {
|
|
485
|
+
// Set encoding for string mode - avoids Buffer.toString() overhead
|
|
486
|
+
req.setEncoding('utf8');
|
|
487
|
+
|
|
488
|
+
let body = '';
|
|
489
|
+
|
|
490
|
+
const onData = (chunk: string) => {
|
|
491
|
+
body += chunk;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const onEnd = () => {
|
|
495
|
+
// Cleanup listeners immediately
|
|
496
|
+
req.removeListener('data', onData);
|
|
497
|
+
req.removeListener('end', onEnd);
|
|
498
|
+
req.removeListener('error', onError);
|
|
499
|
+
|
|
500
|
+
if (!body) {
|
|
501
|
+
resolve({});
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
if (isJSON) {
|
|
507
|
+
resolve(JSON.parse(body));
|
|
508
|
+
} else if (isForm) {
|
|
509
|
+
resolve(fastQueryParse(body));
|
|
510
|
+
} else {
|
|
511
|
+
resolve(body);
|
|
512
|
+
}
|
|
513
|
+
} catch (e) {
|
|
514
|
+
reject(e);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const onError = (err: Error) => {
|
|
519
|
+
req.removeListener('data', onData);
|
|
520
|
+
req.removeListener('end', onEnd);
|
|
521
|
+
req.removeListener('error', onError);
|
|
522
|
+
reject(err);
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
req.on('data', onData);
|
|
526
|
+
req.on('end', onEnd);
|
|
527
|
+
req.on('error', onError);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Internal body parser - optimized for performance
|
|
533
|
+
* Uses string accumulation instead of Buffer.concat for better perf
|
|
534
|
+
* @deprecated Use parseBodyOptimized instead
|
|
535
|
+
*/
|
|
536
|
+
private parseBodyInternal(): Promise<any> {
|
|
537
|
+
return new Promise((resolve, reject) => {
|
|
538
|
+
const req = this.raw.req;
|
|
539
|
+
const contentType = req.headers['content-type'] || '';
|
|
540
|
+
|
|
541
|
+
// Use setEncoding to get strings directly - faster than Buffer.toString()
|
|
542
|
+
req.setEncoding('utf8');
|
|
543
|
+
|
|
544
|
+
let body = '';
|
|
545
|
+
|
|
546
|
+
req.on('data', (chunk: string) => {
|
|
547
|
+
body += chunk;
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
req.on('end', () => {
|
|
551
|
+
if (!body) {
|
|
552
|
+
resolve({});
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
// Inline content type check for hot path (JSON)
|
|
558
|
+
if (contentType.includes('application/json')) {
|
|
559
|
+
resolve(JSON.parse(body));
|
|
560
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
561
|
+
resolve(fastQueryParse(body));
|
|
562
|
+
} else {
|
|
563
|
+
resolve(body);
|
|
564
|
+
}
|
|
565
|
+
} catch (error) {
|
|
566
|
+
reject(error);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
req.on('error', reject);
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Parse body based on content type
|
|
576
|
+
*/
|
|
577
|
+
private parseContentType(body: string, contentType: string): any {
|
|
578
|
+
if (contentType.includes('application/json')) {
|
|
579
|
+
return body ? JSON.parse(body) : {};
|
|
580
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
581
|
+
return fastQueryParse(body);
|
|
582
|
+
} else if (contentType.includes('text/')) {
|
|
583
|
+
return body;
|
|
584
|
+
}
|
|
585
|
+
return body;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Clear body state (for pooling)
|
|
590
|
+
*/
|
|
591
|
+
clearBody(): void {
|
|
592
|
+
this._parsedBody = undefined;
|
|
593
|
+
this._bodyPromise = null;
|
|
594
|
+
this._bodyParsed = false;
|
|
198
595
|
}
|
|
199
596
|
|
|
200
597
|
// Convenience methods
|
|
@@ -252,6 +649,26 @@ export class ContextImpl implements Context {
|
|
|
252
649
|
return this._storeRegistry.get(StoreClass);
|
|
253
650
|
}
|
|
254
651
|
|
|
652
|
+
/**
|
|
653
|
+
* Lazy getter for request store registry
|
|
654
|
+
*/
|
|
655
|
+
private getOrCreateRequestStoreRegistry(): RequestStoreRegistry {
|
|
656
|
+
if (!this._requestStoreRegistry) {
|
|
657
|
+
this._requestStoreRegistry = new RequestStoreRegistry(this._debug);
|
|
658
|
+
}
|
|
659
|
+
return this._requestStoreRegistry;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Lazy getter for request data map
|
|
664
|
+
*/
|
|
665
|
+
private getOrCreateData(): Map<string, any> {
|
|
666
|
+
if (!this._data) {
|
|
667
|
+
this._data = new Map();
|
|
668
|
+
}
|
|
669
|
+
return this._data;
|
|
670
|
+
}
|
|
671
|
+
|
|
255
672
|
/**
|
|
256
673
|
* Access a request-scoped store by its class
|
|
257
674
|
* Store only exists for this request, disposed after response
|
|
@@ -281,7 +698,7 @@ export class ContextImpl implements Context {
|
|
|
281
698
|
* ```
|
|
282
699
|
*/
|
|
283
700
|
requestStore<T extends RequestStore<any>>(StoreClass: RequestStoreConstructor<T>): T {
|
|
284
|
-
return this.
|
|
701
|
+
return this.getOrCreateRequestStoreRegistry().get(StoreClass);
|
|
285
702
|
}
|
|
286
703
|
|
|
287
704
|
/**
|
|
@@ -302,7 +719,7 @@ export class ContextImpl implements Context {
|
|
|
302
719
|
* ```
|
|
303
720
|
*/
|
|
304
721
|
set<T = any>(key: string, value: T): void {
|
|
305
|
-
this.
|
|
722
|
+
this.getOrCreateData().set(key, value);
|
|
306
723
|
}
|
|
307
724
|
|
|
308
725
|
/**
|
|
@@ -318,7 +735,7 @@ export class ContextImpl implements Context {
|
|
|
318
735
|
* ```
|
|
319
736
|
*/
|
|
320
737
|
get<T = any>(key: string): T | undefined {
|
|
321
|
-
return this._data
|
|
738
|
+
return this._data?.get(key) as T | undefined;
|
|
322
739
|
}
|
|
323
740
|
|
|
324
741
|
/**
|
|
@@ -335,8 +752,8 @@ export class ContextImpl implements Context {
|
|
|
335
752
|
*/
|
|
336
753
|
setDebugMode(debug: boolean): void {
|
|
337
754
|
this._debug = debug;
|
|
338
|
-
//
|
|
339
|
-
this._requestStoreRegistry =
|
|
755
|
+
// Reset request store registry - will be created lazily with new debug mode
|
|
756
|
+
this._requestStoreRegistry = null;
|
|
340
757
|
}
|
|
341
758
|
|
|
342
759
|
/**
|
|
@@ -344,8 +761,17 @@ export class ContextImpl implements Context {
|
|
|
344
761
|
* @internal
|
|
345
762
|
*/
|
|
346
763
|
disposeRequestStores(): void {
|
|
347
|
-
this._requestStoreRegistry
|
|
348
|
-
|
|
764
|
+
if (this._requestStoreRegistry) {
|
|
765
|
+
this._requestStoreRegistry.dispose();
|
|
766
|
+
this._requestStoreRegistry = null;
|
|
767
|
+
}
|
|
768
|
+
if (this._data) {
|
|
769
|
+
this._data.clear();
|
|
770
|
+
}
|
|
771
|
+
// Release response builder back to pool
|
|
772
|
+
if (this.response && typeof (this.response as ResponseBuilderImpl).reset === 'function') {
|
|
773
|
+
releaseResponseBuilder(this.response as ResponseBuilderImpl);
|
|
774
|
+
}
|
|
349
775
|
}
|
|
350
776
|
|
|
351
777
|
/**
|
|
@@ -353,7 +779,7 @@ export class ContextImpl implements Context {
|
|
|
353
779
|
* @internal
|
|
354
780
|
*/
|
|
355
781
|
getRequestStoreRegistry(): RequestStoreRegistry {
|
|
356
|
-
return this.
|
|
782
|
+
return this.getOrCreateRequestStoreRegistry();
|
|
357
783
|
}
|
|
358
784
|
|
|
359
785
|
/**
|
|
@@ -364,22 +790,40 @@ export class ContextImpl implements Context {
|
|
|
364
790
|
}
|
|
365
791
|
|
|
366
792
|
/**
|
|
367
|
-
* Set
|
|
793
|
+
* Set response serializers for fast JSON serialization
|
|
794
|
+
* Called by router when route has response schema
|
|
795
|
+
* @internal
|
|
796
|
+
*/
|
|
797
|
+
setSerializers(serializers: Map<number | string, SerializerFunction>): void {
|
|
798
|
+
if (this.response && typeof (this.response as ResponseBuilderImpl).setSerializers === 'function') {
|
|
799
|
+
(this.response as ResponseBuilderImpl).setSerializers(serializers);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Set request body (called after parsing or by middleware)
|
|
805
|
+
* @deprecated Use ctx.getBody() for async body access
|
|
368
806
|
*/
|
|
369
807
|
setBody(body: any): void {
|
|
370
|
-
this.
|
|
808
|
+
this._parsedBody = body;
|
|
809
|
+
this._bodyParsed = true;
|
|
371
810
|
}
|
|
372
811
|
|
|
373
812
|
/**
|
|
374
813
|
* Get all Set-Cookie headers
|
|
375
814
|
*/
|
|
376
815
|
getSetCookieHeaders(): string[] {
|
|
377
|
-
|
|
816
|
+
// Use _cookies directly to avoid creating CookieManager if not needed
|
|
817
|
+
if (!this._cookies) {
|
|
818
|
+
return [];
|
|
819
|
+
}
|
|
820
|
+
return this._cookies.getSetCookieHeaders();
|
|
378
821
|
}
|
|
379
822
|
}
|
|
380
823
|
|
|
381
824
|
/**
|
|
382
825
|
* Parse request body based on Content-Type
|
|
826
|
+
* @deprecated Use ctx.getBody() instead for lazy parsing
|
|
383
827
|
*/
|
|
384
828
|
export async function parseBody(req: IncomingMessage): Promise<any> {
|
|
385
829
|
return new Promise((resolve, reject) => {
|
|
@@ -396,7 +840,7 @@ export async function parseBody(req: IncomingMessage): Promise<any> {
|
|
|
396
840
|
if (contentType.includes('application/json')) {
|
|
397
841
|
resolve(body ? JSON.parse(body) : {});
|
|
398
842
|
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
399
|
-
resolve(
|
|
843
|
+
resolve(fastQueryParse(body));
|
|
400
844
|
} else if (contentType.includes('text/')) {
|
|
401
845
|
resolve(body);
|
|
402
846
|
} else {
|