@engjts/nexus 0.1.8 → 0.1.9

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 (205) hide show
  1. package/package.json +1 -1
  2. package/BENCHMARK_REPORT.md +0 -343
  3. package/documentation/01-getting-started.md +0 -240
  4. package/documentation/02-context.md +0 -335
  5. package/documentation/03-routing.md +0 -397
  6. package/documentation/04-middleware.md +0 -483
  7. package/documentation/05-validation.md +0 -514
  8. package/documentation/06-error-handling.md +0 -465
  9. package/documentation/07-performance.md +0 -364
  10. package/documentation/08-adapters.md +0 -470
  11. package/documentation/09-api-reference.md +0 -548
  12. package/documentation/10-examples.md +0 -582
  13. package/documentation/11-deployment.md +0 -477
  14. package/documentation/12-sentry.md +0 -620
  15. package/documentation/13-sentry-data-storage.md +0 -996
  16. package/documentation/14-sentry-data-reference.md +0 -457
  17. package/documentation/15-sentry-summary.md +0 -409
  18. package/documentation/16-alerts-system.md +0 -745
  19. package/documentation/17-alert-adapters.md +0 -696
  20. package/documentation/18-alerts-implementation-summary.md +0 -385
  21. package/documentation/19-class-based-routing.md +0 -840
  22. package/documentation/20-websocket-realtime.md +0 -813
  23. package/documentation/21-cache-system.md +0 -510
  24. package/documentation/22-job-queue.md +0 -772
  25. package/documentation/23-sentry-plugin.md +0 -551
  26. package/documentation/24-testing-utilities.md +0 -1287
  27. package/documentation/25-api-versioning.md +0 -533
  28. package/documentation/26-context-store.md +0 -607
  29. package/documentation/27-dependency-injection.md +0 -329
  30. package/documentation/28-lifecycle-hooks.md +0 -521
  31. package/documentation/29-package-structure.md +0 -196
  32. package/documentation/30-plugin-system.md +0 -414
  33. package/documentation/31-jwt-authentication.md +0 -597
  34. package/documentation/32-cli.md +0 -268
  35. package/documentation/ALERTS-COMPLETE-SUMMARY.md +0 -429
  36. package/documentation/ALERTS-INDEX.md +0 -330
  37. package/documentation/ALERTS-QUICK-REFERENCE.md +0 -286
  38. package/documentation/README.md +0 -178
  39. package/documentation/index.html +0 -34
  40. package/modern_framework_paper.md +0 -1870
  41. package/public/css/style.css +0 -87
  42. package/public/index.html +0 -34
  43. package/public/js/app.js +0 -27
  44. package/src/advanced/cache/InMemoryCacheStore.ts +0 -68
  45. package/src/advanced/cache/MultiTierCache.ts +0 -194
  46. package/src/advanced/cache/RedisCacheStore.ts +0 -341
  47. package/src/advanced/cache/index.ts +0 -5
  48. package/src/advanced/cache/types.ts +0 -40
  49. package/src/advanced/graphql/SimpleDataLoader.ts +0 -42
  50. package/src/advanced/graphql/index.ts +0 -22
  51. package/src/advanced/graphql/server.ts +0 -252
  52. package/src/advanced/graphql/types.ts +0 -42
  53. package/src/advanced/jobs/InMemoryQueueStore.ts +0 -68
  54. package/src/advanced/jobs/JobQueue.ts +0 -556
  55. package/src/advanced/jobs/RedisQueueStore.ts +0 -367
  56. package/src/advanced/jobs/index.ts +0 -5
  57. package/src/advanced/jobs/types.ts +0 -70
  58. package/src/advanced/observability/APMManager.ts +0 -163
  59. package/src/advanced/observability/AlertManager.ts +0 -109
  60. package/src/advanced/observability/MetricRegistry.ts +0 -151
  61. package/src/advanced/observability/ObservabilityCenter.ts +0 -304
  62. package/src/advanced/observability/StructuredLogger.ts +0 -154
  63. package/src/advanced/observability/TracingManager.ts +0 -117
  64. package/src/advanced/observability/adapters.ts +0 -304
  65. package/src/advanced/observability/createObservabilityMiddleware.ts +0 -63
  66. package/src/advanced/observability/index.ts +0 -11
  67. package/src/advanced/observability/types.ts +0 -174
  68. package/src/advanced/playground/extractPathParams.ts +0 -6
  69. package/src/advanced/playground/generateFieldExample.ts +0 -31
  70. package/src/advanced/playground/generatePlaygroundHTML.ts +0 -1956
  71. package/src/advanced/playground/generateSummary.ts +0 -19
  72. package/src/advanced/playground/getTagFromPath.ts +0 -9
  73. package/src/advanced/playground/index.ts +0 -8
  74. package/src/advanced/playground/playground.ts +0 -250
  75. package/src/advanced/playground/types.ts +0 -49
  76. package/src/advanced/playground/zodToExample.ts +0 -16
  77. package/src/advanced/playground/zodToParams.ts +0 -15
  78. package/src/advanced/postman/buildAuth.ts +0 -31
  79. package/src/advanced/postman/buildBody.ts +0 -15
  80. package/src/advanced/postman/buildQueryParams.ts +0 -27
  81. package/src/advanced/postman/buildRequestItem.ts +0 -36
  82. package/src/advanced/postman/buildResponses.ts +0 -11
  83. package/src/advanced/postman/buildUrl.ts +0 -33
  84. package/src/advanced/postman/capitalize.ts +0 -4
  85. package/src/advanced/postman/generateCollection.ts +0 -59
  86. package/src/advanced/postman/generateEnvironment.ts +0 -34
  87. package/src/advanced/postman/generateExampleFromZod.ts +0 -21
  88. package/src/advanced/postman/generateFieldExample.ts +0 -45
  89. package/src/advanced/postman/generateName.ts +0 -20
  90. package/src/advanced/postman/generateUUID.ts +0 -11
  91. package/src/advanced/postman/getTagFromPath.ts +0 -10
  92. package/src/advanced/postman/index.ts +0 -28
  93. package/src/advanced/postman/postman.ts +0 -156
  94. package/src/advanced/postman/slugify.ts +0 -7
  95. package/src/advanced/postman/types.ts +0 -140
  96. package/src/advanced/realtime/index.ts +0 -18
  97. package/src/advanced/realtime/websocket.ts +0 -231
  98. package/src/advanced/sentry/index.ts +0 -1236
  99. package/src/advanced/sentry/types.ts +0 -355
  100. package/src/advanced/static/generateDirectoryListing.ts +0 -47
  101. package/src/advanced/static/generateETag.ts +0 -7
  102. package/src/advanced/static/getMimeType.ts +0 -9
  103. package/src/advanced/static/index.ts +0 -32
  104. package/src/advanced/static/isSafePath.ts +0 -13
  105. package/src/advanced/static/publicDir.ts +0 -21
  106. package/src/advanced/static/serveStatic.ts +0 -225
  107. package/src/advanced/static/spa.ts +0 -24
  108. package/src/advanced/static/types.ts +0 -159
  109. package/src/advanced/swagger/SwaggerGenerator.ts +0 -66
  110. package/src/advanced/swagger/buildOperation.ts +0 -61
  111. package/src/advanced/swagger/buildParameters.ts +0 -61
  112. package/src/advanced/swagger/buildRequestBody.ts +0 -21
  113. package/src/advanced/swagger/buildResponses.ts +0 -54
  114. package/src/advanced/swagger/capitalize.ts +0 -5
  115. package/src/advanced/swagger/convertPath.ts +0 -9
  116. package/src/advanced/swagger/createSwagger.ts +0 -12
  117. package/src/advanced/swagger/generateOperationId.ts +0 -21
  118. package/src/advanced/swagger/generateSpec.ts +0 -105
  119. package/src/advanced/swagger/generateSummary.ts +0 -24
  120. package/src/advanced/swagger/generateSwaggerUI.ts +0 -70
  121. package/src/advanced/swagger/generateThemeCss.ts +0 -53
  122. package/src/advanced/swagger/index.ts +0 -25
  123. package/src/advanced/swagger/swagger.ts +0 -237
  124. package/src/advanced/swagger/types.ts +0 -206
  125. package/src/advanced/swagger/zodFieldToOpenAPI.ts +0 -94
  126. package/src/advanced/swagger/zodSchemaToOpenAPI.ts +0 -50
  127. package/src/advanced/swagger/zodToOpenAPI.ts +0 -22
  128. package/src/advanced/testing/factory.ts +0 -509
  129. package/src/advanced/testing/harness.ts +0 -612
  130. package/src/advanced/testing/index.ts +0 -430
  131. package/src/advanced/testing/load-test.ts +0 -618
  132. package/src/advanced/testing/mock-server.ts +0 -498
  133. package/src/advanced/testing/mock.ts +0 -670
  134. package/src/cli/bin.ts +0 -9
  135. package/src/cli/cli.ts +0 -158
  136. package/src/cli/commands/add.ts +0 -178
  137. package/src/cli/commands/build.ts +0 -73
  138. package/src/cli/commands/create.ts +0 -166
  139. package/src/cli/commands/dev.ts +0 -85
  140. package/src/cli/commands/generate.ts +0 -99
  141. package/src/cli/commands/help.ts +0 -95
  142. package/src/cli/commands/init.ts +0 -91
  143. package/src/cli/commands/version.ts +0 -38
  144. package/src/cli/index.ts +0 -6
  145. package/src/cli/templates/generators.ts +0 -359
  146. package/src/cli/templates/index.ts +0 -680
  147. package/src/cli/utils/exec.ts +0 -52
  148. package/src/cli/utils/file-system.ts +0 -78
  149. package/src/cli/utils/logger.ts +0 -111
  150. package/src/core/adapter.ts +0 -88
  151. package/src/core/application.ts +0 -1453
  152. package/src/core/context-pool.ts +0 -79
  153. package/src/core/context.ts +0 -856
  154. package/src/core/index.ts +0 -94
  155. package/src/core/middleware.ts +0 -272
  156. package/src/core/performance/buffer-pool.ts +0 -108
  157. package/src/core/performance/middleware-optimizer.ts +0 -162
  158. package/src/core/plugin/PluginManager.ts +0 -435
  159. package/src/core/plugin/builder.ts +0 -358
  160. package/src/core/plugin/index.ts +0 -50
  161. package/src/core/plugin/types.ts +0 -214
  162. package/src/core/router/file-router.ts +0 -623
  163. package/src/core/router/index.ts +0 -260
  164. package/src/core/router/radix-tree.ts +0 -242
  165. package/src/core/serializer.ts +0 -397
  166. package/src/core/store/index.ts +0 -30
  167. package/src/core/store/registry.ts +0 -178
  168. package/src/core/store/request-store.ts +0 -240
  169. package/src/core/store/types.ts +0 -233
  170. package/src/core/types.ts +0 -616
  171. package/src/database/adapter.ts +0 -35
  172. package/src/database/adapters/index.ts +0 -1
  173. package/src/database/adapters/mysql.ts +0 -669
  174. package/src/database/database.ts +0 -70
  175. package/src/database/dialect.ts +0 -388
  176. package/src/database/index.ts +0 -12
  177. package/src/database/migrations.ts +0 -86
  178. package/src/database/optimizer.ts +0 -125
  179. package/src/database/query-builder.ts +0 -404
  180. package/src/database/realtime.ts +0 -53
  181. package/src/database/schema.ts +0 -71
  182. package/src/database/transactions.ts +0 -56
  183. package/src/database/types.ts +0 -87
  184. package/src/deployment/cluster.ts +0 -471
  185. package/src/deployment/config.ts +0 -454
  186. package/src/deployment/docker.ts +0 -599
  187. package/src/deployment/graceful-shutdown.ts +0 -373
  188. package/src/deployment/index.ts +0 -56
  189. package/src/index.ts +0 -281
  190. package/src/security/adapter.ts +0 -318
  191. package/src/security/auth/JWTPlugin.ts +0 -234
  192. package/src/security/auth/JWTProvider.ts +0 -316
  193. package/src/security/auth/adapter.ts +0 -12
  194. package/src/security/auth/jwt.ts +0 -234
  195. package/src/security/auth/middleware.ts +0 -188
  196. package/src/security/csrf.ts +0 -220
  197. package/src/security/headers.ts +0 -108
  198. package/src/security/index.ts +0 -60
  199. package/src/security/rate-limit/adapter.ts +0 -7
  200. package/src/security/rate-limit/memory.ts +0 -108
  201. package/src/security/rate-limit/middleware.ts +0 -181
  202. package/src/security/sanitization.ts +0 -75
  203. package/src/security/types.ts +0 -240
  204. package/src/security/utils.ts +0 -52
  205. package/tsconfig.json +0 -39
