@engjts/nexus 0.1.7 → 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 (259) hide show
  1. package/dist/advanced/playground/generatePlaygroundHTML.d.ts.map +1 -1
  2. package/dist/advanced/playground/generatePlaygroundHTML.js +107 -0
  3. package/dist/advanced/playground/generatePlaygroundHTML.js.map +1 -1
  4. package/dist/advanced/playground/playground.d.ts +19 -0
  5. package/dist/advanced/playground/playground.d.ts.map +1 -1
  6. package/dist/advanced/playground/playground.js +70 -0
  7. package/dist/advanced/playground/playground.js.map +1 -1
  8. package/dist/advanced/playground/types.d.ts +20 -0
  9. package/dist/advanced/playground/types.d.ts.map +1 -1
  10. package/dist/core/application.d.ts +14 -0
  11. package/dist/core/application.d.ts.map +1 -1
  12. package/dist/core/application.js +173 -71
  13. package/dist/core/application.js.map +1 -1
  14. package/dist/core/context-pool.d.ts +2 -13
  15. package/dist/core/context-pool.d.ts.map +1 -1
  16. package/dist/core/context-pool.js +7 -45
  17. package/dist/core/context-pool.js.map +1 -1
  18. package/dist/core/context.d.ts +108 -5
  19. package/dist/core/context.d.ts.map +1 -1
  20. package/dist/core/context.js +449 -53
  21. package/dist/core/context.js.map +1 -1
  22. package/dist/core/index.d.ts +1 -0
  23. package/dist/core/index.d.ts.map +1 -1
  24. package/dist/core/index.js +9 -1
  25. package/dist/core/index.js.map +1 -1
  26. package/dist/core/middleware.d.ts +6 -0
  27. package/dist/core/middleware.d.ts.map +1 -1
  28. package/dist/core/middleware.js +83 -84
  29. package/dist/core/middleware.js.map +1 -1
  30. package/dist/core/performance/fast-json.d.ts +149 -0
  31. package/dist/core/performance/fast-json.d.ts.map +1 -0
  32. package/dist/core/performance/fast-json.js +473 -0
  33. package/dist/core/performance/fast-json.js.map +1 -0
  34. package/dist/core/router/file-router.d.ts +20 -7
  35. package/dist/core/router/file-router.d.ts.map +1 -1
  36. package/dist/core/router/file-router.js +41 -13
  37. package/dist/core/router/file-router.js.map +1 -1
  38. package/dist/core/router/index.d.ts +6 -0
  39. package/dist/core/router/index.d.ts.map +1 -1
  40. package/dist/core/router/index.js +33 -6
  41. package/dist/core/router/index.js.map +1 -1
  42. package/dist/core/router/radix-tree.d.ts +4 -1
  43. package/dist/core/router/radix-tree.d.ts.map +1 -1
  44. package/dist/core/router/radix-tree.js +7 -3
  45. package/dist/core/router/radix-tree.js.map +1 -1
  46. package/dist/core/serializer.d.ts +251 -0
  47. package/dist/core/serializer.d.ts.map +1 -0
  48. package/dist/core/serializer.js +290 -0
  49. package/dist/core/serializer.js.map +1 -0
  50. package/dist/core/types.d.ts +39 -1
  51. package/dist/core/types.d.ts.map +1 -1
  52. package/dist/core/types.js.map +1 -1
  53. package/dist/index.d.ts +1 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +12 -2
  56. package/dist/index.js.map +1 -1
  57. package/package.json +3 -1
  58. package/documentation/01-getting-started.md +0 -240
  59. package/documentation/02-context.md +0 -335
  60. package/documentation/03-routing.md +0 -397
  61. package/documentation/04-middleware.md +0 -483
  62. package/documentation/05-validation.md +0 -514
  63. package/documentation/06-error-handling.md +0 -465
  64. package/documentation/07-performance.md +0 -364
  65. package/documentation/08-adapters.md +0 -470
  66. package/documentation/09-api-reference.md +0 -548
  67. package/documentation/10-examples.md +0 -582
  68. package/documentation/11-deployment.md +0 -477
  69. package/documentation/12-sentry.md +0 -620
  70. package/documentation/13-sentry-data-storage.md +0 -996
  71. package/documentation/14-sentry-data-reference.md +0 -457
  72. package/documentation/15-sentry-summary.md +0 -409
  73. package/documentation/16-alerts-system.md +0 -745
  74. package/documentation/17-alert-adapters.md +0 -696
  75. package/documentation/18-alerts-implementation-summary.md +0 -385
  76. package/documentation/19-class-based-routing.md +0 -840
  77. package/documentation/20-websocket-realtime.md +0 -813
  78. package/documentation/21-cache-system.md +0 -510
  79. package/documentation/22-job-queue.md +0 -772
  80. package/documentation/23-sentry-plugin.md +0 -551
  81. package/documentation/24-testing-utilities.md +0 -1287
  82. package/documentation/25-api-versioning.md +0 -533
  83. package/documentation/26-context-store.md +0 -607
  84. package/documentation/27-dependency-injection.md +0 -329
  85. package/documentation/28-lifecycle-hooks.md +0 -521
  86. package/documentation/29-package-structure.md +0 -196
  87. package/documentation/30-plugin-system.md +0 -414
  88. package/documentation/31-jwt-authentication.md +0 -597
  89. package/documentation/32-cli.md +0 -268
  90. package/documentation/ALERTS-COMPLETE-SUMMARY.md +0 -429
  91. package/documentation/ALERTS-INDEX.md +0 -330
  92. package/documentation/ALERTS-QUICK-REFERENCE.md +0 -286
  93. package/documentation/README.md +0 -178
  94. package/documentation/index.html +0 -34
  95. package/modern_framework_paper.md +0 -1870
  96. package/public/css/style.css +0 -87
  97. package/public/index.html +0 -34
  98. package/public/js/app.js +0 -27
  99. package/src/advanced/cache/InMemoryCacheStore.ts +0 -68
  100. package/src/advanced/cache/MultiTierCache.ts +0 -194
  101. package/src/advanced/cache/RedisCacheStore.ts +0 -341
  102. package/src/advanced/cache/index.ts +0 -5
  103. package/src/advanced/cache/types.ts +0 -40
  104. package/src/advanced/graphql/SimpleDataLoader.ts +0 -42
  105. package/src/advanced/graphql/index.ts +0 -22
  106. package/src/advanced/graphql/server.ts +0 -252
  107. package/src/advanced/graphql/types.ts +0 -42
  108. package/src/advanced/jobs/InMemoryQueueStore.ts +0 -68
  109. package/src/advanced/jobs/JobQueue.ts +0 -556
  110. package/src/advanced/jobs/RedisQueueStore.ts +0 -367
  111. package/src/advanced/jobs/index.ts +0 -5
  112. package/src/advanced/jobs/types.ts +0 -70
  113. package/src/advanced/observability/APMManager.ts +0 -163
  114. package/src/advanced/observability/AlertManager.ts +0 -109
  115. package/src/advanced/observability/MetricRegistry.ts +0 -151
  116. package/src/advanced/observability/ObservabilityCenter.ts +0 -304
  117. package/src/advanced/observability/StructuredLogger.ts +0 -154
  118. package/src/advanced/observability/TracingManager.ts +0 -117
  119. package/src/advanced/observability/adapters.ts +0 -304
  120. package/src/advanced/observability/createObservabilityMiddleware.ts +0 -63
  121. package/src/advanced/observability/index.ts +0 -11
  122. package/src/advanced/observability/types.ts +0 -174
  123. package/src/advanced/playground/extractPathParams.ts +0 -6
  124. package/src/advanced/playground/generateFieldExample.ts +0 -31
  125. package/src/advanced/playground/generatePlaygroundHTML.ts +0 -1849
  126. package/src/advanced/playground/generateSummary.ts +0 -19
  127. package/src/advanced/playground/getTagFromPath.ts +0 -9
  128. package/src/advanced/playground/index.ts +0 -8
  129. package/src/advanced/playground/playground.ts +0 -170
  130. package/src/advanced/playground/types.ts +0 -20
  131. package/src/advanced/playground/zodToExample.ts +0 -16
  132. package/src/advanced/playground/zodToParams.ts +0 -15
  133. package/src/advanced/postman/buildAuth.ts +0 -31
  134. package/src/advanced/postman/buildBody.ts +0 -15
  135. package/src/advanced/postman/buildQueryParams.ts +0 -27
  136. package/src/advanced/postman/buildRequestItem.ts +0 -36
  137. package/src/advanced/postman/buildResponses.ts +0 -11
  138. package/src/advanced/postman/buildUrl.ts +0 -33
  139. package/src/advanced/postman/capitalize.ts +0 -4
  140. package/src/advanced/postman/generateCollection.ts +0 -59
  141. package/src/advanced/postman/generateEnvironment.ts +0 -34
  142. package/src/advanced/postman/generateExampleFromZod.ts +0 -21
  143. package/src/advanced/postman/generateFieldExample.ts +0 -45
  144. package/src/advanced/postman/generateName.ts +0 -20
  145. package/src/advanced/postman/generateUUID.ts +0 -11
  146. package/src/advanced/postman/getTagFromPath.ts +0 -10
  147. package/src/advanced/postman/index.ts +0 -28
  148. package/src/advanced/postman/postman.ts +0 -156
  149. package/src/advanced/postman/slugify.ts +0 -7
  150. package/src/advanced/postman/types.ts +0 -140
  151. package/src/advanced/realtime/index.ts +0 -18
  152. package/src/advanced/realtime/websocket.ts +0 -231
  153. package/src/advanced/sentry/index.ts +0 -1236
  154. package/src/advanced/sentry/types.ts +0 -355
  155. package/src/advanced/static/generateDirectoryListing.ts +0 -47
  156. package/src/advanced/static/generateETag.ts +0 -7
  157. package/src/advanced/static/getMimeType.ts +0 -9
  158. package/src/advanced/static/index.ts +0 -32
  159. package/src/advanced/static/isSafePath.ts +0 -13
  160. package/src/advanced/static/publicDir.ts +0 -21
  161. package/src/advanced/static/serveStatic.ts +0 -225
  162. package/src/advanced/static/spa.ts +0 -24
  163. package/src/advanced/static/types.ts +0 -159
  164. package/src/advanced/swagger/SwaggerGenerator.ts +0 -66
  165. package/src/advanced/swagger/buildOperation.ts +0 -61
  166. package/src/advanced/swagger/buildParameters.ts +0 -61
  167. package/src/advanced/swagger/buildRequestBody.ts +0 -21
  168. package/src/advanced/swagger/buildResponses.ts +0 -54
  169. package/src/advanced/swagger/capitalize.ts +0 -5
  170. package/src/advanced/swagger/convertPath.ts +0 -9
  171. package/src/advanced/swagger/createSwagger.ts +0 -12
  172. package/src/advanced/swagger/generateOperationId.ts +0 -21
  173. package/src/advanced/swagger/generateSpec.ts +0 -105
  174. package/src/advanced/swagger/generateSummary.ts +0 -24
  175. package/src/advanced/swagger/generateSwaggerUI.ts +0 -70
  176. package/src/advanced/swagger/generateThemeCss.ts +0 -53
  177. package/src/advanced/swagger/index.ts +0 -25
  178. package/src/advanced/swagger/swagger.ts +0 -237
  179. package/src/advanced/swagger/types.ts +0 -206
  180. package/src/advanced/swagger/zodFieldToOpenAPI.ts +0 -94
  181. package/src/advanced/swagger/zodSchemaToOpenAPI.ts +0 -50
  182. package/src/advanced/swagger/zodToOpenAPI.ts +0 -22
  183. package/src/advanced/testing/factory.ts +0 -509
  184. package/src/advanced/testing/harness.ts +0 -612
  185. package/src/advanced/testing/index.ts +0 -430
  186. package/src/advanced/testing/load-test.ts +0 -618
  187. package/src/advanced/testing/mock-server.ts +0 -498
  188. package/src/advanced/testing/mock.ts +0 -670
  189. package/src/cli/bin.ts +0 -9
  190. package/src/cli/cli.ts +0 -158
  191. package/src/cli/commands/add.ts +0 -178
  192. package/src/cli/commands/build.ts +0 -73
  193. package/src/cli/commands/create.ts +0 -166
  194. package/src/cli/commands/dev.ts +0 -85
  195. package/src/cli/commands/generate.ts +0 -99
  196. package/src/cli/commands/help.ts +0 -95
  197. package/src/cli/commands/init.ts +0 -91
  198. package/src/cli/commands/version.ts +0 -38
  199. package/src/cli/index.ts +0 -6
  200. package/src/cli/templates/generators.ts +0 -359
  201. package/src/cli/templates/index.ts +0 -680
  202. package/src/cli/utils/exec.ts +0 -52
  203. package/src/cli/utils/file-system.ts +0 -78
  204. package/src/cli/utils/logger.ts +0 -111
  205. package/src/core/adapter.ts +0 -88
  206. package/src/core/application.ts +0 -1335
  207. package/src/core/context-pool.ts +0 -127
  208. package/src/core/context.ts +0 -412
  209. package/src/core/index.ts +0 -80
  210. package/src/core/middleware.ts +0 -262
  211. package/src/core/performance/buffer-pool.ts +0 -108
  212. package/src/core/performance/middleware-optimizer.ts +0 -162
  213. package/src/core/plugin/PluginManager.ts +0 -435
  214. package/src/core/plugin/builder.ts +0 -358
  215. package/src/core/plugin/index.ts +0 -50
  216. package/src/core/plugin/types.ts +0 -214
  217. package/src/core/router/file-router.ts +0 -594
  218. package/src/core/router/index.ts +0 -227
  219. package/src/core/router/radix-tree.ts +0 -226
  220. package/src/core/store/index.ts +0 -30
  221. package/src/core/store/registry.ts +0 -178
  222. package/src/core/store/request-store.ts +0 -240
  223. package/src/core/store/types.ts +0 -233
  224. package/src/core/types.ts +0 -574
  225. package/src/database/adapter.ts +0 -35
  226. package/src/database/adapters/index.ts +0 -1
  227. package/src/database/adapters/mysql.ts +0 -669
  228. package/src/database/database.ts +0 -70
  229. package/src/database/dialect.ts +0 -388
  230. package/src/database/index.ts +0 -12
  231. package/src/database/migrations.ts +0 -86
  232. package/src/database/optimizer.ts +0 -125
  233. package/src/database/query-builder.ts +0 -404
  234. package/src/database/realtime.ts +0 -53
  235. package/src/database/schema.ts +0 -71
  236. package/src/database/transactions.ts +0 -56
  237. package/src/database/types.ts +0 -87
  238. package/src/deployment/cluster.ts +0 -471
  239. package/src/deployment/config.ts +0 -454
  240. package/src/deployment/docker.ts +0 -599
  241. package/src/deployment/graceful-shutdown.ts +0 -373
  242. package/src/deployment/index.ts +0 -56
  243. package/src/index.ts +0 -264
  244. package/src/security/adapter.ts +0 -318
  245. package/src/security/auth/JWTPlugin.ts +0 -234
  246. package/src/security/auth/JWTProvider.ts +0 -316
  247. package/src/security/auth/adapter.ts +0 -12
  248. package/src/security/auth/jwt.ts +0 -234
  249. package/src/security/auth/middleware.ts +0 -188
  250. package/src/security/csrf.ts +0 -220
  251. package/src/security/headers.ts +0 -108
  252. package/src/security/index.ts +0 -60
  253. package/src/security/rate-limit/adapter.ts +0 -7
  254. package/src/security/rate-limit/memory.ts +0 -108
  255. package/src/security/rate-limit/middleware.ts +0 -181
  256. package/src/security/sanitization.ts +0 -75
  257. package/src/security/types.ts +0 -240
  258. package/src/security/utils.ts +0 -52
  259. package/tsconfig.json +0 -39
