@coherent.js/api 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -0
  3. package/dist/api/errors.d.ts +92 -0
  4. package/dist/api/errors.d.ts.map +1 -0
  5. package/dist/api/errors.js +161 -0
  6. package/dist/api/errors.js.map +1 -0
  7. package/dist/api/index.d.ts +61 -0
  8. package/dist/api/index.d.ts.map +1 -0
  9. package/dist/api/index.js +41 -0
  10. package/dist/api/index.js.map +1 -0
  11. package/dist/api/middleware.d.ts +57 -0
  12. package/dist/api/middleware.d.ts.map +1 -0
  13. package/dist/api/middleware.js +244 -0
  14. package/dist/api/middleware.js.map +1 -0
  15. package/dist/api/openapi.d.ts +54 -0
  16. package/dist/api/openapi.d.ts.map +1 -0
  17. package/dist/api/openapi.js +144 -0
  18. package/dist/api/openapi.js.map +1 -0
  19. package/dist/api/router.d.ts +368 -0
  20. package/dist/api/router.d.ts.map +1 -0
  21. package/dist/api/router.js +1508 -0
  22. package/dist/api/router.js.map +1 -0
  23. package/dist/api/security.d.ts +64 -0
  24. package/dist/api/security.d.ts.map +1 -0
  25. package/dist/api/security.js +239 -0
  26. package/dist/api/security.js.map +1 -0
  27. package/dist/api/serialization.d.ts +86 -0
  28. package/dist/api/serialization.d.ts.map +1 -0
  29. package/dist/api/serialization.js +151 -0
  30. package/dist/api/serialization.js.map +1 -0
  31. package/dist/api/validation.d.ts +34 -0
  32. package/dist/api/validation.d.ts.map +1 -0
  33. package/dist/api/validation.js +172 -0
  34. package/dist/api/validation.js.map +1 -0
  35. package/dist/index.cjs +1776 -0
  36. package/dist/index.cjs.map +7 -0
  37. package/dist/index.js +1722 -0
  38. package/dist/index.js.map +7 -0
  39. package/package.json +46 -0
  40. package/types/index.d.ts +720 -0
