@engjts/nexus 0.1.7 → 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.
Files changed (72) hide show
  1. package/BENCHMARK_REPORT.md +343 -0
  2. package/dist/advanced/playground/generatePlaygroundHTML.d.ts.map +1 -1
  3. package/dist/advanced/playground/generatePlaygroundHTML.js +107 -0
  4. package/dist/advanced/playground/generatePlaygroundHTML.js.map +1 -1
  5. package/dist/advanced/playground/playground.d.ts +19 -0
  6. package/dist/advanced/playground/playground.d.ts.map +1 -1
  7. package/dist/advanced/playground/playground.js +70 -0
  8. package/dist/advanced/playground/playground.js.map +1 -1
  9. package/dist/advanced/playground/types.d.ts +20 -0
  10. package/dist/advanced/playground/types.d.ts.map +1 -1
  11. package/dist/core/application.d.ts +14 -0
  12. package/dist/core/application.d.ts.map +1 -1
  13. package/dist/core/application.js +173 -71
  14. package/dist/core/application.js.map +1 -1
  15. package/dist/core/context-pool.d.ts +2 -13
  16. package/dist/core/context-pool.d.ts.map +1 -1
  17. package/dist/core/context-pool.js +7 -45
  18. package/dist/core/context-pool.js.map +1 -1
  19. package/dist/core/context.d.ts +108 -5
  20. package/dist/core/context.d.ts.map +1 -1
  21. package/dist/core/context.js +449 -53
  22. package/dist/core/context.js.map +1 -1
  23. package/dist/core/index.d.ts +1 -0
  24. package/dist/core/index.d.ts.map +1 -1
  25. package/dist/core/index.js +9 -1
  26. package/dist/core/index.js.map +1 -1
  27. package/dist/core/middleware.d.ts +6 -0
  28. package/dist/core/middleware.d.ts.map +1 -1
  29. package/dist/core/middleware.js +83 -84
  30. package/dist/core/middleware.js.map +1 -1
  31. package/dist/core/performance/fast-json.d.ts +149 -0
  32. package/dist/core/performance/fast-json.d.ts.map +1 -0
  33. package/dist/core/performance/fast-json.js +473 -0
  34. package/dist/core/performance/fast-json.js.map +1 -0
  35. package/dist/core/router/file-router.d.ts +20 -7
  36. package/dist/core/router/file-router.d.ts.map +1 -1
  37. package/dist/core/router/file-router.js +41 -13
  38. package/dist/core/router/file-router.js.map +1 -1
  39. package/dist/core/router/index.d.ts +6 -0
  40. package/dist/core/router/index.d.ts.map +1 -1
  41. package/dist/core/router/index.js +33 -6
  42. package/dist/core/router/index.js.map +1 -1
  43. package/dist/core/router/radix-tree.d.ts +4 -1
  44. package/dist/core/router/radix-tree.d.ts.map +1 -1
  45. package/dist/core/router/radix-tree.js +7 -3
  46. package/dist/core/router/radix-tree.js.map +1 -1
  47. package/dist/core/serializer.d.ts +251 -0
  48. package/dist/core/serializer.d.ts.map +1 -0
  49. package/dist/core/serializer.js +290 -0
  50. package/dist/core/serializer.js.map +1 -0
  51. package/dist/core/types.d.ts +39 -1
  52. package/dist/core/types.d.ts.map +1 -1
  53. package/dist/core/types.js.map +1 -1
  54. package/dist/index.d.ts +1 -0
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +12 -2
  57. package/dist/index.js.map +1 -1
  58. package/package.json +3 -1
  59. package/src/advanced/playground/generatePlaygroundHTML.ts +107 -0
  60. package/src/advanced/playground/playground.ts +225 -145
  61. package/src/advanced/playground/types.ts +29 -0
  62. package/src/core/application.ts +202 -84
  63. package/src/core/context-pool.ts +8 -56
  64. package/src/core/context.ts +497 -53
  65. package/src/core/index.ts +14 -0
  66. package/src/core/middleware.ts +99 -89
  67. package/src/core/router/file-router.ts +41 -12
  68. package/src/core/router/index.ts +213 -180
  69. package/src/core/router/radix-tree.ts +20 -4
  70. package/src/core/serializer.ts +397 -0
  71. package/src/core/types.ts +43 -1
  72. package/src/index.ts +17 -0