@@ -1,1335 +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
- */
1049
- private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
1050
- let ctx: Context | null = null;
1051
- let response: Response | null = null;
1052
-
1053
- 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
- }
1066
-
1067
- // === HOOK: onRequest ===
1068
- if (this.lifecycleHooks.onRequest) {
1069
- const hookResult = await this.lifecycleHooks.onRequest(ctx);
1070
- if (this.isResponse(hookResult)) {
1071
- await this.sendResponse(res, hookResult, ctx);
1072
- return;
1073
- }
1074
- }
1075
-
1076
- // Apply versioning if configured
1077
- let matchPath = ctx.path;
1078
- if (this.versioningConfig) {
1079
- const { version, basePath, source } = this.resolveVersion(ctx);
1080
- ctx.version = version;
1081
- ctx.versionSource = source;
1082
-
1083
- // For header/query strategy, rewrite path to versioned path
1084
- if (source === 'header' || source === 'query' || source === 'default') {
1085
- matchPath = `/${version}${basePath.startsWith('/') ? basePath : '/' + basePath}`;
1086
- }
1087
-
1088
- if (this.config.debug) {
1089
- console.log(`[Versioning] ${source} → ${version}, path: ${ctx.path} → ${matchPath}`);
1090
- }
1091
- }
1092
-
1093
- // Find matching route
1094
- const match = this.router.match(ctx.method, matchPath);
1095
-
1096
- if (!match) {
1097
- // Try fallback handler (e.g., static files)
1098
- if (this.fallbackHandler) {
1099
- const fallbackResponse = await this.fallbackHandler(ctx, this.dependencies);
1100
- await this.sendResponse(res, fallbackResponse, ctx);
1101
- return;
1102
- }
1103
-
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);
1110
- return;
1111
- }
1112
-
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;
1118
- }
1119
-
1120
- // Combine global and route-specific middleware
1121
- const allMiddlewares = [...this.globalMiddlewares, ...match.middlewares];
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
- );
1131
-
1132
- // === HOOK: onResponse ===
1133
- if (this.lifecycleHooks.onResponse) {
1134
- const hookResult = await this.lifecycleHooks.onResponse(ctx, response);
1135
- if (this.isResponse(hookResult)) {
1136
- response = hookResult;
1137
- }
1138
- }
1139
-
1140
- // Send response
1141
- await this.sendResponse(res, response, ctx);
1142
-
1143
- } catch (error) {
1144
- // === HOOK: onError ===
1145
- if (ctx && this.lifecycleHooks.onError) {
1146
- try {
1147
- const hookResult = await this.lifecycleHooks.onError(ctx, error as Error);
1148
- if (this.isResponse(hookResult)) {
1149
- await this.sendResponse(res, hookResult, ctx);
1150
- return;
1151
- }
1152
- } catch (hookError) {
1153
- // Hook itself threw an error, continue to default error handler
1154
- console.error('onError hook failed:', hookError);
1155
- }
1156
- }
1157
-
1158
- // 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
- }
1169
- }
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
- }
1182
-
1183
- // Release context back to pool
1184
- if (ctx) {
1185
- this.contextPool.release(ctx);
1186
- }
1187
- }
1188
- }
1189
-
1190
- /**
1191
- * Check if value is a Response object
1192
- */
1193
- private isResponse(value: any): value is Response {
1194
- return (
1195
- value &&
1196
- typeof value === 'object' &&
1197
- 'statusCode' in value &&
1198
- 'headers' in value &&
1199
- 'body' in value
1200
- );
1201
- }
1202
-
1203
- /**
1204
- * Send HTTP response
1205
- */
1206
- private async sendResponse(res: ServerResponse, response: Response, ctx: Context): Promise<void> {
1207
- // Set status code
1208
- res.statusCode = response.statusCode;
1209
-
1210
- // Set headers
1211
- for (const [key, value] of Object.entries(response.headers)) {
1212
- if (value !== undefined) {
1213
- res.setHeader(key, value);
1214
- }
1215
- }
1216
-
1217
- // Set cookies
1218
- if ('getSetCookieHeaders' in ctx && typeof ctx.getSetCookieHeaders === 'function') {
1219
- const setCookies = ctx.getSetCookieHeaders();
1220
- if (setCookies.length > 0) {
1221
- res.setHeader('Set-Cookie', setCookies);
1222
- }
1223
- }
1224
-
1225
- // Send body or stream
1226
- if (response.stream) {
1227
- response.stream.pipe(res);
1228
- } else {
1229
- res.end(response.body);
1230
- }
1231
- }
1232
-
1233
- /**
1234
- * Get all registered routes
1235
- */
1236
- getRoutes() {
1237
- return this.router.getRoutes();
1238
- }
1239
-
1240
- /**
1241
- * Get context pool statistics
1242
- */
1243
- getPoolStats() {
1244
- return this.contextPool.getStats();
1245
- }
1246
-
1247
- /**
1248
- * Close the server
1249
- */
1250
- close(callback?: (err?: Error) => void): void {
1251
- if (this.server) {
1252
- this.server.close(callback);
1253
- }
1254
- }
1255
-
1256
- /**
1257
- * Enable graceful shutdown for zero-downtime deployments
1258
- */
1259
- gracefulShutdown(options: GracefulShutdownOptions = {}): this {
1260
- this.shutdownManager = new GracefulShutdownManager({
1261
- verbose: this.config.debug,
1262
- ...options
1263
- });
1264
- return this;
1265
- }
1266
-
1267
- /**
1268
- * Add a shutdown hook (e.g., closing database connections)
1269
- */
1270
- onShutdown(name: string, handler: () => Promise<void>, priority?: number): this {
1271
- if (!this.shutdownManager) {
1272
- this.gracefulShutdown();
1273
- }
1274
- this.shutdownManager!.addHook({ name, handler, priority });
1275
- return this;
1276
- }
1277
-
1278
- /**
1279
- * Initiate graceful shutdown programmatically
1280
- */
1281
- async shutdown(): Promise<void> {
1282
- // Shutdown all plugins first
1283
- await this.pluginManager.shutdown();
1284
-
1285
- // Dispose all stores
1286
- this.storeRegistry.dispose();
1287
-
1288
- if (this.shutdownManager) {
1289
- await this.shutdownManager.shutdown();
1290
- } else {
1291
- this.close();
1292
- }
1293
- }
1294
-
1295
- /**
1296
- * Check if shutdown is in progress
1297
- */
1298
- isShuttingDown(): boolean {
1299
- return this.shutdownManager?.isInShutdown() ?? false;
1300
- }
1301
-
1302
- /**
1303
- * Get the shutdown manager for advanced usage
1304
- */
1305
- getShutdownManager(): GracefulShutdownManager | undefined {
1306
- return this.shutdownManager;
1307
- }
1308
-
1309
- /**
1310
- * Start the application with clustering support
1311
- * @param options Cluster options
1312
- * @param startFn Function to start the server (called in each worker)
1313
- */
1314
- cluster(options: ClusterOptions = {}): ClusterManager {
1315
- this.clusterManager = new ClusterManager({
1316
- verbose: this.config.debug,
1317
- ...options
1318
- });
1319
- return this.clusterManager;
1320
- }
1321
-
1322
- /**
1323
- * Get the cluster manager
1324
- */
1325
- getClusterManager(): ClusterManager | undefined {
1326
- return this.clusterManager;
1327
- }
1328
- }
1329
-
1330
- /**
1331
- * Factory function to create an application
1332
- */
1333
- export function createApp(config?: AppConfig): Application {
1334
- return new Application(config);
1335
- }