@@ -0,0 +1,1508 @@
1
+ /**
2
+ * Object-based Router for Coherent.js API framework
3
+ * Pure object-oriented approach to backend API routing
4
+ *
5
+ * @fileoverview Transforms nested JavaScript objects into API routes with
6
+ * middleware, validation, and error handling support.
7
+ *
8
+ * @author Coherent.js Team
9
+ * @version 1.0.0
10
+ */
11
+ import { createHash, randomBytes } from 'node:crypto';
12
+ import { withValidation } from './validation.js';
13
+ import { withErrorHandling } from './errors.js';
14
+ import { createServer } from 'node:http';
15
+ import { parse as parseUrl } from 'node:url';
16
+ /**
17
+ * HTTP methods supported by the object router
18
+ * @private
19
+ */
20
+ const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
21
+ /**
22
+ * Parse JSON request body with security limits
23
+ * @private
24
+ */
25
+ function parseBody(req, maxSize = 1024 * 1024) {
26
+ return new Promise((resolve, reject) => {
27
+ if (req.method === 'GET' || req.method === 'DELETE') {
28
+ resolve({});
29
+ return;
30
+ }
31
+ let body = '';
32
+ let size = 0;
33
+ req.on('data', chunk => {
34
+ size += chunk.length;
35
+ if (size > maxSize) {
36
+ reject(new Error('Request body too large'));
37
+ return;
38
+ }
39
+ body += chunk.toString();
40
+ });
41
+ req.on('end', () => {
42
+ try {
43
+ const contentType = req.headers['content-type'] || '';
44
+ if (contentType.includes('application/json')) {
45
+ const parsed = body ? JSON.parse(body) : {};
46
+ // Basic input sanitization
47
+ resolve(sanitizeInput(parsed));
48
+ }
49
+ else {
50
+ resolve({});
51
+ }
52
+ }
53
+ catch {
54
+ reject(new Error('Invalid JSON body'));
55
+ }
56
+ });
57
+ req.on('error', reject);
58
+ });
59
+ }
60
+ /**
61
+ * Basic input sanitization
62
+ * @private
63
+ */
64
+ function sanitizeInput(obj) {
65
+ if (typeof obj !== 'object' || obj === null)
66
+ return obj;
67
+ const sanitized = {};
68
+ for (const [key, value] of Object.entries(obj)) {
69
+ // Remove potentially dangerous keys
70
+ if (key.startsWith('__') || key.includes('prototype'))
71
+ continue;
72
+ if (typeof value === 'string') {
73
+ // Basic XSS prevention
74
+ sanitized[key] = value.replace(/<script[^>]*>.*?<\/script>/gi, '')
75
+ .replace(/javascript:/gi, '')
76
+ .replace(/on\w+=/gi, '');
77
+ }
78
+ else if (typeof value === 'object') {
79
+ sanitized[key] = sanitizeInput(value);
80
+ }
81
+ else {
82
+ sanitized[key] = value;
83
+ }
84
+ }
85
+ return sanitized;
86
+ }
87
+ /**
88
+ * Rate limiting store
89
+ * @private
90
+ */
91
+ const rateLimitStore = new Map();
92
+ /**
93
+ * Rate limiting middleware for API endpoints
94
+ * @private
95
+ * @param {string} ip - Client IP address
96
+ * @param {number} [windowMs=60000] - Time window in milliseconds
97
+ * @param {number} [maxRequests=100] - Maximum requests per window
98
+ * @returns {boolean} True if request is allowed, false if rate limited
99
+ */
100
+ function checkRateLimit(ip, windowMs = 60000, maxRequests = 100) {
101
+ const now = Date.now();
102
+ const key = ip;
103
+ if (!rateLimitStore.has(key)) {
104
+ rateLimitStore.set(key, { count: 1, resetTime: now + windowMs });
105
+ return true;
106
+ }
107
+ const record = rateLimitStore.get(key);
108
+ if (now > record.resetTime) {
109
+ record.count = 1;
110
+ record.resetTime = now + windowMs;
111
+ return true;
112
+ }
113
+ if (record.count >= maxRequests) {
114
+ return false;
115
+ }
116
+ record.count++;
117
+ return true;
118
+ }
119
+ /**
120
+ * Add comprehensive security headers to HTTP response
121
+ * @private
122
+ * @param {Object} res - HTTP response object
123
+ * @param {string|null} [corsOrigin=null] - CORS origin, defaults to localhost:3000
124
+ */
125
+ function addSecurityHeaders(res, corsOrigin = null) {
126
+ // CORS headers - configurable origin
127
+ const origin = corsOrigin || 'http://localhost:3000';
128
+ res.setHeader('Access-Control-Allow-Origin', origin);
129
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
130
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
131
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
132
+ // Security headers
133
+ res.setHeader('X-Content-Type-Options', 'nosniff');
134
+ res.setHeader('X-Frame-Options', 'DENY');
135
+ res.setHeader('X-XSS-Protection', '1; mode=block');
136
+ res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
137
+ res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
138
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
139
+ }
140
+ /**
141
+ * Extract parameters from URL path with constraint support
142
+ * @private
143
+ * @param {string} pattern - URL pattern with parameters (e.g., '/users/:id(\\d+)')
144
+ * @param {string} path - Actual URL path to match
145
+ * @returns {Object|null} Extracted parameters object or null if no match
146
+ * @example
147
+ * extractParams('/users/:id(\\d+)', '/users/123') // { id: '123' }
148
+ * extractParams('/users/:id?', '/users') // {}
149
+ */
150
+ function extractParams(pattern, path) {
151
+ const patternParts = pattern.split('/');
152
+ const pathParts = path.split('/');
153
+ const params = {};
154
+ // Handle wildcard patterns that can match different lengths
155
+ const hasMultiWildcard = patternParts.includes('**');
156
+ const hasSingleWildcard = patternParts.includes('*');
157
+ if (!hasMultiWildcard && !hasSingleWildcard && patternParts.length !== pathParts.length) {
158
+ return null;
159
+ }
160
+ for (let i = 0; i < patternParts.length; i++) {
161
+ const patternPart = patternParts[i];
162
+ const pathPart = pathParts[i];
163
+ if (patternPart.startsWith(':')) {
164
+ // Parse parameter with optional constraint: :name(regex) or :name?
165
+ const match = patternPart.match(/^:([^(]+)(\(([^)]+)\))?(\?)?$/);
166
+ if (match) {
167
+ const [, paramName, , constraint, optional] = match;
168
+ // Check if parameter is optional and path part is missing
169
+ if (optional && !pathPart) {
170
+ continue;
171
+ }
172
+ // Apply constraint if present
173
+ if (constraint) {
174
+ const regex = new RegExp(`^${constraint}$`);
175
+ if (!regex.test(pathPart)) {
176
+ return null; // Constraint failed
177
+ }
178
+ }
179
+ params[paramName] = pathPart;
180
+ }
181
+ else {
182
+ // Fallback to simple parameter
183
+ params[patternPart.slice(1)] = pathPart;
184
+ }
185
+ }
186
+ else if (patternPart === '*') {
187
+ // Single wildcard - matches one segment
188
+ params.splat = pathPart;
189
+ }
190
+ else if (patternPart === '**') {
191
+ // Multi-segment wildcard - matches remaining path
192
+ params.splat = pathParts.slice(i).join('/');
193
+ return params; // ** consumes rest of path
194
+ }
195
+ else if (patternPart !== pathPart) {
196
+ return null;
197
+ }
198
+ }
199
+ return params;
200
+ }
201
+ /**
202
+ * Transforms nested route objects into registered API routes
203
+ *
204
+ * @param {Object} routeObj - Route definition object
205
+ * @param {Object} router - Router instance
206
+ * @param {string} basePath - Current path prefix
207
+ */
208
+ function processRoutes(routeObj, router, basePath = '') {
209
+ if (!routeObj || typeof routeObj !== 'object')
210
+ return;
211
+ Object.entries(routeObj).forEach(([key, config]) => {
212
+ if (!config || typeof config !== 'object')
213
+ return;
214
+ // Check if this is a WebSocket route configuration
215
+ if (config.ws && typeof config.ws === 'function') {
216
+ // WebSocket route - register directly
217
+ const cleanKey = key.startsWith('/') ? key.slice(1) : key;
218
+ const wsPath = basePath ? `${basePath}/${cleanKey}` : `/${cleanKey}`;
219
+ router.addWebSocketRoute(wsPath, config.ws);
220
+ return;
221
+ }
222
+ if (HTTP_METHODS.includes(key.toUpperCase())) {
223
+ // HTTP method route
224
+ registerRoute(key.toUpperCase(), config, router, basePath);
225
+ }
226
+ else {
227
+ // Nested path - handle leading slash correctly
228
+ const cleanKey = key.startsWith('/') ? key.slice(1) : key;
229
+ const path = basePath ? `${basePath}/${cleanKey}` : `/${cleanKey}`;
230
+ processRoutes(config, router, path);
231
+ }
232
+ });
233
+ }
234
+ /**
235
+ * Registers a single route with middleware chain
236
+ *
237
+ * @param {string} method - HTTP method
238
+ * @param {Object} config - Route configuration
239
+ * @param {Object} router - Router instance
240
+ * @param {string} path - Route path
241
+ */
242
+ function registerRoute(method, config, router, path) {
243
+ const { handler, handlers, validation, middleware, errorHandling = true, path: customPath, name } = config;
244
+ const routePath = customPath || path || '/';
245
+ const chain = [];
246
+ // Add middleware
247
+ if (middleware) {
248
+ chain.push(...(Array.isArray(middleware) ? middleware : [middleware]));
249
+ }
250
+ // Add validation
251
+ if (validation) {
252
+ chain.push(withValidation(validation));
253
+ }
254
+ // Add handlers
255
+ if (handlers) {
256
+ chain.push(...handlers);
257
+ }
258
+ else if (handler) {
259
+ chain.push(handler);
260
+ }
261
+ else {
262
+ console.warn(`No handler for ${method} ${routePath}`);
263
+ return;
264
+ }
265
+ // Apply error handling
266
+ if (errorHandling) {
267
+ chain.forEach((fn, i) => {
268
+ chain[i] = withErrorHandling(fn);
269
+ });
270
+ }
271
+ // Register route with name option
272
+ router.addRoute(method, routePath, async (req, res) => {
273
+ try {
274
+ // Execute middleware and handler chain
275
+ let result = null;
276
+ for (const fn of chain) {
277
+ result = await fn(req, res);
278
+ if (result && typeof result === 'object') {
279
+ break;
280
+ }
281
+ }
282
+ if (result && typeof result === 'object') {
283
+ res.writeHead(200, { 'Content-Type': 'application/json' });
284
+ res.end(JSON.stringify(result));
285
+ }
286
+ else {
287
+ res.writeHead(204);
288
+ res.end();
289
+ }
290
+ }
291
+ catch (error) {
292
+ const statusCode = error.statusCode || 500;
293
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
294
+ res.end(JSON.stringify({ error: error.message }));
295
+ }
296
+ }, { name });
297
+ }
298
+ /**
299
+ * Simple router implementation for object-based routing with WebSocket support
300
+ *
301
+ * @class SimpleRouter
302
+ * @description Provides HTTP and WebSocket routing with middleware support, caching,
303
+ * versioning, content negotiation, and performance metrics.
304
+ *
305
+ * @param {Object} [options={}] - Router configuration options
306
+ * @param {number} [options.maxCacheSize=1000] - Maximum cache size for route lookups
307
+ * @param {boolean} [options.enableCompilation=true] - Enable route pattern compilation
308
+ * @param {boolean} [options.enableVersioning=false] - Enable API versioning
309
+ * @param {string} [options.defaultVersion='v1'] - Default API version
310
+ * @param {string} [options.versionHeader='api-version'] - Header for version detection
311
+ * @param {boolean} [options.enableContentNegotiation=true] - Enable content type negotiation
312
+ * @param {string} [options.defaultContentType='application/json'] - Default response content type
313
+ * @param {boolean} [options.enableWebSockets=false] - Enable WebSocket routing
314
+ * @param {boolean} [options.enableMetrics=false] - Enable performance metrics collection
315
+ *
316
+ * @example
317
+ * const router = new SimpleRouter({
318
+ * enableWebSockets: true,
319
+ * enableMetrics: true,
320
+ * maxCacheSize: 2000
321
+ * });
322
+ */
323
+ class SimpleRouter {
324
+ constructor(options = {}) {
325
+ this.routes = [];
326
+ this.routeCache = new Map();
327
+ this.namedRoutes = new Map();
328
+ this.maxCacheSize = options.maxCacheSize || 1000;
329
+ this.routeGroups = [];
330
+ this.globalMiddleware = [];
331
+ // Route compilation options
332
+ this.enableCompilation = options.enableCompilation !== false; // Default true
333
+ this.compiledRoutes = new Map(); // Compiled regex patterns
334
+ this.routeCompilationCache = new Map(); // Cache compiled patterns
335
+ // Route versioning options
336
+ this.enableVersioning = options.enableVersioning || false;
337
+ this.defaultVersion = options.defaultVersion || 'v1';
338
+ this.versionHeader = options.versionHeader || 'api-version';
339
+ this.versionedRoutes = new Map(); // version -> routes mapping
340
+ // Content negotiation options
341
+ this.enableContentNegotiation = options.enableContentNegotiation !== false; // Default true
342
+ this.defaultContentType = options.defaultContentType || 'application/json';
343
+ // WebSocket routing options
344
+ this.enableWebSockets = options.enableWebSockets || false;
345
+ this.wsRoutes = [];
346
+ this.wsConnections = new Map(); // Track active WebSocket connections
347
+ // Performance metrics
348
+ this.enableMetrics = options.enableMetrics || false;
349
+ if (this.enableMetrics) {
350
+ this.metrics = {
351
+ requests: 0,
352
+ cacheHits: 0,
353
+ compilationHits: 0,
354
+ routeMatches: new Map(),
355
+ responseTime: [],
356
+ errors: 0,
357
+ versionRequests: new Map(), // Track requests per version
358
+ contentTypeRequests: new Map(), // Track requests per content type
359
+ wsConnections: 0, // Track WebSocket connections
360
+ wsMessages: 0 // Track WebSocket messages
361
+ };
362
+ }
363
+ }
364
+ /**
365
+ * Add an HTTP route to the router
366
+ *
367
+ * @param {string} method - HTTP method (GET, POST, PUT, DELETE, PATCH)
368
+ * @param {string} path - Route path pattern (supports :param and wildcards)
369
+ * @param {Function} handler - Route handler function
370
+ * @param {Object} [options={}] - Route options
371
+ * @param {Array} [options.middleware] - Route-specific middleware
372
+ * @param {string} [options.name] - Named route for URL generation
373
+ * @param {string} [options.version] - API version for this route
374
+ *
375
+ * @example
376
+ * router.addRoute('GET', '/users/:id', (req, res) => {
377
+ * return { user: { id: req.params.id } };
378
+ * }, { name: 'getUser', version: 'v2' });
379
+ */
380
+ addRoute(method, path, handler, options = {}) {
381
+ // Apply group prefix
382
+ const prefix = this.getCurrentPrefix();
383
+ const fullPath = prefix + (path.startsWith('/') ? path : `/${path}`);
384
+ // Combine group middleware with route middleware
385
+ const groupMiddleware = this.getCurrentGroupMiddleware();
386
+ const routeMiddleware = options.middleware || [];
387
+ const allMiddleware = [...this.globalMiddleware, ...groupMiddleware, ...routeMiddleware];
388
+ const route = {
389
+ method: method.toUpperCase(),
390
+ path: fullPath,
391
+ handler,
392
+ middleware: allMiddleware,
393
+ name: options.name,
394
+ version: options.version || this.defaultVersion
395
+ };
396
+ // Compile route pattern if compilation is enabled
397
+ if (this.enableCompilation) {
398
+ route.compiled = this.compileRoute(fullPath);
399
+ }
400
+ this.routes.push(route);
401
+ // Store versioned route if versioning is enabled
402
+ if (this.enableVersioning) {
403
+ if (!this.versionedRoutes.has(route.version)) {
404
+ this.versionedRoutes.set(route.version, []);
405
+ }
406
+ this.versionedRoutes.get(route.version).push(route);
407
+ }
408
+ // Store named route for URL generation
409
+ if (options.name) {
410
+ this.namedRoutes.set(options.name, { method: route.method, path: fullPath, version: route.version });
411
+ }
412
+ }
413
+ /**
414
+ * Add a versioned route
415
+ * @param {string} version - API version (e.g., 'v1', 'v2')
416
+ * @param {string} method - HTTP method
417
+ * @param {string} path - Route path
418
+ * @param {Function} handler - Route handler
419
+ * @param {Object} options - Route options
420
+ */
421
+ addVersionedRoute(version, method, path, handler, options = {}) {
422
+ this.addRoute(method, path, handler, { ...options, version });
423
+ }
424
+ /**
425
+ * Add route with content negotiation support
426
+ * @param {string} method - HTTP method
427
+ * @param {string} path - Route path
428
+ * @param {Object} handlers - Content type handlers { 'application/json': handler, 'text/xml': handler }
429
+ * @param {Object} options - Route options
430
+ */
431
+ addContentNegotiatedRoute(method, path, handlers, options = {}) {
432
+ const negotiationHandler = async (req, res) => {
433
+ const acceptedType = this.negotiateContentType(req, Object.keys(handlers));
434
+ if (this.enableMetrics) {
435
+ this.metrics.contentTypeRequests.set(acceptedType, (this.metrics.contentTypeRequests.get(acceptedType) || 0) + 1);
436
+ }
437
+ const handler = handlers[acceptedType];
438
+ if (!handler) {
439
+ res.writeHead(406, { 'Content-Type': 'application/json' });
440
+ res.end(JSON.stringify({
441
+ error: 'Not Acceptable',
442
+ supportedTypes: Object.keys(handlers)
443
+ }));
444
+ return;
445
+ }
446
+ const result = await handler(req, res);
447
+ if (result && typeof result === 'object') {
448
+ res.writeHead(200, { 'Content-Type': acceptedType });
449
+ if (acceptedType === 'application/json') {
450
+ res.end(JSON.stringify(result));
451
+ }
452
+ else if (acceptedType === 'text/xml' || acceptedType === 'application/xml') {
453
+ res.end(this.objectToXml(result));
454
+ }
455
+ else if (acceptedType === 'text/html') {
456
+ res.end(typeof result === 'string' ? result : `<pre>${JSON.stringify(result, null, 2)}</pre>`);
457
+ }
458
+ else if (acceptedType === 'text/plain') {
459
+ res.end(typeof result === 'string' ? result : JSON.stringify(result));
460
+ }
461
+ else {
462
+ res.end(JSON.stringify(result));
463
+ }
464
+ }
465
+ };
466
+ this.addRoute(method, path, negotiationHandler, options);
467
+ }
468
+ /**
469
+ * Negotiate content type based on Accept header
470
+ * @param {Object} req - Request object
471
+ * @param {Array} supportedTypes - Array of supported content types
472
+ * @returns {string} Best matching content type
473
+ * @private
474
+ */
475
+ negotiateContentType(req, supportedTypes) {
476
+ const acceptHeader = req.headers.accept || this.defaultContentType;
477
+ // Parse Accept header and find best match
478
+ const acceptedTypes = acceptHeader
479
+ .split(',')
480
+ .map(type => {
481
+ const [mediaType, ...params] = type.trim().split(';');
482
+ const qValue = params.find(p => p.trim().startsWith('q='));
483
+ const quality = qValue ? parseFloat(qValue.split('=')[1]) : 1.0;
484
+ return { type: mediaType.trim(), quality };
485
+ })
486
+ .sort((a, b) => b.quality - a.quality);
487
+ // Find first supported type
488
+ for (const accepted of acceptedTypes) {
489
+ if (accepted.type === '*/*') {
490
+ return supportedTypes[0] || this.defaultContentType;
491
+ }
492
+ const [mainType, subType] = accepted.type.split('/');
493
+ for (const supported of supportedTypes) {
494
+ const [supportedMain, supportedSub] = supported.split('/');
495
+ if (accepted.type === supported ||
496
+ (mainType === supportedMain && subType === '*') ||
497
+ (mainType === '*' && subType === supportedSub)) {
498
+ return supported;
499
+ }
500
+ }
501
+ }
502
+ return supportedTypes[0] || this.defaultContentType;
503
+ }
504
+ /**
505
+ * Convert object to XML string
506
+ * @param {Object} obj - Object to convert
507
+ * @param {string} rootName - Root element name
508
+ * @returns {string} XML string
509
+ * @private
510
+ */
511
+ objectToXml(obj, rootName = 'root') {
512
+ const xmlEscape = (str) => String(str)
513
+ .replace(/&/g, '&amp;')
514
+ .replace(/</g, '&lt;')
515
+ .replace(/>/g, '&gt;')
516
+ .replace(/"/g, '&quot;')
517
+ .replace(/'/g, '&#39;');
518
+ const toXml = (obj, name) => {
519
+ if (obj === null || obj === undefined) {
520
+ return `<${name}/>`;
521
+ }
522
+ if (typeof obj !== 'object') {
523
+ return `<${name}>${xmlEscape(obj)}</${name}>`;
524
+ }
525
+ if (Array.isArray(obj)) {
526
+ return obj.map(item => toXml(item, 'item')).join('');
527
+ }
528
+ const content = Object.entries(obj)
529
+ .map(([key, value]) => toXml(value, key))
530
+ .join('');
531
+ return `<${name}>${content}</${name}>`;
532
+ };
533
+ return `<?xml version="1.0" encoding="UTF-8"?>${toXml(obj, rootName)}`;
534
+ }
535
+ /**
536
+ * Add WebSocket route
537
+ * @param {string} path - WebSocket path
538
+ * @param {Function} handler - WebSocket handler function
539
+ * @param {Object} options - Route options
540
+ */
541
+ addWebSocketRoute(path, handler, options = {}) {
542
+ if (!this.enableWebSockets) {
543
+ throw new Error('WebSocket routing is disabled. Enable with { enableWebSockets: true }');
544
+ }
545
+ const prefix = this.getCurrentPrefix();
546
+ const fullPath = prefix + (path.startsWith('/') ? path : `/${path}`);
547
+ const wsRoute = {
548
+ path: fullPath,
549
+ handler,
550
+ name: options.name,
551
+ version: options.version || this.defaultVersion,
552
+ compiled: this.enableCompilation ? this.compileRoute(fullPath) : null
553
+ };
554
+ this.wsRoutes.push(wsRoute);
555
+ if (options.name) {
556
+ this.namedRoutes.set(options.name, { method: 'WS', path: fullPath, version: wsRoute.version });
557
+ }
558
+ }
559
+ /**
560
+ * Handle WebSocket upgrade request
561
+ * @param {Object} request - HTTP request object
562
+ * @param {Object} socket - Socket object
563
+ * @param {Buffer} head - First packet of the upgraded stream
564
+ */
565
+ handleWebSocketUpgrade(request, socket, head) {
566
+ if (!this.enableWebSockets) {
567
+ socket.end('HTTP/1.1 501 Not Implemented\r\n\r\n');
568
+ return;
569
+ }
570
+ const url = new URL(request.url, `http://${request.headers.host}`);
571
+ const pathname = url.pathname;
572
+ // Find matching WebSocket route
573
+ let matchedRoute = null;
574
+ for (const wsRoute of this.wsRoutes) {
575
+ let params = null;
576
+ if (this.enableCompilation && wsRoute.compiled) {
577
+ params = this.matchCompiledRoute(wsRoute.compiled, pathname);
578
+ }
579
+ else {
580
+ params = extractParams(wsRoute.path, pathname);
581
+ }
582
+ if (params !== null) {
583
+ matchedRoute = { route: wsRoute, params };
584
+ break;
585
+ }
586
+ }
587
+ if (!matchedRoute) {
588
+ socket.end('HTTP/1.1 404 Not Found\r\n\r\n');
589
+ return;
590
+ }
591
+ // Create WebSocket connection
592
+ this.createWebSocketConnection(request, socket, head, matchedRoute);
593
+ }
594
+ /**
595
+ * Create WebSocket connection
596
+ * @param {Object} request - HTTP request object
597
+ * @param {Object} socket - Socket object
598
+ * @param {Buffer} head - First packet of the upgraded stream
599
+ * @param {Object} matchedRoute - Matched WebSocket route
600
+ * @private
601
+ */
602
+ createWebSocketConnection(request, socket, head, matchedRoute) {
603
+ // WebSocket handshake
604
+ const key = request.headers['sec-websocket-key'];
605
+ if (!key) {
606
+ socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
607
+ return;
608
+ }
609
+ const acceptKey = createHash('sha1')
610
+ .update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
611
+ .digest('base64');
612
+ const responseHeaders = [
613
+ 'HTTP/1.1 101 Switching Protocols',
614
+ 'Upgrade: websocket',
615
+ 'Connection: Upgrade',
616
+ `Sec-WebSocket-Accept: ${acceptKey}`,
617
+ '', ''
618
+ ].join('\r\n');
619
+ socket.write(responseHeaders);
620
+ // Create WebSocket wrapper
621
+ const ws = this.createWebSocketWrapper(socket, matchedRoute);
622
+ // Track connection
623
+ const connectionId = randomBytes(16).toString('hex');
624
+ this.wsConnections.set(connectionId, ws);
625
+ if (this.enableMetrics) {
626
+ this.metrics.wsConnections++;
627
+ }
628
+ // Add connection metadata
629
+ ws.id = connectionId;
630
+ ws.params = matchedRoute.params;
631
+ ws.path = matchedRoute.route.path;
632
+ // Handle connection cleanup
633
+ socket.on('close', () => {
634
+ // Call the route handler's close callback before cleanup
635
+ if (matchedRoute.route.handler.onClose) {
636
+ matchedRoute.route.handler.onClose(ws);
637
+ }
638
+ // Store connection info before deletion for potential use in handlers
639
+ // const connectionInfo = { id: connectionId, path: ws.path };
640
+ // Fire any custom close handlers set by the route handler
641
+ if (ws.onclose) {
642
+ try {
643
+ ws.onclose();
644
+ }
645
+ catch (error) {
646
+ console.error('WebSocket onclose handler error:', error);
647
+ }
648
+ }
649
+ this.wsConnections.delete(connectionId);
650
+ if (this.enableMetrics) {
651
+ this.metrics.wsConnections--;
652
+ }
653
+ });
654
+ // Call route handler
655
+ try {
656
+ matchedRoute.route.handler(ws, request);
657
+ }
658
+ catch (err) {
659
+ console.error('WebSocket upgrade error:', err);
660
+ socket.end('HTTP/1.1 500 Internal Server Error\r\n\r\n');
661
+ }
662
+ }
663
+ /**
664
+ * Create WebSocket wrapper with message handling
665
+ * @param {Object} socket - Raw socket
666
+ * @param {Object} matchedRoute - Matched route info
667
+ * @returns {Object} WebSocket wrapper
668
+ * @private
669
+ */
670
+ createWebSocketWrapper(socket) {
671
+ const ws = {
672
+ socket,
673
+ readyState: 1, // OPEN
674
+ send(data) {
675
+ if (this.readyState !== 1)
676
+ return;
677
+ const message = typeof data === 'string' ? data : JSON.stringify(data);
678
+ const frame = this.createFrame(message);
679
+ socket.write(frame);
680
+ if (ws.router && ws.router.enableMetrics) {
681
+ ws.router.metrics.wsMessages++;
682
+ }
683
+ },
684
+ close(code = 1000, reason = '') {
685
+ if (this.readyState !== 1)
686
+ return;
687
+ this.readyState = 3; // CLOSED
688
+ const frame = this.createCloseFrame(code, reason);
689
+ socket.write(frame);
690
+ socket.destroy(); // Force close the socket to trigger 'close' event
691
+ },
692
+ ping(data = Buffer.alloc(0)) {
693
+ if (this.readyState !== 1)
694
+ return;
695
+ const frame = this.createPingFrame(data);
696
+ socket.write(frame);
697
+ },
698
+ createFrame(data) {
699
+ const payload = Buffer.from(data, 'utf8');
700
+ const payloadLength = payload.length;
701
+ let frame;
702
+ if (payloadLength < 126) {
703
+ frame = Buffer.allocUnsafe(2 + payloadLength);
704
+ frame[0] = 0x81; // FIN + text frame
705
+ frame[1] = payloadLength;
706
+ payload.copy(frame, 2);
707
+ }
708
+ else if (payloadLength < 65536) {
709
+ frame = Buffer.allocUnsafe(4 + payloadLength);
710
+ frame[0] = 0x81;
711
+ frame[1] = 126;
712
+ frame.writeUInt16BE(payloadLength, 2);
713
+ payload.copy(frame, 4);
714
+ }
715
+ else {
716
+ frame = Buffer.allocUnsafe(10 + payloadLength);
717
+ frame[0] = 0x81;
718
+ frame[1] = 127;
719
+ frame.writeUInt32BE(0, 2);
720
+ frame.writeUInt32BE(payloadLength, 6);
721
+ payload.copy(frame, 10);
722
+ }
723
+ return frame;
724
+ },
725
+ createCloseFrame(code, reason) {
726
+ const reasonBuffer = Buffer.from(reason, 'utf8');
727
+ const frame = Buffer.allocUnsafe(4 + reasonBuffer.length);
728
+ frame[0] = 0x88; // FIN + close frame
729
+ frame[1] = 2 + reasonBuffer.length;
730
+ frame.writeUInt16BE(code, 2);
731
+ reasonBuffer.copy(frame, 4);
732
+ return frame;
733
+ },
734
+ createPingFrame(data) {
735
+ const frame = Buffer.allocUnsafe(2 + data.length);
736
+ frame[0] = 0x89; // FIN + ping frame
737
+ frame[1] = data.length;
738
+ data.copy(frame, 2);
739
+ return frame;
740
+ }
741
+ };
742
+ ws.router = this;
743
+ // Handle incoming messages
744
+ socket.on('data', (buffer) => {
745
+ try {
746
+ const message = this.parseWebSocketFrame(buffer);
747
+ if (message && ws.onmessage) {
748
+ ws.onmessage({ data: message });
749
+ }
750
+ }
751
+ catch {
752
+ }
753
+ });
754
+ // Handle socket errors
755
+ socket.on('error', (err) => {
756
+ console.log('WebSocket socket error (connection likely closed):', err.code);
757
+ // Don't re-throw the error, just log it
758
+ });
759
+ return ws;
760
+ }
761
+ /**
762
+ * Parse WebSocket frame
763
+ * @param {Buffer} buffer - Raw frame data
764
+ * @returns {string|null} Parsed message
765
+ * @private
766
+ */
767
+ parseWebSocketFrame(buffer) {
768
+ if (buffer.length < 2)
769
+ return null;
770
+ const firstByte = buffer[0];
771
+ const secondByte = buffer[1];
772
+ const opcode = firstByte & 0x0f;
773
+ const masked = (secondByte & 0x80) === 0x80;
774
+ let payloadLength = secondByte & 0x7f;
775
+ // Handle close frame (opcode 8)
776
+ if (opcode === 8) {
777
+ return null; // Close frame, don't process as message
778
+ }
779
+ // Only process text frames (opcode 1)
780
+ if (opcode !== 1) {
781
+ return null;
782
+ }
783
+ let offset = 2;
784
+ if (payloadLength === 126) {
785
+ if (buffer.length < offset + 2)
786
+ return null;
787
+ payloadLength = buffer.readUInt16BE(offset);
788
+ offset += 2;
789
+ }
790
+ else if (payloadLength === 127) {
791
+ if (buffer.length < offset + 8)
792
+ return null;
793
+ payloadLength = buffer.readUInt32BE(offset + 4); // Ignore high 32 bits
794
+ offset += 8;
795
+ }
796
+ if (masked) {
797
+ if (buffer.length < offset + 4 + payloadLength)
798
+ return null;
799
+ const maskKey = buffer.slice(offset, offset + 4);
800
+ offset += 4;
801
+ const payload = buffer.slice(offset, offset + payloadLength);
802
+ for (let i = 0; i < payload.length; i++) {
803
+ payload[i] ^= maskKey[i % 4];
804
+ }
805
+ return payload.toString('utf8');
806
+ }
807
+ if (buffer.length < offset + payloadLength)
808
+ return null;
809
+ return buffer.slice(offset, offset + payloadLength).toString('utf8');
810
+ }
811
+ /**
812
+ * Broadcast message to all WebSocket connections on a path
813
+ * @param {string} path - WebSocket path pattern
814
+ * @param {*} message - Message to broadcast
815
+ * @param {string} [excludeId=null] - Connection ID to exclude from broadcast
816
+ */
817
+ broadcast(path, message, excludeId = null) {
818
+ for (const [id, ws] of this.wsConnections) {
819
+ if (id === excludeId)
820
+ continue;
821
+ if (ws.path === path || (path === '*' && ws.path)) {
822
+ try {
823
+ ws.send(message);
824
+ }
825
+ catch {
826
+ console.log('Failed to send message to connection:', id);
827
+ }
828
+ }
829
+ }
830
+ }
831
+ /**
832
+ * Get active WebSocket connections
833
+ * @returns {Array} Array of connection info
834
+ */
835
+ getWebSocketConnections() {
836
+ return Array.from(this.wsConnections.entries()).map(([id, ws]) => ({
837
+ id,
838
+ path: ws.path,
839
+ params: ws.params,
840
+ readyState: ws.readyState
841
+ }));
842
+ }
843
+ /**
844
+ * Get version from request
845
+ * @param {Object} req - Request object
846
+ * @returns {string} API version
847
+ * @private
848
+ */
849
+ getRequestVersion(req) {
850
+ // Check header first
851
+ if (req.headers[this.versionHeader]) {
852
+ return req.headers[this.versionHeader];
853
+ }
854
+ // Check URL path for version prefix (e.g., /v1/users)
855
+ const pathMatch = req.url.match(/^\/v(\d+)/);
856
+ if (pathMatch) {
857
+ return `v${pathMatch[1]}`;
858
+ }
859
+ // Check query parameter
860
+ if (req.query && req.query.version) {
861
+ return req.query.version;
862
+ }
863
+ return this.defaultVersion;
864
+ }
865
+ /**
866
+ * Generate URL for named route with parameter substitution
867
+ *
868
+ * @param {string} name - Route name (set during route registration)
869
+ * @param {Object} [params={}] - Parameters to substitute in the URL pattern
870
+ * @returns {string} Generated URL with parameters substituted
871
+ * @throws {Error} If named route is not found
872
+ *
873
+ * @example
874
+ * // Route registered as: router.addRoute('GET', '/users/:id', handler, { name: 'getUser' })
875
+ * const url = router.url('getUser', { id: 123 }); // '/users/123'
876
+ *
877
+ * // With constrained parameters
878
+ * const url = router.url('getUserPosts', { userId: 123, postId: 456 }); // '/users/123/posts/456'
879
+ */
880
+ generateUrl(name, params = {}) {
881
+ const route = this.namedRoutes.get(name);
882
+ if (!route) {
883
+ throw new Error(`Named route '${name}' not found`);
884
+ }
885
+ let url = route.path;
886
+ // Replace parameters in the URL
887
+ for (const [key, value] of Object.entries(params)) {
888
+ // Handle both simple params (:key) and constrained params (:key(regex))
889
+ const paramPattern = new RegExp(`:${key}(\\([^)]+\\))?`, 'g');
890
+ url = url.replace(paramPattern, encodeURIComponent(value));
891
+ }
892
+ return url;
893
+ }
894
+ /**
895
+ * Add routes from configuration object
896
+ *
897
+ * @param {Object} routeConfig - Route configuration object with nested structure
898
+ * @description Processes nested route objects and registers HTTP and WebSocket routes.
899
+ * Supports declarative route definition with automatic method detection.
900
+ *
901
+ * @example
902
+ * router.addRoutes({
903
+ * 'api': {
904
+ * 'users': {
905
+ * GET: (req, res) => ({ users: [] }),
906
+ * POST: (req, res) => ({ created: true })
907
+ * }
908
+ * }
909
+ * });
910
+ */
911
+ addRoutes(routeConfig) {
912
+ processRoutes(routeConfig, this);
913
+ }
914
+ /**
915
+ * Add global middleware to the router
916
+ *
917
+ * @param {Function|Object} middleware - Middleware function or conditional middleware object
918
+ * @description Adds middleware that runs before all route handlers. Supports both
919
+ * simple functions and conditional middleware objects.
920
+ *
921
+ * @example
922
+ * // Simple middleware
923
+ * router.use((req, res) => {
924
+ * console.log(`${req.method} ${req.url}`);
925
+ * });
926
+ *
927
+ * // Conditional middleware
928
+ * router.use({
929
+ * condition: (req) => req.url.startsWith('/api'),
930
+ * middleware: authMiddleware,
931
+ * name: 'apiAuth'
932
+ * });
933
+ */
934
+ use(middleware) {
935
+ if (typeof middleware === 'function') {
936
+ this.globalMiddleware.push(middleware);
937
+ }
938
+ else if (middleware && typeof middleware === 'object') {
939
+ // Conditional middleware: { condition, middleware, name }
940
+ this.globalMiddleware.push(this.createConditionalMiddleware(middleware));
941
+ }
942
+ }
943
+ /**
944
+ * Create conditional middleware wrapper
945
+ * @param {Object} config - Conditional middleware configuration
946
+ * @returns {Function} Wrapped middleware function
947
+ * @private
948
+ */
949
+ createConditionalMiddleware(config) {
950
+ const { condition, middleware } = config;
951
+ return async (req, res) => {
952
+ // Evaluate condition
953
+ let shouldExecute = false;
954
+ if (typeof condition === 'function') {
955
+ shouldExecute = await condition(req, res);
956
+ }
957
+ else if (typeof condition === 'object') {
958
+ // Object-based conditions
959
+ shouldExecute = this.evaluateConditionObject(condition, req);
960
+ }
961
+ else {
962
+ shouldExecute = !!condition;
963
+ }
964
+ if (shouldExecute) {
965
+ return await middleware(req, res);
966
+ }
967
+ return null; // Skip middleware
968
+ };
969
+ }
970
+ /**
971
+ * Evaluate condition object
972
+ * @param {Object} condition - Condition object
973
+ * @param {Object} req - Request object
974
+ * @param {Object} res - Response object
975
+ * @returns {boolean} Whether condition is met
976
+ * @private
977
+ */
978
+ evaluateConditionObject(condition, req) {
979
+ const { method, path, header, query, body, user } = condition;
980
+ // Method condition
981
+ if (method && !this.matchCondition(req.method, method))
982
+ return false;
983
+ // Path condition
984
+ if (path && !this.matchCondition(req.url, path))
985
+ return false;
986
+ // Header condition
987
+ if (header) {
988
+ for (const [key, value] of Object.entries(header)) {
989
+ if (!this.matchCondition(req.headers[key.toLowerCase()], value))
990
+ return false;
991
+ }
992
+ }
993
+ // Query condition
994
+ if (query && req.query) {
995
+ for (const [key, value] of Object.entries(query)) {
996
+ if (!this.matchCondition(req.query[key], value))
997
+ return false;
998
+ }
999
+ }
1000
+ // Body condition
1001
+ if (body && req.body) {
1002
+ for (const [key, value] of Object.entries(body)) {
1003
+ if (!this.matchCondition(req.body[key], value))
1004
+ return false;
1005
+ }
1006
+ }
1007
+ // User condition (for auth-based conditions)
1008
+ if (user && req.user) {
1009
+ for (const [key, value] of Object.entries(user)) {
1010
+ if (!this.matchCondition(req.user[key], value))
1011
+ return false;
1012
+ }
1013
+ }
1014
+ return true;
1015
+ }
1016
+ /**
1017
+ * Match condition value
1018
+ * @param {*} actual - Actual value
1019
+ * @param {*} expected - Expected value or condition
1020
+ * @returns {boolean} Whether condition matches
1021
+ * @private
1022
+ */
1023
+ matchCondition(actual, expected) {
1024
+ if (expected instanceof RegExp) {
1025
+ return expected.test(String(actual || ''));
1026
+ }
1027
+ if (Array.isArray(expected)) {
1028
+ return expected.includes(actual);
1029
+ }
1030
+ if (typeof expected === 'function') {
1031
+ return expected(actual);
1032
+ }
1033
+ return actual === expected;
1034
+ }
1035
+ /**
1036
+ * Create a route group with shared middleware and prefix
1037
+ * @param {string} prefix - Path prefix for the group
1038
+ * @param {Function|Array} middleware - Shared middleware
1039
+ * @param {Function} callback - Function to define routes in the group
1040
+ */
1041
+ group(prefix, middleware, callback) {
1042
+ const group = {
1043
+ prefix: prefix.startsWith('/') ? prefix : `/${prefix}`,
1044
+ middleware: Array.isArray(middleware) ? middleware : [middleware]
1045
+ };
1046
+ this.routeGroups.push(group);
1047
+ callback(this);
1048
+ this.routeGroups.pop();
1049
+ return this;
1050
+ }
1051
+ /**
1052
+ * Get current route prefix from active groups
1053
+ * @private
1054
+ */
1055
+ getCurrentPrefix() {
1056
+ return this.routeGroups.map(g => g.prefix).join('');
1057
+ }
1058
+ /**
1059
+ * Get current group middleware
1060
+ * @private
1061
+ */
1062
+ getCurrentGroupMiddleware() {
1063
+ return this.routeGroups.flatMap(g => g.middleware);
1064
+ }
1065
+ /**
1066
+ * Compile route pattern into optimized regex
1067
+ * @param {string} pattern - Route pattern to compile
1068
+ * @returns {Object} Compiled route object with regex and parameter names
1069
+ * @private
1070
+ */
1071
+ compileRoute(pattern) {
1072
+ // Check compilation cache first
1073
+ if (this.routeCompilationCache.has(pattern)) {
1074
+ if (this.enableMetrics)
1075
+ this.metrics.compilationHits++;
1076
+ return this.routeCompilationCache.get(pattern);
1077
+ }
1078
+ const paramNames = [];
1079
+ let regexPattern = pattern;
1080
+ // Handle wildcards first
1081
+ if (pattern.includes('**')) {
1082
+ regexPattern = regexPattern.replace(/\/\*\*/g, '/(.*)');
1083
+ paramNames.push('splat');
1084
+ }
1085
+ else if (pattern.includes('*')) {
1086
+ regexPattern = regexPattern.replace(/\/\*/g, '/([^/]+)');
1087
+ paramNames.push('splat');
1088
+ }
1089
+ // Handle parameters with constraints and optional parameters
1090
+ regexPattern = regexPattern.replace(/:([^(/]+)(\([^)]+\))?(\?)?/g, (match, paramName, constraint, optional) => {
1091
+ paramNames.push(paramName);
1092
+ if (constraint) {
1093
+ // Use the constraint pattern directly
1094
+ const constraintPattern = constraint.slice(1, -1); // Remove parentheses
1095
+ return optional ? `(?:/(?:${constraintPattern}))?` : `/(${constraintPattern})`;
1096
+ }
1097
+ else {
1098
+ // Default parameter pattern
1099
+ return optional ? '(?:/([^/]+))?' : '/([^/]+)';
1100
+ }
1101
+ });
1102
+ // Escape special regex characters except those we want to keep
1103
+ regexPattern = regexPattern
1104
+ .replace(/[.+?^${}|[\]\\]/g, '\\$&')
1105
+ .replace(/\\\(/g, '(')
1106
+ .replace(/\\\)/g, ')')
1107
+ .replace(/\\\?/g, '?');
1108
+ // Ensure exact match
1109
+ regexPattern = `^${regexPattern}$`;
1110
+ const compiled = {
1111
+ regex: new RegExp(regexPattern),
1112
+ paramNames,
1113
+ pattern
1114
+ };
1115
+ // Cache the compiled route
1116
+ if (this.routeCompilationCache.size < 1000) {
1117
+ this.routeCompilationCache.set(pattern, compiled);
1118
+ }
1119
+ return compiled;
1120
+ }
1121
+ /**
1122
+ * Match path using compiled route
1123
+ * @param {Object} compiledRoute - Compiled route object
1124
+ * @param {string} path - Path to match
1125
+ * @returns {Object|null} Parameters object or null if no match
1126
+ * @private
1127
+ */
1128
+ matchCompiledRoute(compiledRoute, path) {
1129
+ const match = compiledRoute.regex.exec(path);
1130
+ if (!match)
1131
+ return null;
1132
+ const params = {};
1133
+ for (let i = 0; i < compiledRoute.paramNames.length; i++) {
1134
+ const paramName = compiledRoute.paramNames[i];
1135
+ const value = match[i + 1];
1136
+ if (value !== undefined) {
1137
+ params[paramName] = value;
1138
+ }
1139
+ }
1140
+ return params;
1141
+ }
1142
+ /**
1143
+ * Get performance metrics
1144
+ * @returns {Object} Performance metrics object
1145
+ */
1146
+ getMetrics() {
1147
+ if (!this.enableMetrics) {
1148
+ throw new Error('Metrics collection is disabled. Enable with { enableMetrics: true }');
1149
+ }
1150
+ const avgResponseTime = this.metrics.responseTime.length > 0
1151
+ ? this.metrics.responseTime.reduce((a, b) => a + b, 0) / this.metrics.responseTime.length
1152
+ : 0;
1153
+ return {
1154
+ ...this.metrics,
1155
+ averageResponseTime: Math.round(avgResponseTime * 100) / 100,
1156
+ cacheHitRate: this.metrics.requests > 0 ? `${(this.metrics.cacheHits / this.metrics.requests * 100).toFixed(2)}%` : '0%',
1157
+ compilationHitRate: this.metrics.requests > 0 ? `${(this.metrics.compilationHits / this.metrics.requests * 100).toFixed(2)}%` : '0%'
1158
+ };
1159
+ }
1160
+ /**
1161
+ * Get compilation statistics
1162
+ * @returns {Object} Compilation statistics
1163
+ */
1164
+ getCompilationStats() {
1165
+ const totalRoutes = this.routes.length;
1166
+ const compiledRoutes = this.routes.filter(r => r.compiled).length;
1167
+ const compilationCacheSize = this.routeCompilationCache.size;
1168
+ return {
1169
+ totalRoutes,
1170
+ compiledRoutes,
1171
+ compilationEnabled: this.enableCompilation,
1172
+ compilationCacheSize,
1173
+ compilationCacheHits: this.enableMetrics ? this.metrics.compilationHits : 'N/A (metrics disabled)'
1174
+ };
1175
+ }
1176
+ /**
1177
+ * Clear route cache (useful for development)
1178
+ */
1179
+ clearCache() {
1180
+ this.routeCache.clear();
1181
+ }
1182
+ /**
1183
+ * Clear compilation cache
1184
+ */
1185
+ clearCompilationCache() {
1186
+ this.routeCompilationCache.clear();
1187
+ }
1188
+ /**
1189
+ * Get all registered routes with detailed information
1190
+ * @returns {Array} Array of route information objects
1191
+ */
1192
+ getRoutes() {
1193
+ return this.routes.map(route => ({
1194
+ method: route.method,
1195
+ path: route.path,
1196
+ name: route.name || null,
1197
+ hasMiddleware: route.middleware && route.middleware.length > 0,
1198
+ middlewareCount: route.middleware ? route.middleware.length : 0,
1199
+ compiled: !!route.compiled,
1200
+ compiledPattern: route.compiled ? route.compiled.regex.source : null,
1201
+ paramNames: route.compiled ? route.compiled.paramNames : null
1202
+ }));
1203
+ }
1204
+ /**
1205
+ * Find routes matching a pattern or method
1206
+ * @param {Object} criteria - Search criteria
1207
+ * @returns {Array} Matching routes
1208
+ */
1209
+ findRoutes(criteria = {}) {
1210
+ const { method, path, name, hasMiddleware } = criteria;
1211
+ return this.routes.filter(route => {
1212
+ if (method && route.method !== method.toUpperCase())
1213
+ return false;
1214
+ if (path && !route.path.includes(path))
1215
+ return false;
1216
+ if (name && route.name !== name)
1217
+ return false;
1218
+ if (hasMiddleware !== undefined && !!route.middleware?.length !== hasMiddleware)
1219
+ return false;
1220
+ return true;
1221
+ }).map(route => ({
1222
+ method: route.method,
1223
+ path: route.path,
1224
+ name: route.name || null,
1225
+ middlewareCount: route.middleware ? route.middleware.length : 0
1226
+ }));
1227
+ }
1228
+ /**
1229
+ * Test route matching without executing handlers
1230
+ * @param {string} method - HTTP method
1231
+ * @param {string} path - Path to test
1232
+ * @returns {Object} Match result with route info and extracted parameters
1233
+ */
1234
+ testRoute(method, path) {
1235
+ const upperMethod = method.toUpperCase();
1236
+ for (const route of this.routes) {
1237
+ if (route.method === upperMethod) {
1238
+ let params = null;
1239
+ // Use compiled route if available
1240
+ if (this.enableCompilation && route.compiled) {
1241
+ params = this.matchCompiledRoute(route.compiled, path);
1242
+ }
1243
+ else {
1244
+ params = extractParams(route.path, path);
1245
+ }
1246
+ if (params !== null) {
1247
+ return {
1248
+ matched: true,
1249
+ route: {
1250
+ method: route.method,
1251
+ path: route.path,
1252
+ name: route.name || null,
1253
+ middlewareCount: route.middleware ? route.middleware.length : 0
1254
+ },
1255
+ params,
1256
+ compiledUsed: this.enableCompilation && !!route.compiled
1257
+ };
1258
+ }
1259
+ }
1260
+ }
1261
+ return { matched: false, route: null, params: null };
1262
+ }
1263
+ /**
1264
+ * Get router debug information
1265
+ * @returns {Object} Comprehensive debug information
1266
+ */
1267
+ getDebugInfo() {
1268
+ const routesByMethod = {};
1269
+ const namedRoutes = {};
1270
+ // Group routes by method
1271
+ this.routes.forEach(route => {
1272
+ if (!routesByMethod[route.method]) {
1273
+ routesByMethod[route.method] = [];
1274
+ }
1275
+ routesByMethod[route.method].push({
1276
+ path: route.path,
1277
+ name: route.name,
1278
+ middlewareCount: route.middleware ? route.middleware.length : 0,
1279
+ compiled: !!route.compiled
1280
+ });
1281
+ });
1282
+ // Get named routes
1283
+ this.namedRoutes.forEach((routeInfo, name) => {
1284
+ namedRoutes[name] = routeInfo;
1285
+ });
1286
+ return {
1287
+ totalRoutes: this.routes.length,
1288
+ routesByMethod,
1289
+ namedRoutes,
1290
+ globalMiddleware: this.globalMiddleware.length,
1291
+ activeGroups: this.routeGroups.length,
1292
+ cacheSize: this.routeCache.size,
1293
+ maxCacheSize: this.maxCacheSize,
1294
+ compilationEnabled: this.enableCompilation,
1295
+ compilationCacheSize: this.routeCompilationCache.size,
1296
+ metricsEnabled: this.enableMetrics
1297
+ };
1298
+ }
1299
+ async handle(req, res, options = {}) {
1300
+ const startTime = Date.now();
1301
+ // Metrics collection
1302
+ if (this.enableMetrics) {
1303
+ this.metrics.requests++;
1304
+ }
1305
+ const { corsOrigin, rateLimit = { windowMs: 60000, maxRequests: 100 } } = options;
1306
+ // Add security headers
1307
+ addSecurityHeaders(res, corsOrigin);
1308
+ // Handle preflight requests
1309
+ if (req.method === 'OPTIONS') {
1310
+ res.writeHead(204);
1311
+ res.end();
1312
+ return;
1313
+ }
1314
+ // Rate limiting
1315
+ const clientIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress || 'unknown';
1316
+ if (!checkRateLimit(clientIP, rateLimit.windowMs, rateLimit.maxRequests)) {
1317
+ res.writeHead(429, { 'Content-Type': 'application/json' });
1318
+ res.end(JSON.stringify({ error: 'Too Many Requests' }));
1319
+ return;
1320
+ }
1321
+ // Parse URL and query parameters
1322
+ const parsedUrl = parseUrl(req.url, true);
1323
+ const pathname = parsedUrl.pathname;
1324
+ req.query = parsedUrl.query || {};
1325
+ // Parse request body with size limits
1326
+ try {
1327
+ req.body = await parseBody(req, options.maxBodySize);
1328
+ }
1329
+ catch (error) {
1330
+ if (this.enableMetrics)
1331
+ this.metrics.errors++;
1332
+ const statusCode = error.message.includes('too large') ? 413 : 400;
1333
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1334
+ res.end(JSON.stringify({ error: error.message }));
1335
+ return;
1336
+ }
1337
+ // Check route cache first
1338
+ const cacheKey = `${req.method}:${pathname}`;
1339
+ let matchedRoute = this.routeCache.get(cacheKey);
1340
+ if (matchedRoute && this.enableMetrics) {
1341
+ this.metrics.cacheHits++;
1342
+ }
1343
+ if (!matchedRoute) {
1344
+ // Get request version if versioning is enabled
1345
+ const requestVersion = this.enableVersioning ? this.getRequestVersion(req) : null;
1346
+ // Track version requests in metrics
1347
+ if (this.enableMetrics && requestVersion) {
1348
+ this.metrics.versionRequests.set(requestVersion, (this.metrics.versionRequests.get(requestVersion) || 0) + 1);
1349
+ }
1350
+ // Find matching route using compiled patterns or fallback to extractParams
1351
+ const routesToSearch = this.enableVersioning && this.versionedRoutes.has(requestVersion)
1352
+ ? this.versionedRoutes.get(requestVersion)
1353
+ : this.routes;
1354
+ for (const route of routesToSearch) {
1355
+ if (route.method === req.method) {
1356
+ // Skip route if versioning is enabled and versions don't match
1357
+ if (this.enableVersioning && route.version !== requestVersion) {
1358
+ continue;
1359
+ }
1360
+ let params = null;
1361
+ // Use compiled route if available
1362
+ if (this.enableCompilation && route.compiled) {
1363
+ params = this.matchCompiledRoute(route.compiled, pathname);
1364
+ }
1365
+ else {
1366
+ // Fallback to original parameter extraction
1367
+ params = extractParams(route.path, pathname);
1368
+ }
1369
+ if (params !== null) {
1370
+ matchedRoute = { route, params };
1371
+ // Cache the match if under size limit
1372
+ if (this.routeCache.size < this.maxCacheSize) {
1373
+ this.routeCache.set(cacheKey, matchedRoute);
1374
+ }
1375
+ break;
1376
+ }
1377
+ }
1378
+ }
1379
+ }
1380
+ if (matchedRoute) {
1381
+ req.params = matchedRoute.params;
1382
+ // Record route match metrics
1383
+ if (this.enableMetrics) {
1384
+ const routeKey = `${req.method}:${matchedRoute.route.path}`;
1385
+ this.metrics.routeMatches.set(routeKey, (this.metrics.routeMatches.get(routeKey) || 0) + 1);
1386
+ }
1387
+ // Log request
1388
+ console.log(`${new Date().toISOString()} ${req.method} ${pathname}`);
1389
+ try {
1390
+ // Execute middleware chain
1391
+ if (matchedRoute.route.middleware && matchedRoute.route.middleware.length > 0) {
1392
+ for (const middleware of matchedRoute.route.middleware) {
1393
+ const result = await middleware(req, res);
1394
+ if (result)
1395
+ break; // Middleware handled response
1396
+ }
1397
+ }
1398
+ // Execute handler
1399
+ const { route } = matchedRoute;
1400
+ const result = await route.handler(req, res);
1401
+ // Only write response if handler returned data and response hasn't been sent
1402
+ if (result && typeof result === 'object' && !res.headersSent) {
1403
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1404
+ res.end(JSON.stringify(result));
1405
+ }
1406
+ // Record response time
1407
+ if (this.enableMetrics) {
1408
+ const responseTime = Date.now() - startTime;
1409
+ this.metrics.responseTime.push(responseTime);
1410
+ // Keep only last 1000 response times to prevent memory growth
1411
+ if (this.metrics.responseTime.length > 1000) {
1412
+ this.metrics.responseTime = this.metrics.responseTime.slice(-1000);
1413
+ }
1414
+ }
1415
+ return;
1416
+ }
1417
+ catch (error) {
1418
+ if (this.enableMetrics)
1419
+ this.metrics.errors++;
1420
+ if (!res.headersSent) {
1421
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1422
+ res.end(JSON.stringify({ error: error.message }));
1423
+ }
1424
+ return;
1425
+ }
1426
+ }
1427
+ // No route found
1428
+ if (this.enableMetrics)
1429
+ this.metrics.errors++;
1430
+ if (!res.headersSent) {
1431
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1432
+ res.end(JSON.stringify({ error: 'Not Found' }));
1433
+ }
1434
+ }
1435
+ createServer(options = {}) {
1436
+ const mergedOptions = { ...this.defaultOptions, ...options };
1437
+ return createServer((req, res) => this.handle(req, res, mergedOptions));
1438
+ }
1439
+ // HTTP convenience methods
1440
+ get(path, handler, options = {}) {
1441
+ return this.addRoute('GET', path, handler, options);
1442
+ }
1443
+ post(path, handler, options = {}) {
1444
+ return this.addRoute('POST', path, handler, options);
1445
+ }
1446
+ put(path, handler, options = {}) {
1447
+ return this.addRoute('PUT', path, handler, options);
1448
+ }
1449
+ patch(path, handler, options = {}) {
1450
+ return this.addRoute('PATCH', path, handler, options);
1451
+ }
1452
+ delete(path, handler, options = {}) {
1453
+ return this.addRoute('DELETE', path, handler, options);
1454
+ }
1455
+ options(path, handler, options = {}) {
1456
+ return this.addRoute('OPTIONS', path, handler, options);
1457
+ }
1458
+ head(path, handler, options = {}) {
1459
+ return this.addRoute('HEAD', path, handler, options);
1460
+ }
1461
+ }
1462
+ /**
1463
+ * Creates an object-based router from nested route definitions
1464
+ *
1465
+ * @param {Object} routes - Nested route definition object
1466
+ * @param {Object} options - Router options (corsOrigin, rateLimit, maxBodySize)
1467
+ * @returns {Object} Configured router instance
1468
+ *
1469
+ * @example
1470
+ * const routes = {
1471
+ * api: {
1472
+ * users: {
1473
+ * get: { handler: () => ({ users: [] }) },
1474
+ * post: {
1475
+ * validation: userSchema,
1476
+ * handler: (req) => ({ user: req.body })
1477
+ * }
1478
+ * }
1479
+ * }
1480
+ * };
1481
+ *
1482
+ * const router = createObjectRouter(routes, {
1483
+ * corsOrigin: 'https://myapp.com',
1484
+ * rateLimit: { windowMs: 60000, maxRequests: 50 }
1485
+ * });
1486
+ * const server = router.createServer();
1487
+ * server.listen(3000);
1488
+ */
1489
+ /**
1490
+ * Factory function to create a SimpleRouter instance
1491
+ *
1492
+ * @param {Object} options - Router options
1493
+ * @returns {SimpleRouter} Router instance
1494
+ */
1495
+ export function createSimpleRouter(options = {}) {
1496
+ return new SimpleRouter(options);
1497
+ }
1498
+ export function createObjectRouter(routeConfig, options = {}) {
1499
+ const router = new SimpleRouter(options);
1500
+ router.defaultOptions = options; // Store default options
1501
+ if (routeConfig) {
1502
+ router.addRoutes(routeConfig);
1503
+ }
1504
+ return router;
1505
+ }
1506
+ export { SimpleRouter };
1507
+ export default createObjectRouter;
1508
+ //# sourceMappingURL=router.js.map