@@ -1045,145 +1045,211 @@ export class Application<TDeps extends DependencyContainer = {}> {
1045
1045
 
1046
1046
  /**
1047
1047
  * Handle incoming HTTP request
1048
+ * Optimized hot path for maximum performance
1048
1049
  */
1049
1050
  private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
1050
- let ctx: Context | null = null;
1051
- let response: Response | null = null;
1052
-
1051
+ // Use non-null assertion - context pool always returns valid context
1052
+ // acquire is now sync for better performance
1053
+ const ctx = this.contextPool.acquire(req, res) as any;
1054
+
1053
1055
  try {
1054
- // Acquire context from pool
1055
- ctx = await this.contextPool.acquire(req, res);
1056
-
1057
- // Inject store registry into context
1058
- if ('setStoreRegistry' in ctx && typeof ctx.setStoreRegistry === 'function') {
1059
- (ctx as any).setStoreRegistry(this.storeRegistry);
1060
- }
1061
-
1062
- // Set debug mode for request stores
1063
- if ('setDebugMode' in ctx && typeof ctx.setDebugMode === 'function') {
1064
- (ctx as any).setDebugMode(this.config.debug ?? false);
1065
- }
1056
+ // Direct method calls - ContextImpl always has these methods
1057
+ ctx.setStoreRegistry(this.storeRegistry);
1058
+ ctx.setDebugMode(this.config.debug ?? false);
1066
1059
 
1067
- // === HOOK: onRequest ===
1060
+ // === HOOK: onRequest (skip check if no hooks) ===
1068
1061
  if (this.lifecycleHooks.onRequest) {
1069
1062
  const hookResult = await this.lifecycleHooks.onRequest(ctx);
1070
- if (this.isResponse(hookResult)) {
1063
+ if (hookResult && typeof hookResult === 'object' && 'statusCode' in hookResult) {
1071
1064
  await this.sendResponse(res, hookResult, ctx);
1065
+ this.cleanupRequest(ctx);
1072
1066
  return;
1073
1067
  }
1074
1068
  }
1075
1069
 
1076
- // Apply versioning if configured
1070
+ // Fast path: no versioning configured (most common)
1077
1071
  let matchPath = ctx.path;
1078
1072
  if (this.versioningConfig) {
1079
1073
  const { version, basePath, source } = this.resolveVersion(ctx);
1080
1074
  ctx.version = version;
1081
1075
  ctx.versionSource = source;
1082
1076
 
1083
- // For header/query strategy, rewrite path to versioned path
1084
- if (source === 'header' || source === 'query' || source === 'default') {
1077
+ if (source !== 'path') {
1085
1078
  matchPath = `/${version}${basePath.startsWith('/') ? basePath : '/' + basePath}`;
1086
1079
  }
1087
-
1088
- if (this.config.debug) {
1089
- console.log(`[Versioning] ${source} → ${version}, path: ${ctx.path} → ${matchPath}`);
1090
- }
1091
1080
  }
1092
1081
 
1093
1082
  // Find matching route
1094
1083
  const match = this.router.match(ctx.method, matchPath);
1095
1084
 
1096
1085
  if (!match) {
1097
- // Try fallback handler (e.g., static files)
1086
+ // Try fallback handler
1098
1087
  if (this.fallbackHandler) {
1099
1088
  const fallbackResponse = await this.fallbackHandler(ctx, this.dependencies);
1100
1089
  await this.sendResponse(res, fallbackResponse, ctx);
1090
+ this.cleanupRequest(ctx);
1101
1091
  return;
1102
1092
  }
1103
1093
 
1104
- // 404 Not Found
1105
- await this.sendResponse(res, {
1106
- statusCode: 404,
1107
- headers: { 'Content-Type': 'application/json' },
1108
- body: JSON.stringify({ error: 'Not Found' })
1109
- }, ctx);
1094
+ // 404 Not Found - use cached response
1095
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1096
+ res.end('{"error":"Not Found"}');
1097
+ this.cleanupRequest(ctx);
1110
1098
  return;
1111
1099
  }
1112
1100
 
1113
- // Set route params
1114
- if ('setParams' in ctx && typeof ctx.setParams === 'function') {
1115
- ctx.setParams(match.params);
1116
- } else {
1117
- ctx.params = match.params;
1101
+ // Set route params directly
1102
+ ctx.params = match.params;
1103
+
1104
+ // Set serializers for fast JSON response if available
1105
+ if (match._serializer) {
1106
+ ctx.setSerializers(match._serializer);
1118
1107
  }
1119
1108
 
1120
- // Combine global and route-specific middleware
1121
- const allMiddlewares = [...this.globalMiddlewares, ...match.middlewares];
1109
+ // Auto-parse body for POST/PUT/PATCH if body exists
1110
+ // This maintains backwards compatibility while keeping lazy-loading for GET
1111
+ const method = ctx.method;
1112
+ if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
1113
+ // Only parse if there's actually a body (Content-Length > 0 or Transfer-Encoding)
1114
+ const contentLength = req.headers['content-length'];
1115
+ const transferEncoding = req.headers['transfer-encoding'];
1116
+ const hasBody = (contentLength && contentLength !== '0') || transferEncoding;
1117
+
1118
+ if (hasBody) {
1119
+ await ctx.getBody();
1120
+ }
1121
+ }
1122
1122
 
1123
- // Execute middleware chain and handler with hooks
1124
- response = await this.middlewareExecutor.executeWithHooks(
1125
- ctx,
1126
- allMiddlewares,
1127
- match.handler,
1128
- this.lifecycleHooks,
1129
- this.dependencies
1130
- );
1123
+ // Fast path: no middleware
1124
+ let response: Response;
1125
+ if (this.globalMiddlewares.length === 0 && match.middlewares.length === 0) {
1126
+ // Direct handler execution (skip middleware executor)
1127
+ response = await this.executeHandlerDirect(ctx, match.handler);
1128
+ } else {
1129
+ // Combine global and route-specific middleware
1130
+ const allMiddlewares = this.globalMiddlewares.length === 0
1131
+ ? match.middlewares
1132
+ : match.middlewares.length === 0
1133
+ ? this.globalMiddlewares
1134
+ : [...this.globalMiddlewares, ...match.middlewares];
1135
+
1136
+ // Execute middleware chain and handler with hooks
1137
+ response = await this.middlewareExecutor.executeWithHooks(
1138
+ ctx,
1139
+ allMiddlewares,
1140
+ match.handler,
1141
+ this.lifecycleHooks,
1142
+ this.dependencies
1143
+ );
1144
+ }
1131
1145
 
1132
1146
  // === HOOK: onResponse ===
1133
1147
  if (this.lifecycleHooks.onResponse) {
1134
1148
  const hookResult = await this.lifecycleHooks.onResponse(ctx, response);
1135
- if (this.isResponse(hookResult)) {
1149
+ if (hookResult && typeof hookResult === 'object' && 'statusCode' in hookResult) {
1136
1150
  response = hookResult;
1137
1151
  }
1138
1152
  }
1139
1153
 
1140
1154
  // Send response
1141
1155
  await this.sendResponse(res, response, ctx);
1156
+ this.cleanupRequest(ctx);
1142
1157
 
1143
1158
  } catch (error) {
1159
+ await this.handleError(error as Error, ctx, res);
1160
+ }
1161
+ }
1162
+
1163
+ /**
1164
+ * Execute handler directly without middleware (fast path)
1165
+ */
1166
+ private async executeHandlerDirect(ctx: any, handler: Handler): Promise<Response> {
1167
+ // === HOOK: beforeHandler ===
1168
+ if (this.lifecycleHooks.beforeHandler) {
1169
+ const hookResult = await this.lifecycleHooks.beforeHandler(ctx);
1170
+ if (hookResult && typeof hookResult === 'object' && 'statusCode' in hookResult) {
1171
+ return hookResult;
1172
+ }
1173
+ }
1174
+
1175
+ let result = await handler(ctx, this.dependencies);
1176
+
1177
+ // If handler returns an Error, throw it
1178
+ if (result instanceof Error) {
1179
+ (result as any)._isIntentional = true;
1180
+ throw result;
1181
+ }
1182
+
1183
+ // === HOOK: afterHandler ===
1184
+ if (this.lifecycleHooks.afterHandler) {
1185
+ const transformedResult = await this.lifecycleHooks.afterHandler(ctx, result);
1186
+ if (transformedResult !== undefined) {
1187
+ result = transformedResult;
1188
+ }
1189
+ }
1190
+
1191
+ // If result is a Response, return it
1192
+ if (result && typeof result === 'object' && 'statusCode' in result && 'body' in result) {
1193
+ return result as Response;
1194
+ }
1195
+
1196
+ // Otherwise, wrap in JSON response
1197
+ return ctx.json(result);
1198
+ }
1199
+
1200
+ /**
1201
+ * Cleanup request resources
1202
+ */
1203
+ private cleanupRequest(ctx: any): void {
1204
+ // Dispose request-scoped stores
1205
+ if (ctx.disposeRequestStores) {
1206
+ ctx.disposeRequestStores();
1207
+ }
1208
+ // Release context back to pool
1209
+ this.contextPool.release(ctx);
1210
+ }
1211
+
1212
+ /**
1213
+ * Handle errors
1214
+ */
1215
+ private async handleError(error: Error, ctx: any, res: ServerResponse): Promise<void> {
1216
+ try {
1144
1217
  // === HOOK: onError ===
1145
1218
  if (ctx && this.lifecycleHooks.onError) {
1146
1219
  try {
1147
- const hookResult = await this.lifecycleHooks.onError(ctx, error as Error);
1148
- if (this.isResponse(hookResult)) {
1220
+ const hookResult = await this.lifecycleHooks.onError(ctx, error);
1221
+ if (hookResult && typeof hookResult === 'object' && 'statusCode' in hookResult) {
1149
1222
  await this.sendResponse(res, hookResult, ctx);
1223
+ this.cleanupRequest(ctx);
1150
1224
  return;
1151
1225
  }
1152
1226
  } catch (hookError) {
1153
- // Hook itself threw an error, continue to default error handler
1154
1227
  console.error('onError hook failed:', hookError);
1155
1228
  }
1156
1229
  }
1157
1230
 
1158
1231
  // Handle errors with default error handler
1159
- try {
1160
- const errorResponse = await this.errorHandler(error as Error, ctx!);
1161
-
1162
- // === HOOK: onResponse (for error responses) ===
1163
- if (ctx && this.lifecycleHooks.onResponse) {
1164
- const hookResult = await this.lifecycleHooks.onResponse(ctx, errorResponse);
1165
- if (this.isResponse(hookResult)) {
1166
- await this.sendResponse(res, hookResult, ctx);
1167
- return;
1168
- }
1232
+ const errorResponse = await this.errorHandler(error, ctx);
1233
+
1234
+ // === HOOK: onResponse (for error responses) ===
1235
+ if (ctx && this.lifecycleHooks.onResponse) {
1236
+ const hookResult = await this.lifecycleHooks.onResponse(ctx, errorResponse);
1237
+ if (hookResult && typeof hookResult === 'object' && 'statusCode' in hookResult) {
1238
+ await this.sendResponse(res, hookResult, ctx);
1239
+ this.cleanupRequest(ctx);
1240
+ return;
1169
1241
  }
1170
-
1171
- await this.sendResponse(res, errorResponse, ctx!);
1172
- } catch (handlerError) {
1173
- // Fallback error response
1174
- res.writeHead(500, { 'Content-Type': 'text/plain' });
1175
- res.end('Internal Server Error');
1176
- }
1177
- } finally {
1178
- // Dispose request-scoped stores
1179
- if (ctx && 'disposeRequestStores' in ctx && typeof ctx.disposeRequestStores === 'function') {
1180
- (ctx as any).disposeRequestStores();
1181
1242
  }
1182
1243
 
1183
- // Release context back to pool
1184
- if (ctx) {
1185
- this.contextPool.release(ctx);
1186
- }
1244
+ await this.sendResponse(res, errorResponse, ctx);
1245
+ } catch (handlerError) {
1246
+ // Fallback error response
1247
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
1248
+ res.end('Internal Server Error');
1249
+ }
1250
+
1251
+ if (ctx) {
1252
+ this.cleanupRequest(ctx);
1187
1253
  }
1188
1254
  }
1189
1255
 
@@ -1202,13 +1268,65 @@ export class Application<TDeps extends DependencyContainer = {}> {
1202
1268
 
1203
1269
  /**
1204
1270
  * Send HTTP response
1271
+ * Optimized for minimal overhead - key for benchmark performance
1205
1272
  */
1206
1273
  private async sendResponse(res: ServerResponse, response: Response, ctx: Context): Promise<void> {
1207
- // Set status code
1208
- res.statusCode = response.statusCode;
1274
+ // Handle stream responses separately
1275
+ if (response.stream) {
1276
+ res.statusCode = response.statusCode;
1277
+ const headers = response.headers;
1278
+ for (const key in headers) {
1279
+ const value = headers[key];
1280
+ if (value !== undefined) {
1281
+ res.setHeader(key, value);
1282
+ }
1283
+ }
1284
+ response.stream.pipe(res);
1285
+ return;
1286
+ }
1209
1287
 
1210
- // Set headers
1211
- for (const [key, value] of Object.entries(response.headers)) {
1288
+ const body = response.body;
1289
+ const headers = response.headers;
1290
+
1291
+ // Fast path for JSON responses (most common case)
1292
+ // Use writeHead for single syscall instead of multiple setHeader calls
1293
+ if (headers['Content-Type'] === 'application/json') {
1294
+ // Calculate content length for proper HTTP behavior
1295
+ const contentLength = Buffer.byteLength(body, 'utf8');
1296
+
1297
+ // Check if we need to set cookies
1298
+ let setCookies: string[] | null = null;
1299
+ if ('getSetCookieHeaders' in ctx && typeof ctx.getSetCookieHeaders === 'function') {
1300
+ const cookies = ctx.getSetCookieHeaders();
1301
+ if (cookies.length > 0) {
1302
+ setCookies = cookies;
1303
+ }
1304
+ }
1305
+
1306
+ // Single writeHead call for all headers
1307
+ if (setCookies) {
1308
+ res.writeHead(response.statusCode, {
1309
+ 'Content-Type': 'application/json',
1310
+ 'Content-Length': contentLength,
1311
+ 'Set-Cookie': setCookies
1312
+ });
1313
+ } else {
1314
+ res.writeHead(response.statusCode, {
1315
+ 'Content-Type': 'application/json',
1316
+ 'Content-Length': contentLength
1317
+ });
1318
+ }
1319
+
1320
+ res.end(body);
1321
+ return;
1322
+ }
1323
+
1324
+ // General path for other response types
1325
+ res.statusCode = response.statusCode;
1326
+
1327
+ // Set headers using for-in (faster than Object.entries)
1328
+ for (const key in headers) {
1329
+ const value = headers[key];
1212
1330
  if (value !== undefined) {
1213
1331
  res.setHeader(key, value);
1214
1332
  }
@@ -1222,12 +1340,12 @@ export class Application<TDeps extends DependencyContainer = {}> {
1222
1340
  }
1223
1341
  }
1224
1342
 
1225
- // Send body or stream
1226
- if (response.stream) {
1227
- response.stream.pipe(res);
1228
- } else {
1229
- res.end(response.body);
1343
+ // Set Content-Length for non-stream responses
1344
+ if (body && typeof body === 'string') {
1345
+ res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'));
1230
1346
  }
1347
+
1348
+ res.end(body);
1231
1349
  }
1232
1350
 
1233
1351
  /**
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { IncomingMessage, ServerResponse } from 'http';
7
- import { ContextImpl, parseBody } from './context';
7
+ import { ContextImpl } from './context';
8
8
  import { Context } from './types';
9
9
 
10
10
  /**
@@ -23,26 +23,25 @@ export class ContextPool {
23
23
 
24
24
  /**
25
25
  * Acquire a context from the pool or create a new one
26
+ * Body parsing is now lazy - handlers should use ctx.getBody() for POST/PUT/PATCH
26
27
  */
27
- async acquire(req: IncomingMessage, res: ServerResponse): Promise<Context> {
28
+ acquire(req: IncomingMessage, res: ServerResponse): Context {
28
29
  let ctx: ContextImpl;
29
30
 
30
31
  // Try to reuse from pool
31
32
  if (this.pool.length > 0) {
32
33
  ctx = this.pool.pop()!;
33
34
  this.reused++;
34
- await this.reset(ctx, req, res);
35
+ // Use reinitialize instead of creating new object
36
+ ctx.reinitialize(req, res);
35
37
  } else {
36
38
  // Create new context
37
39
  ctx = new ContextImpl(req, res);
38
40
  this.created++;
39
41
  }
40
42
 
41
- // Parse body if applicable
42
- if (this.hasBody(req)) {
43
- const body = await parseBody(req);
44
- ctx.setBody(body);
45
- }
43
+ // Body parsing removed - now lazy via ctx.getBody()
44
+ // This matches Fastify's architecture for optimal POST performance
46
45
 
47
46
  return ctx;
48
47
  }
@@ -52,59 +51,12 @@ export class ContextPool {
52
51
  */
53
52
  release(ctx: Context): void {
54
53
  if (this.pool.length < this.maxSize) {
55
- // Clear sensitive data before pooling
56
- this.clearContext(ctx as ContextImpl);
54
+ // Just push back - reinitialize will handle reset
57
55
  this.pool.push(ctx as ContextImpl);
58
56
  }
59
57
  // If pool is full, let GC handle it
60
58
  }
61
59
 
62
- /**
63
- * Reset context for reuse
64
- */
65
- private async reset(
66
- ctx: ContextImpl,
67
- req: IncomingMessage,
68
- res: ServerResponse
69
- ): Promise<void> {
70
- // Recreate the context with new request/response
71
- const newCtx = new ContextImpl(req, res);
72
-
73
- // Copy properties
74
- Object.assign(ctx, newCtx);
75
- }
76
-
77
- /**
78
- * Clear sensitive data from context
79
- */
80
- private clearContext(ctx: ContextImpl): void {
81
- ctx.params = {};
82
- ctx.query = {};
83
- ctx.body = null;
84
-
85
- // Clear custom properties added by middleware
86
- const knownProps = new Set([
87
- 'method', 'path', 'url', 'params', 'query', 'body',
88
- 'headers', 'cookies', 'raw', 'response',
89
- 'json', 'html', 'text', 'redirect', 'stream',
90
- 'setParams', 'setBody', 'getSetCookieHeaders'
91
- ]);
92
-
93
- for (const key in ctx) {
94
- if (!knownProps.has(key)) {
95
- delete (ctx as any)[key];
96
- }
97
- }
98
- }
99
-
100
- /**
101
- * Check if request has a body
102
- */
103
- private hasBody(req: IncomingMessage): boolean {
104
- const method = req.method?.toUpperCase();
105
- return method === 'POST' || method === 'PUT' || method === 'PATCH';
106
- }
107
-
108
60
  /**
109
61
  * Get pool statistics
110
62
  */