@@ -1,1453 +0,0 @@
1
- /**
2
- * Main application class
3
- * Orchestrates all framework components
4
- */
5
-
6
- import { createServer, Server as HTTPServer, IncomingMessage, ServerResponse } from 'http';
7
- import { Router } from './router';
8
- import { ContextPool } from './context-pool';
9
- import { MiddlewareExecutor } from './middleware';
10
- import {
11
- Context,
12
- Handler,
13
- Middleware,
14
- Response,
15
- AppConfig,
16
- ErrorHandler,
17
- RouteConfig,
18
- Plugin,
19
- HTTPMethod,
20
- RouteBase,
21
- Route,
22
- VersioningConfig,
23
- VersioningStrategy,
24
- DependencyContainer,
25
- InjectedRouteConfig,
26
- LifecycleHooks
27
- } from './types';
28
- import { AdapterRegistry } from './adapter';
29
- import { PluginManager, NexusPlugin, SimplePlugin } from './plugin';
30
- import { FileRouter, FileRouterOptions } from './router/file-router';
31
- import { createObservabilityMiddleware } from '../advanced/observability/createObservabilityMiddleware';
32
- import { ObservabilityCenter } from '../advanced/observability/ObservabilityCenter';
33
- import {
34
- GracefulShutdownManager,
35
- GracefulShutdownOptions,
36
- ClusterManager,
37
- ClusterOptions
38
- } from '../deployment';
39
- import {
40
- WebSocketGateway,
41
- WebSocketRouteConfig
42
- } from '../advanced/realtime/websocket';
43
- import { ObservabilityOptions } from '../advanced/observability/types';
44
- import { StoreRegistry, StoreConstructor, ContextStore } from './store';
45
-
46
- /**
47
- * Default error handler
48
- */
49
- const defaultErrorHandler: ErrorHandler = (error: Error, ctx: Context): Response => {
50
- // Only log unexpected errors, not intentional ones (returned from handler)
51
- if (!(error as any)._isIntentional) {
52
- console.error('Unhandled error:', error);
53
- console.error('Path:', ctx.path, 'Method:', ctx.method);
54
- }
55
-
56
- return {
57
- statusCode: 500,
58
- headers: { 'Content-Type': 'application/json' },
59
- body: JSON.stringify({
60
- error: error.message || 'Internal Server Error',
61
- stack: process.env.NODE_ENV === 'development' && !(error as any)._isIntentional ? error.stack : undefined
62
- })
63
- };
64
- };
65
-
66
- /**
67
- * Application class
68
- */
69
- export class Application<TDeps extends DependencyContainer = {}> {
70
- private router: Router;
71
- private contextPool: ContextPool;
72
- private middlewareExecutor: MiddlewareExecutor;
73
- private globalMiddlewares: Middleware[] = [];
74
- private errorHandler: ErrorHandler;
75
- private server?: HTTPServer;
76
- private config: AppConfig;
77
- private adapters: AdapterRegistry;
78
- private shutdownManager?: GracefulShutdownManager;
79
- private clusterManager?: ClusterManager;
80
- private fallbackHandler?: Handler;
81
- private wsGateway?: WebSocketGateway;
82
-
83
- // Dependency injection container
84
- private dependencies: TDeps = {} as TDeps;
85
-
86
- // Versioning properties
87
- private versioningConfig?: VersioningConfig;
88
- private registeredVersions: Set<string> = new Set();
89
-
90
- // Store registry for ContextStore system
91
- private storeRegistry: StoreRegistry;
92
-
93
- // Plugin manager for advanced plugins
94
- private pluginManager: PluginManager;
95
-
96
- // Lifecycle hooks
97
- private lifecycleHooks: LifecycleHooks = {};
98
-
99
- constructor(config: AppConfig = {}) {
100
- this.config = {
101
- contextPoolSize: 100,
102
- enableJIT: true,
103
- debug: false,
104
- logRequests: true,
105
- ...config
106
- };
107
-
108
- this.router = new Router();
109
- this.contextPool = new ContextPool(this.config.contextPoolSize);
110
- this.middlewareExecutor = new MiddlewareExecutor();
111
- this.errorHandler = config.onError || defaultErrorHandler;
112
- this.adapters = new AdapterRegistry();
113
- this.storeRegistry = new StoreRegistry({ debug: config.debug });
114
- this.pluginManager = new PluginManager(this, { debug: config.debug });
115
- }
116
-
117
- /**
118
- * Register lifecycle hooks for request processing
119
- * Hooks are called at specific points during request lifecycle
120
- *
121
- * @example
122
- * ```typescript
123
- * app.hooks({
124
- * onRequest: async (ctx) => {
125
- * ctx.requestId = crypto.randomUUID();
126
- * console.log(`[${ctx.requestId}] ${ctx.method} ${ctx.path}`);
127
- * },
128
- *
129
- * beforeValidation: async (ctx) => {
130
- * // Transform raw body before validation
131
- * if (ctx.body?.data) {
132
- * ctx.body = ctx.body.data;
133
- * }
134
- * },
135
- *
136
- * afterValidation: async (ctx) => {
137
- * // Check authorization after body is validated
138
- * if (!ctx.headers.authorization) {
139
- * return ctx.response.status(401).json({ error: 'Unauthorized' });
140
- * }
141
- * },
142
- *
143
- * beforeHandler: async (ctx) => {
144
- * // Load user from session
145
- * ctx.user = await getUserFromToken(ctx.headers.authorization);
146
- * },
147
- *
148
- * afterHandler: async (ctx, result) => {
149
- * // Add metadata to all responses
150
- * return { ...result, timestamp: Date.now(), requestId: ctx.requestId };
151
- * },
152
- *
153
- * onError: async (ctx, error) => {
154
- * // Log errors to external service
155
- * await logToSentry(error, { requestId: ctx.requestId });
156
- * },
157
- *
158
- * onResponse: async (ctx, response) => {
159
- * console.log(`[${ctx.requestId}] Response: ${response.statusCode}`);
160
- * }
161
- * });
162
- * ```
163
- */
164
- hooks(hooks: LifecycleHooks): this {
165
- this.lifecycleHooks = { ...this.lifecycleHooks, ...hooks };
166
- return this;
167
- }
168
-
169
- /**
170
- * Set a fallback handler for unmatched routes (e.g., static files)
171
- * Called before returning 404
172
- */
173
- setFallbackHandler(handler: Handler): this {
174
- this.fallbackHandler = handler;
175
- return this;
176
- }
177
-
178
- /**
179
- * Provide dependencies for injection into route handlers
180
- * Dependencies are available via the second parameter in handlers
181
- *
182
- * @example
183
- * ```typescript
184
- * const db = new Database();
185
- * const cache = new Redis();
186
- * const mailer = new Mailer();
187
- *
188
- * const app = createApp()
189
- * .provide({ db, cache, mailer });
190
- *
191
- * // Use with inject option
192
- * app.get('/users', {
193
- * inject: ['db', 'cache'],
194
- * handler: async (ctx, { db, cache }) => {
195
- * // db and cache are fully typed!
196
- * const users = await db.query('SELECT * FROM users');
197
- * await cache.set('users', users);
198
- * return { users };
199
- * }
200
- * });
201
- *
202
- * // Or access all dependencies
203
- * app.get('/mail', async (ctx, deps) => {
204
- * await deps.mailer.send({ to: 'user@example.com', subject: 'Hello' });
205
- * return { sent: true };
206
- * });
207
- * ```
208
- */
209
- provide<T extends DependencyContainer>(deps: T): Application<TDeps & T> {
210
- this.dependencies = { ...this.dependencies, ...deps } as TDeps & T;
211
- return this as unknown as Application<TDeps & T>;
212
- }
213
-
214
- /**
215
- * Get a specific dependency by name
216
- *
217
- * @example
218
- * ```typescript
219
- * const db = app.getDep('db');
220
- * ```
221
- */
222
- getDep<K extends keyof TDeps>(name: K): TDeps[K] {
223
- return this.dependencies[name];
224
- }
225
-
226
- /**
227
- * Get all registered dependencies
228
- */
229
- getDeps(): TDeps {
230
- return this.dependencies;
231
- }
232
-
233
- /**
234
- * Configure API versioning
235
- *
236
- * @example
237
- * ```typescript
238
- * app.configVersions({
239
- * strategies: ['header', 'query'],
240
- * header: 'api-version',
241
- * queryParam: 'v',
242
- * defaultVersion: 'v1',
243
- * register: ['v1', 'v2']
244
- * });
245
- *
246
- * // Route tanpa prefix → otomatis jadi /{defaultVersion}/login
247
- * app.post('/login', handler);
248
- *
249
- * // Route dengan prefix explicit
250
- * app.post('/v2/login', handler);
251
- * ```
252
- */
253
- configVersions(config: VersioningConfig): this {
254
- this.versioningConfig = {
255
- header: 'api-version',
256
- queryParam: 'v',
257
- ...config
258
- };
259
-
260
- // Register all versions
261
- config.register.forEach(v => this.registeredVersions.add(v));
262
-
263
- return this;
264
- }
265
-
266
- /**
267
- * Check if a path starts with a registered version prefix
268
- */
269
- private hasVersionPrefix(path: string): string | null {
270
- const segment = path.split('/').filter(Boolean)[0];
271
- if (segment && this.registeredVersions.has(segment)) {
272
- return segment;
273
- }
274
- return null;
275
- }
276
-
277
- /**
278
- * Resolve version from request context
279
- */
280
- private resolveVersion(ctx: Context): { version: string; basePath: string; source: VersioningStrategy | 'default' } {
281
- if (!this.versioningConfig) {
282
- return { version: '', basePath: ctx.path, source: 'default' };
283
- }
284
-
285
- const { strategies, header, queryParam, defaultVersion } = this.versioningConfig;
286
-
287
- // 1. Check path strategy: /v1/login
288
- if (strategies.includes('path')) {
289
- const versionPrefix = this.hasVersionPrefix(ctx.path);
290
- if (versionPrefix) {
291
- const basePath = ctx.path.replace(new RegExp(`^/${versionPrefix}`), '') || '/';
292
- return { version: versionPrefix, basePath, source: 'path' };
293
- }
294
- }
295
-
296
- // 2. Check header strategy
297
- if (strategies.includes('header') && header) {
298
- const headerValue = ctx.headers[header] || ctx.headers[header.toLowerCase()];
299
- const version = Array.isArray(headerValue) ? headerValue[0] : headerValue;
300
- if (version && this.registeredVersions.has(version)) {
301
- return { version, basePath: ctx.path, source: 'header' };
302
- }
303
- }
304
-
305
- // 3. Check query strategy
306
- if (strategies.includes('query') && queryParam) {
307
- const queryVersion = ctx.query?.[queryParam];
308
- const version = Array.isArray(queryVersion) ? queryVersion[0] : queryVersion;
309
- if (version && this.registeredVersions.has(version)) {
310
- return { version, basePath: ctx.path, source: 'query' };
311
- }
312
- }
313
-
314
- // 4. Default version
315
- return { version: defaultVersion, basePath: ctx.path, source: 'default' };
316
- }
317
-
318
- /**
319
- * Build versioned path for route registration
320
- */
321
- private buildVersionedPath(path: string): string {
322
- if (!this.versioningConfig) return path;
323
-
324
- // If path already has version prefix, return as-is
325
- if (this.hasVersionPrefix(path)) {
326
- return path;
327
- }
328
-
329
- // Add default version prefix
330
- const normalizedPath = path.startsWith('/') ? path : `/${path}`;
331
- return `/${this.versioningConfig.defaultVersion}${normalizedPath}`;
332
- }
333
-
334
- /**
335
- * Add global middleware or mount a router
336
- *
337
- * @example
338
- * ```typescript
339
- * // Add middleware
340
- * app.use(loggerMiddleware);
341
- * app.use(corsMiddleware);
342
- *
343
- * // Mount router
344
- * const routes = new Router();
345
- * routes.get('/users', getUsers);
346
- * app.use(routes);
347
- *
348
- * // Mount router with prefix
349
- * app.use('/api', routes);
350
- * ```
351
- */
352
- use(middlewareOrRouter: Middleware | Router): this;
353
- use(prefix: string, router: Router): this;
354
- use(middlewareOrPrefixOrRouter: Middleware | Router | string, router?: Router): this {
355
- // app.use('/api', router)
356
- if (typeof middlewareOrPrefixOrRouter === 'string' && router) {
357
- this.mountRouter(middlewareOrPrefixOrRouter, router);
358
- return this;
359
- }
360
-
361
- // app.use(router)
362
- if (middlewareOrPrefixOrRouter instanceof Router) {
363
- this.mountRouter('', middlewareOrPrefixOrRouter);
364
- return this;
365
- }
366
-
367
- // app.use(middleware)
368
- this.globalMiddlewares.push(middlewareOrPrefixOrRouter as Middleware);
369
- return this;
370
- }
371
-
372
- /**
373
- * Mount a router's routes into the application
374
- */
375
- private mountRouter(prefix: string, router: Router): void {
376
- const routes = router.getRawRoutes();
377
- for (const route of routes) {
378
- const fullPath = prefix ? `${prefix}${route.path}` : route.path;
379
- this.router.addRoute({
380
- method: route.method,
381
- path: fullPath,
382
- handler: route.config.handler,
383
- middlewares: route.config.middlewares,
384
- schema: route.config.schema,
385
- meta: route.config.meta
386
- });
387
- }
388
- }
389
-
390
- /**
391
- * Register ContextStore classes
392
- * Stores are singleton instances accessible via ctx.store(StoreClass)
393
- *
394
- * @example
395
- * ```typescript
396
- * class UserStore extends ContextStore<UserState> {
397
- * protected initial() { return { users: [], loading: false }; }
398
- *
399
- * async fetchUsers() {
400
- * this.update({ loading: true });
401
- * const users = await api.getUsers();
402
- * this.set({ users, loading: false });
403
- * }
404
- * }
405
- *
406
- * const app = createApp();
407
- * app.stores([UserStore, ProductStore]);
408
- *
409
- * app.get('/users', async (ctx) => {
410
- * const userStore = ctx.store(UserStore);
411
- * return { users: userStore.state.users };
412
- * });
413
- * ```
414
- */
415
- stores(storeClasses: StoreConstructor<any>[]): this {
416
- this.storeRegistry.registerAll(storeClasses);
417
- return this;
418
- }
419
-
420
- /**
421
- * Register a single ContextStore class
422
- *
423
- * @example
424
- * ```typescript
425
- * app.store(UserStore);
426
- * ```
427
- */
428
- store<T extends ContextStore<any>>(StoreClass: StoreConstructor<T>): this {
429
- this.storeRegistry.register(StoreClass);
430
- return this;
431
- }
432
-
433
- /**
434
- * Get a store instance directly from application level
435
- * Useful for accessing stores outside of request context
436
- *
437
- * @example
438
- * ```typescript
439
- * const userStore = app.getStore(UserStore);
440
- * userStore.listen((state) => console.log('Users updated:', state));
441
- * ```
442
- */
443
- getStore<T extends ContextStore<any>>(StoreClass: StoreConstructor<T>): T {
444
- return this.storeRegistry.get(StoreClass);
445
- }
446
-
447
- /**
448
- * Get the store registry for advanced usage
449
- */
450
- getStoreRegistry(): StoreRegistry {
451
- return this.storeRegistry;
452
- }
453
-
454
- /**
455
- * Register a route
456
- */
457
- route(config: RouteConfig): this {
458
- this.router.addRoute(config);
459
- return this;
460
- }
461
-
462
- /**
463
- * Convenience: GET route
464
- * Supports both function-style and class-based routing
465
- *
466
- * @example
467
- * ```typescript
468
- * // Function style (with dependencies)
469
- * app.get('/users', async (ctx, { db }) => ({ users: await db.getUsers() }));
470
- *
471
- * // Config style with inject
472
- * app.get('/users', {
473
- * inject: ['db', 'cache'],
474
- * handler: async (ctx, { db, cache }) => ({ users: [] }),
475
- * meta: {...}
476
- * });
477
- *
478
- * // Class-based style
479
- * app.get(new UserListRoute());
480
- * ```
481
- */
482
- get<K extends keyof TDeps = keyof TDeps>(
483
- pathOrRoute: string | Route,
484
- handlerOrConfig?: Handler<Context, TDeps> | InjectedRouteConfig<Context, TDeps, K>
485
- ): this {
486
- // Class-based routing
487
- if (typeof pathOrRoute === 'object' && ('pathName' in pathOrRoute || 'handler' in pathOrRoute)) {
488
- return this.registerClassRoute('GET', pathOrRoute as Route);
489
- }
490
-
491
- return this.registerVersionedRoute('GET', pathOrRoute as string, handlerOrConfig);
492
- }
493
-
494
- /**
495
- * Convenience: POST route
496
- * Supports both function-style and class-based routing
497
- */
498
- post<K extends keyof TDeps = keyof TDeps>(
499
- pathOrRoute: string | Route,
500
- handlerOrConfig?: Handler<Context, TDeps> | InjectedRouteConfig<Context, TDeps, K>
501
- ): this {
502
- // Class-based routing
503
- if (typeof pathOrRoute === 'object' && ('pathName' in pathOrRoute || 'handler' in pathOrRoute)) {
504
- return this.registerClassRoute('POST', pathOrRoute as Route);
505
- }
506
-
507
- return this.registerVersionedRoute('POST', pathOrRoute as string, handlerOrConfig);
508
- }
509
-
510
- /**
511
- * Convenience: PUT route
512
- * Supports both function-style and class-based routing
513
- */
514
- put<K extends keyof TDeps = keyof TDeps>(
515
- pathOrRoute: string | Route,
516
- handlerOrConfig?: Handler<Context, TDeps> | InjectedRouteConfig<Context, TDeps, K>
517
- ): this {
518
- // Class-based routing
519
- if (typeof pathOrRoute === 'object' && ('pathName' in pathOrRoute || 'handler' in pathOrRoute)) {
520
- return this.registerClassRoute('PUT', pathOrRoute as Route);
521
- }
522
-
523
- return this.registerVersionedRoute('PUT', pathOrRoute as string, handlerOrConfig);
524
- }
525
-
526
- /**
527
- * Convenience: DELETE route
528
- * Supports both function-style and class-based routing
529
- */
530
- delete<K extends keyof TDeps = keyof TDeps>(
531
- pathOrRoute: string | Route,
532
- handlerOrConfig?: Handler<Context, TDeps> | InjectedRouteConfig<Context, TDeps, K>
533
- ): this {
534
- // Class-based routing
535
- if (typeof pathOrRoute === 'object' && ('pathName' in pathOrRoute || 'handler' in pathOrRoute)) {
536
- return this.registerClassRoute('DELETE', pathOrRoute as Route);
537
- }
538
-
539
- return this.registerVersionedRoute('DELETE', pathOrRoute as string, handlerOrConfig);
540
- }
541
-
542
- /**
543
- * Convenience: PATCH route
544
- * Supports both function-style and class-based routing
545
- */
546
- patch<K extends keyof TDeps = keyof TDeps>(
547
- pathOrRoute: string | Route,
548
- handlerOrConfig?: Handler<Context, TDeps> | InjectedRouteConfig<Context, TDeps, K>
549
- ): this {
550
- // Class-based routing
551
- if (typeof pathOrRoute === 'object' && ('pathName' in pathOrRoute || 'handler' in pathOrRoute)) {
552
- return this.registerClassRoute('PATCH', pathOrRoute as Route);
553
- }
554
-
555
- return this.registerVersionedRoute('PATCH', pathOrRoute as string, handlerOrConfig);
556
- }
557
-
558
- /**
559
- * Register a versioned route (internal)
560
- */
561
- private registerVersionedRoute<K extends keyof TDeps>(
562
- method: HTTPMethod,
563
- path: string,
564
- handlerOrConfig?: Handler<Context, TDeps> | InjectedRouteConfig<Context, TDeps, K>
565
- ): this {
566
- // Build versioned path if versioning is configured
567
- const versionedPath = this.buildVersionedPath(path);
568
-
569
- if (typeof handlerOrConfig === 'function') {
570
- // Function handler - wrap to inject all dependencies
571
- const originalHandler = handlerOrConfig;
572
- const deps = this.dependencies;
573
- const wrappedHandler: Handler = async (ctx) => originalHandler(ctx, deps);
574
- this.router.addRoute({ method, path: versionedPath, handler: wrappedHandler });
575
- } else if (handlerOrConfig) {
576
- // Config object with possible inject option
577
- const config = handlerOrConfig as InjectedRouteConfig<Context, TDeps, K>;
578
- const originalHandler = config.handler;
579
- const allDeps = this.dependencies;
580
- const injectKeys = config.inject;
581
-
582
- // Create wrapped handler that injects dependencies
583
- const wrappedHandler: Handler = async (ctx) => {
584
- let injectedDeps: any;
585
- if (injectKeys && injectKeys.length > 0) {
586
- // Only inject specified dependencies
587
- injectedDeps = {} as Pick<TDeps, K>;
588
- for (const key of injectKeys) {
589
- (injectedDeps as any)[key] = allDeps[key];
590
- }
591
- } else {
592
- // Inject all dependencies
593
- injectedDeps = allDeps;
594
- }
595
- return originalHandler(ctx, injectedDeps);
596
- };
597
-
598
- this.router.addRoute({
599
- method,
600
- path: versionedPath,
601
- handler: wrappedHandler,
602
- middlewares: config.middlewares,
603
- schema: config.schema,
604
- meta: config.meta
605
- });
606
- }
607
- return this;
608
- }
609
-
610
- /**
611
- * Register a class-based route with lifecycle hooks support and dependency injection
612
- */
613
- private registerClassRoute(method: HTTPMethod, route: Route): this {
614
- // ⚠️ ENFORCE: Route class MUST extend Route abstract class
615
- if (!(route instanceof Route)) {
616
- const className = (route as any).constructor?.name || 'Unknown';
617
- throw new Error(
618
- `Route class "${className}" must extend the Route abstract class.\n` +
619
- `Example:\n` +
620
- ` import { Route } from 'nexus';\n` +
621
- ` class ${className} extends Route {\n` +
622
- ` pathName = '/your/path';\n` +
623
- ` async handler(ctx) { ... }\n` +
624
- ` }`
625
- );
626
- }
627
-
628
- if (!route.pathName) {
629
- throw new Error(
630
- `Route class must have a pathName property when using manual registration. ` +
631
- `Use app.useFileRoutes() for file-based routing without pathName.`
632
- );
633
- }
634
-
635
- // Determine the handler - either the handler method or the method-specific handler
636
- let originalHandler = route.handler;
637
- if (!originalHandler && typeof (route as any)[method] === 'function') {
638
- originalHandler = (route as any)[method].bind(route);
639
- }
640
-
641
- if (!originalHandler) {
642
- throw new Error(
643
- `Route class must have a handler method or a ${method} method.`
644
- );
645
- }
646
-
647
- // Wrap handler with lifecycle hooks if route extends Route class
648
- const routeInstance = route as Route;
649
- const hasHooks = typeof routeInstance.onBefore === 'function' ||
650
- typeof routeInstance.onAfter === 'function' ||
651
- typeof routeInstance.onError === 'function';
652
-
653
- // Capture dependencies for injection
654
- const deps = this.dependencies;
655
- let finalHandler: Handler;
656
-
657
- if (hasHooks) {
658
- const boundOriginalHandler = originalHandler.bind(route);
659
- const onBefore = routeInstance.onBefore?.bind(route);
660
- const onAfter = routeInstance.onAfter?.bind(route);
661
- const onError = routeInstance.onError?.bind(route);
662
-
663
- finalHandler = async (ctx: Context) => {
664
- try {
665
- // Run onBefore hook (with deps)
666
- if (onBefore) {
667
- const beforeResult = await onBefore(ctx, deps);
668
- // If onBefore returns a value (not undefined), skip handler
669
- if (beforeResult !== undefined) {
670
- return beforeResult;
671
- }
672
- }
673
-
674
- // Run the main handler with dependencies
675
- let result = await boundOriginalHandler(ctx, deps);
676
-
677
- // Run onAfter hook (with deps)
678
- if (onAfter) {
679
- result = await onAfter(ctx, result, deps);
680
- }
681
-
682
- return result;
683
- } catch (error) {
684
- // Run onError hook if defined (with deps)
685
- if (onError) {
686
- return await onError(ctx, error as Error, deps);
687
- }
688
- // Re-throw if no onError handler
689
- throw error;
690
- }
691
- };
692
- } else {
693
- // No hooks - just wrap with dependency injection
694
- const boundHandler = originalHandler.bind(route);
695
- finalHandler = async (ctx: Context) => boundHandler(ctx, deps);
696
- }
697
-
698
- const config: RouteConfig = {
699
- method,
700
- path: route.pathName,
701
- handler: finalHandler,
702
- schema: route.schema?.(),
703
- meta: route.meta?.(),
704
- middlewares: route.middlewares?.()
705
- };
706
- this.router.addRoute(config);
707
- return this;
708
- }
709
-
710
- /**
711
- * Register a WebSocket route
712
- *
713
- * @example
714
- * ```typescript
715
- * app.ws('/ws/chat', {
716
- * auth: async (ctx) => validateToken(ctx.query.token),
717
- * onConnect: async (socket, ctx) => {
718
- * console.log('User connected:', ctx.user);
719
- * },
720
- * onMessage: async (socket, message, ctx) => {
721
- * socket.send(JSON.stringify({ echo: message }));
722
- * },
723
- * onClose: async (socket, ctx) => {
724
- * console.log('User disconnected');
725
- * }
726
- * });
727
- * ```
728
- */
729
- ws(path: string, config: WebSocketRouteConfig): this {
730
- if (!this.wsGateway) {
731
- this.wsGateway = new WebSocketGateway();
732
- }
733
- this.wsGateway.register(path, config);
734
- return this;
735
- }
736
-
737
- /**
738
- * Get the WebSocket gateway for advanced usage (rooms, broadcast, etc.)
739
- *
740
- * @example
741
- * ```typescript
742
- * const ws = app.getWebSocket();
743
- * ws.broadcast('room-name', { type: 'notification', message: 'Hello!' });
744
- * ws.joinRoom('room-name', socket);
745
- * ```
746
- */
747
- getWebSocket(): WebSocketGateway | undefined {
748
- return this.wsGateway;
749
- }
750
-
751
- /**
752
- * Use file-based routing (Next.js style)
753
- *
754
- * Automatically scans a directory and registers routes based on file/folder structure.
755
- * Supports dynamic parameters [id], catch-all [...slug], and nested routes.
756
- *
757
- * @example
758
- * ```typescript
759
- * // Folder structure:
760
- * // routes/
761
- * // api/
762
- * // users/
763
- * // index.ts → GET/POST /api/users
764
- * // [id]/
765
- * // index.ts → GET/PUT/DELETE /api/users/:id
766
- * // posts.ts → GET /api/users/:id/posts
767
- *
768
- * const app = createApp();
769
- *
770
- * await app.useFileRoutes({
771
- * dir: './src/routes',
772
- * prefix: '',
773
- * debug: true
774
- * });
775
- *
776
- * app.listen(3000);
777
- * ```
778
- *
779
- * Route file format (function-style):
780
- * ```typescript
781
- * // routes/api/users/index.ts
782
- * import { Context } from '@engjts/nexus';
783
- *
784
- * export async function GET(ctx: Context) {
785
- * return ctx.json({ users: [] });
786
- * }
787
- *
788
- * export async function POST(ctx: Context) {
789
- * const body = ctx.body;
790
- * return ctx.json({ created: body }, 201);
791
- * }
792
- *
793
- * export const schema = { body: z.object({ name: z.string() }) };
794
- * export const meta = { summary: 'User endpoints', tags: ['Users'] };
795
- * ```
796
- *
797
- * Route file format (class-style):
798
- * ```typescript
799
- * // routes/api/users/[id]/index.ts
800
- * import { RouteBase, Context } from '@engjts/nexus';
801
- *
802
- * export default class UserRoute implements RouteBase {
803
- * method = ['GET', 'PUT', 'DELETE'] as const;
804
- *
805
- * async GET(ctx: Context) {
806
- * return ctx.json({ user: { id: ctx.params.id } });
807
- * }
808
- *
809
- * async PUT(ctx: Context) {
810
- * return ctx.json({ updated: true });
811
- * }
812
- *
813
- * async DELETE(ctx: Context) {
814
- * return ctx.json({ deleted: true });
815
- * }
816
- * }
817
- * ```
818
- */
819
- async useFileRoutes(options: FileRouterOptions): Promise<this> {
820
- const router = new FileRouter(options);
821
- await router.register(this);
822
- return this;
823
- }
824
-
825
- /**
826
- * Set custom error handler
827
- */
828
- onError(handler: ErrorHandler): this {
829
- this.errorHandler = handler;
830
- return this;
831
- }
832
-
833
- /**
834
- * Install a plugin (supports both legacy and new plugin formats)
835
- *
836
- * @example
837
- * ```typescript
838
- * // Legacy plugin
839
- * app.plugin({ name: 'my-plugin', version: '1.0.0', install: (app) => {} });
840
- *
841
- * // New advanced plugin
842
- * const authPlugin = definePlugin('auth')
843
- * .version('1.0.0')
844
- * .config<{ secret: string }>()
845
- * .register(ctx => ctx.app.use(jwtMiddleware))
846
- * .build();
847
- *
848
- * app.plugin(authPlugin, { secret: 'my-secret' });
849
- * ```
850
- */
851
- plugin<TConfig = any>(
852
- plugin: Plugin | NexusPlugin<TConfig> | SimplePlugin,
853
- config?: TConfig
854
- ): this {
855
- // Check if it's a legacy Plugin with install method directly on app
856
- if ('install' in plugin && !('meta' in plugin)) {
857
- // Legacy format - call install directly
858
- (plugin as Plugin).install(this);
859
- } else {
860
- // New plugin format - use PluginManager
861
- this.pluginManager.add(plugin as NexusPlugin<TConfig>, config);
862
- }
863
- return this;
864
- }
865
-
866
- /**
867
- * Get the plugin manager for advanced plugin operations
868
- *
869
- * @example
870
- * ```typescript
871
- * const pm = app.getPluginManager();
872
- *
873
- * // Check if plugin exists
874
- * if (pm.has('auth-plugin')) {
875
- * const authApi = pm.getExports<AuthAPI>('auth-plugin');
876
- * authApi.verify(token);
877
- * }
878
- *
879
- * // Listen to plugin events
880
- * pm.on('plugin:ready', (meta) => {
881
- * console.log(`Plugin ${meta.name} is ready`);
882
- * });
883
- * ```
884
- */
885
- getPluginManager(): PluginManager {
886
- return this.pluginManager;
887
- }
888
-
889
- /**
890
- * Get exports from a specific plugin
891
- * Shorthand for getPluginManager().getExports()
892
- */
893
- getPluginExports<T = any>(pluginName: string): T | undefined {
894
- return this.pluginManager.getExports<T>(pluginName);
895
- }
896
-
897
- /**
898
- * Check if a plugin is installed
899
- */
900
- hasPlugin(name: string): boolean {
901
- return this.pluginManager.has(name);
902
- }
903
-
904
- /**
905
- * Register an adapter
906
- */
907
- adapter<T>(name: string, adapter: T): this {
908
- this.adapters.register(name, adapter);
909
- return this;
910
- }
911
-
912
- /**
913
- * Enable built-in observability (metrics, tracing, health checks)
914
- * with safe, configurable endpoints.
915
- */
916
- observe(options: ObservabilityOptions = {}): this {
917
- const center = new ObservabilityCenter(options);
918
-
919
- // Attach middleware for metrics/tracing/logging
920
- this.use(createObservabilityMiddleware(center, options));
921
-
922
- const existingRoutes = this.router.getRoutes();
923
-
924
- // Metrics endpoint (default: /__nexus/metrics)
925
- const metricsEnabled = options.metrics?.enabled ?? true;
926
- if (metricsEnabled) {
927
- const metricsPath = options.metrics?.endpoint ?? '/__nexus/metrics';
928
- this.ensureNoRouteConflict('GET', metricsPath, existingRoutes);
929
- this.get(metricsPath, async (ctx) => center.metricsHandler()(ctx));
930
- }
931
-
932
- // Health endpoint (default: /__nexus/health)
933
- if (options.health) {
934
- const healthPath = options.health.endpoint ?? '/__nexus/health';
935
- this.ensureNoRouteConflict('GET', healthPath, existingRoutes);
936
- this.get(healthPath, async (ctx) => center.healthHandler()(ctx));
937
- }
938
-
939
- return this;
940
- }
941
-
942
- private ensureNoRouteConflict(method: HTTPMethod, path: string, routes: Array<{ method: string; path: string }>) {
943
- const conflict = routes.some(r => r.method === method && r.path === path);
944
- if (conflict) {
945
- throw new Error(
946
- `Observability endpoint conflict for ${method} ${path}. ` +
947
- 'Silakan override endpoint melalui konfigurasi observability (metrics.endpoint / health.endpoint).'
948
- );
949
- }
950
- }
951
-
952
- /**
953
- * Get an adapter
954
- */
955
- getAdapter<T>(name: string): T | undefined {
956
- return this.adapters.get<T>(name);
957
- }
958
-
959
- /**
960
- * Initialize all plugins (must be called before listen/start)
961
- * This runs: configure → register → boot lifecycle phases
962
- *
963
- * @example
964
- * ```typescript
965
- * const app = createApp()
966
- * .plugin(authPlugin, { secret: 'xxx' })
967
- * .plugin(dbPlugin, { url: 'postgres://...' });
968
- *
969
- * // Initialize all plugins
970
- * await app.initialize();
971
- *
972
- * // Start the server
973
- * app.listen(3000);
974
- * ```
975
- */
976
- async initialize(): Promise<this> {
977
- await this.pluginManager.initialize();
978
- return this;
979
- }
980
-
981
- /**
982
- * Start the HTTP server
983
- */
984
- listen(port: number | string, callback?: () => void): HTTPServer {
985
- const portNumber = typeof port === 'string' ? parseInt(port, 10) : port;
986
-
987
- this.server = createServer(async (req, res) => {
988
- await this.handleRequest(req, res);
989
- });
990
-
991
- // Attach graceful shutdown if enabled
992
- if (this.shutdownManager) {
993
- this.shutdownManager.attach(this.server);
994
- }
995
-
996
- // Attach WebSocket gateway if any ws routes registered
997
- if (this.wsGateway) {
998
- this.wsGateway.attach(this.server);
999
- }
1000
-
1001
- this.server.listen(portNumber, () => {
1002
- // Notify plugins that server is ready
1003
- this.pluginManager.notifyReady().catch(err => {
1004
- console.error('[PluginManager] Error in ready phase:', err);
1005
- });
1006
- callback?.();
1007
- });
1008
- return this.server;
1009
- }
1010
-
1011
- /**
1012
- * Start the server (alias for listen with modern options)
1013
- */
1014
- start(options: { port: number | string; host?: string; callback?: () => void } | number | string): HTTPServer {
1015
- if (typeof options === 'number' || typeof options === 'string') {
1016
- return this.listen(options);
1017
- }
1018
-
1019
- const portNumber = typeof options.port === 'string' ? parseInt(options.port, 10) : options.port;
1020
-
1021
- this.server = createServer(async (req, res) => {
1022
- await this.handleRequest(req, res);
1023
- });
1024
-
1025
- // Attach graceful shutdown if enabled
1026
- if (this.shutdownManager) {
1027
- this.shutdownManager.attach(this.server);
1028
- }
1029
-
1030
- // Attach WebSocket gateway if any ws routes registered
1031
- if (this.wsGateway) {
1032
- this.wsGateway.attach(this.server);
1033
- }
1034
-
1035
- const { host = '0.0.0.0', callback } = options;
1036
- this.server.listen(portNumber, host, () => {
1037
- // Notify plugins that server is ready
1038
- this.pluginManager.notifyReady().catch(err => {
1039
- console.error('[PluginManager] Error in ready phase:', err);
1040
- });
1041
- callback?.();
1042
- });
1043
- return this.server;
1044
- }
1045
-
1046
- /**
1047
- * Handle incoming HTTP request
1048
- * Optimized hot path for maximum performance
1049
- */
1050
- private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
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
-
1055
- try {
1056
- // Direct method calls - ContextImpl always has these methods
1057
- ctx.setStoreRegistry(this.storeRegistry);
1058
- ctx.setDebugMode(this.config.debug ?? false);
1059
-
1060
- // === HOOK: onRequest (skip check if no hooks) ===
1061
- if (this.lifecycleHooks.onRequest) {
1062
- const hookResult = await this.lifecycleHooks.onRequest(ctx);
1063
- if (hookResult && typeof hookResult === 'object' && 'statusCode' in hookResult) {
1064
- await this.sendResponse(res, hookResult, ctx);
1065
- this.cleanupRequest(ctx);
1066
- return;
1067
- }
1068
- }
1069
-
1070
- // Fast path: no versioning configured (most common)
1071
- let matchPath = ctx.path;
1072
- if (this.versioningConfig) {
1073
- const { version, basePath, source } = this.resolveVersion(ctx);
1074
- ctx.version = version;
1075
- ctx.versionSource = source;
1076
-
1077
- if (source !== 'path') {
1078
- matchPath = `/${version}${basePath.startsWith('/') ? basePath : '/' + basePath}`;
1079
- }
1080
- }
1081
-
1082
- // Find matching route
1083
- const match = this.router.match(ctx.method, matchPath);
1084
-
1085
- if (!match) {
1086
- // Try fallback handler
1087
- if (this.fallbackHandler) {
1088
- const fallbackResponse = await this.fallbackHandler(ctx, this.dependencies);
1089
- await this.sendResponse(res, fallbackResponse, ctx);
1090
- this.cleanupRequest(ctx);
1091
- return;
1092
- }
1093
-
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);
1098
- return;
1099
- }
1100
-
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);
1107
- }
1108
-
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
-
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
- }
1145
-
1146
- // === HOOK: onResponse ===
1147
- if (this.lifecycleHooks.onResponse) {
1148
- const hookResult = await this.lifecycleHooks.onResponse(ctx, response);
1149
- if (hookResult && typeof hookResult === 'object' && 'statusCode' in hookResult) {
1150
- response = hookResult;
1151
- }
1152
- }
1153
-
1154
- // Send response
1155
- await this.sendResponse(res, response, ctx);
1156
- this.cleanupRequest(ctx);
1157
-
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 {
1217
- // === HOOK: onError ===
1218
- if (ctx && this.lifecycleHooks.onError) {
1219
- try {
1220
- const hookResult = await this.lifecycleHooks.onError(ctx, error);
1221
- if (hookResult && typeof hookResult === 'object' && 'statusCode' in hookResult) {
1222
- await this.sendResponse(res, hookResult, ctx);
1223
- this.cleanupRequest(ctx);
1224
- return;
1225
- }
1226
- } catch (hookError) {
1227
- console.error('onError hook failed:', hookError);
1228
- }
1229
- }
1230
-
1231
- // Handle errors with default error handler
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;
1241
- }
1242
- }
1243
-
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);
1253
- }
1254
- }
1255
-
1256
- /**
1257
- * Check if value is a Response object
1258
- */
1259
- private isResponse(value: any): value is Response {
1260
- return (
1261
- value &&
1262
- typeof value === 'object' &&
1263
- 'statusCode' in value &&
1264
- 'headers' in value &&
1265
- 'body' in value
1266
- );
1267
- }
1268
-
1269
- /**
1270
- * Send HTTP response
1271
- * Optimized for minimal overhead - key for benchmark performance
1272
- */
1273
- private async sendResponse(res: ServerResponse, response: Response, ctx: Context): Promise<void> {
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
- }
1287
-
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];
1330
- if (value !== undefined) {
1331
- res.setHeader(key, value);
1332
- }
1333
- }
1334
-
1335
- // Set cookies
1336
- if ('getSetCookieHeaders' in ctx && typeof ctx.getSetCookieHeaders === 'function') {
1337
- const setCookies = ctx.getSetCookieHeaders();
1338
- if (setCookies.length > 0) {
1339
- res.setHeader('Set-Cookie', setCookies);
1340
- }
1341
- }
1342
-
1343
- // Set Content-Length for non-stream responses
1344
- if (body && typeof body === 'string') {
1345
- res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'));
1346
- }
1347
-
1348
- res.end(body);
1349
- }
1350
-
1351
- /**
1352
- * Get all registered routes
1353
- */
1354
- getRoutes() {
1355
- return this.router.getRoutes();
1356
- }
1357
-
1358
- /**
1359
- * Get context pool statistics
1360
- */
1361
- getPoolStats() {
1362
- return this.contextPool.getStats();
1363
- }
1364
-
1365
- /**
1366
- * Close the server
1367
- */
1368
- close(callback?: (err?: Error) => void): void {
1369
- if (this.server) {
1370
- this.server.close(callback);
1371
- }
1372
- }
1373
-
1374
- /**
1375
- * Enable graceful shutdown for zero-downtime deployments
1376
- */
1377
- gracefulShutdown(options: GracefulShutdownOptions = {}): this {
1378
- this.shutdownManager = new GracefulShutdownManager({
1379
- verbose: this.config.debug,
1380
- ...options
1381
- });
1382
- return this;
1383
- }
1384
-
1385
- /**
1386
- * Add a shutdown hook (e.g., closing database connections)
1387
- */
1388
- onShutdown(name: string, handler: () => Promise<void>, priority?: number): this {
1389
- if (!this.shutdownManager) {
1390
- this.gracefulShutdown();
1391
- }
1392
- this.shutdownManager!.addHook({ name, handler, priority });
1393
- return this;
1394
- }
1395
-
1396
- /**
1397
- * Initiate graceful shutdown programmatically
1398
- */
1399
- async shutdown(): Promise<void> {
1400
- // Shutdown all plugins first
1401
- await this.pluginManager.shutdown();
1402
-
1403
- // Dispose all stores
1404
- this.storeRegistry.dispose();
1405
-
1406
- if (this.shutdownManager) {
1407
- await this.shutdownManager.shutdown();
1408
- } else {
1409
- this.close();
1410
- }
1411
- }
1412
-
1413
- /**
1414
- * Check if shutdown is in progress
1415
- */
1416
- isShuttingDown(): boolean {
1417
- return this.shutdownManager?.isInShutdown() ?? false;
1418
- }
1419
-
1420
- /**
1421
- * Get the shutdown manager for advanced usage
1422
- */
1423
- getShutdownManager(): GracefulShutdownManager | undefined {
1424
- return this.shutdownManager;
1425
- }
1426
-
1427
- /**
1428
- * Start the application with clustering support
1429
- * @param options Cluster options
1430
- * @param startFn Function to start the server (called in each worker)
1431
- */
1432
- cluster(options: ClusterOptions = {}): ClusterManager {
1433
- this.clusterManager = new ClusterManager({
1434
- verbose: this.config.debug,
1435
- ...options
1436
- });
1437
- return this.clusterManager;
1438
- }
1439
-
1440
- /**
1441
- * Get the cluster manager
1442
- */
1443
- getClusterManager(): ClusterManager | undefined {
1444
- return this.clusterManager;
1445
- }
1446
- }
1447
-
1448
- /**
1449
- * Factory function to create an application
1450
- */
1451
- export function createApp(config?: AppConfig): Application {
1452
- return new Application(config);
1453
- }